Compare commits
10
Commits
8c3bd3c415
...
dab21b6710
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dab21b6710 | ||
|
|
144b5ce45e | ||
|
|
b3d75f8cc2 | ||
|
|
0f9b6fcc5d | ||
|
|
97d8b277d5 | ||
|
|
fbb8263b13 | ||
|
|
4235f8692c | ||
|
|
39b1da37c9 | ||
|
|
e316e18d98 | ||
|
|
690022cea3 |
No files matched your search
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 shuiyu1123
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,206 @@
|
||||
# Android-pyApi
|
||||
|
||||
> **Android API for Python** — 在桌面/Linux 上原生运行 Android API 的 Python 库
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](#)
|
||||
|
||||
## 这是什么
|
||||
|
||||
Android-pyApi 将完整的 Android SDK API 翻译为纯 Python 实现,让你在桌面环境直接调用 Android API——不需要模拟器、不需要真机、不需要 Android Studio。
|
||||
|
||||
**213 个模块,178,000+ 行代码,覆盖 android/java/javax/org/dalvik 全包。**
|
||||
|
||||
在 Linux 上,系统级 API(网络、传感器、蓝牙、音频、位置等)使用真实系统调用,返回真实数据。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### API 覆盖
|
||||
|
||||
| 包 | 模块数 | 内容 |
|
||||
|---|---|---|
|
||||
| `android.*` | 167 | app, content, os, view, widget, graphics, media, hardware, net, telephony, provider 等 |
|
||||
| `java.*` | 35 | lang, io, util, net, nio, security, crypto, sql, math, time 等 |
|
||||
| `javax.*` | 4 | net, crypto, security, sql |
|
||||
| `org.*` | 5 | json, w3c_dom, xml_sax, xml_pull, apache_http |
|
||||
| `dalvik.*` | 1 | system |
|
||||
|
||||
### Linux 真实实现
|
||||
|
||||
在 Linux 环境下,以下 API 使用真实系统调用而非存根:
|
||||
|
||||
| API | 实现方式 | 真实行为 |
|
||||
|-----|---------|---------|
|
||||
| ConnectivityManager | `ip` 命令 + `/proc/net` | 真实 IP、接口、DNS、网关 |
|
||||
| LocationManager | IP 地理定位 | 真实经纬度、城市、ISP |
|
||||
| SensorManager | `/sys/class/hwmon` | 真实温度、CPU 频率、风扇 |
|
||||
| AudioManager | `amixer` (ALSA) | 真实音量控制 |
|
||||
| BluetoothAdapter | `bluetoothctl` (BlueZ) | 真实扫描、配对、连接 |
|
||||
| ClipboardManager | `xclip` | 真实剪贴板读写 |
|
||||
| PowerManager / BatteryManager | `psutil` + `/sys/class/power_supply` | 真实 CPU、内存、电池 |
|
||||
| TelephonyManager | MAC 地址 + 内核版本 | 真实设备 ID |
|
||||
| NotificationManager | `notify-send` | 真实桌面通知 |
|
||||
| Activity / Display | `xdotool` + X11 | 真实屏幕尺寸、截屏 |
|
||||
| ActivityManager | `psutil` + `/proc` | 真实进程列表 |
|
||||
| Shell | `subprocess` | 真实命令执行 |
|
||||
| Camera | OpenCV / V4L2 | 真实拍照 |
|
||||
| UI 自动化 | `xdotool` | 真实点击、输入、按键 |
|
||||
|
||||
### Windows 真实实现
|
||||
|
||||
在 Windows 环境下,以下 API 使用真实系统调用而非存根:
|
||||
|
||||
| API | 实现方式 | 真实行为 |
|
||||
|-----|---------|---------|
|
||||
| ConnectivityManager | `ipconfig` + `netsh` + `socket` | 真实 IP、接口、DNS、网关、WiFi 信息 |
|
||||
| LocationManager | IP 地理定位 | 真实经纬度、城市、ISP |
|
||||
| SensorManager | `wmi` + `psutil` | 真实温度、CPU 频率、风扇转速 |
|
||||
| AudioManager | `pycaw` / PowerShell | 真实音量控制、静音 |
|
||||
| BluetoothAdapter | PowerShell Bluetooth cmdlets | 真实设备列表、配对状态 |
|
||||
| ClipboardManager | `pyperclip` | 真实剪贴板读写 |
|
||||
| PowerManager / BatteryManager | `psutil` | 真实 CPU、内存、电池状态 |
|
||||
| NotificationManager | `win10toast` / PowerShell | 真实桌面通知 (Toast) |
|
||||
| Activity / Display | `pyautogui` + `screeninfo` + `win32api` | 真实屏幕尺寸、截屏 |
|
||||
| ActivityManager | `psutil` + `tasklist` | 真实进程列表 |
|
||||
| Shell | `subprocess` + PowerShell | 真实命令执行 |
|
||||
| Camera | OpenCV | 真实拍照 |
|
||||
| UI 自动化 | `pyautogui` + `win32gui` | 真实点击、输入、按键、窗口操作 |
|
||||
|
||||
### 跨平台
|
||||
|
||||
- **Linux** — 系统级 API 使用真实实现 (`_linux_impl.py`)
|
||||
- **Windows** — 系统级 API 使用真实实现 (`_windows_impl.py`)
|
||||
- **macOS** — 系统级 API 使用存根(后续支持)
|
||||
- **API 形状** — 所有平台一致,方法签名完全对齐 Android SDK
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 从源码安装
|
||||
git clone https://gitee.com/shuiyu1123/Android-pyApi.git
|
||||
cd Android-pyApi
|
||||
pip install -e .
|
||||
|
||||
# 或直接使用
|
||||
export PYTHONPATH=/path/to/Android-pyApi:$PYTHONPATH
|
||||
```
|
||||
|
||||
### 可选依赖
|
||||
|
||||
```bash
|
||||
# Python 库 (所有平台通用)
|
||||
pip install psutil requests Pillow opencv-python numpy
|
||||
|
||||
# Linux 系统工具
|
||||
sudo apt install xclip xdotool libnotify-bin alsa-utils bluez lm-sensors
|
||||
|
||||
# Windows 专用库 (推荐安装以启用完整功能)
|
||||
pip install pyautogui pyperclip screeninfo pywin32 wmi pycaw comtypes win10toast
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```python
|
||||
from android_pyapi import Intent, Uri, Bundle, File, JSONObject, ArrayList, HashMap
|
||||
|
||||
# Intent — Android 最核心的 API
|
||||
intent = Intent("android.intent.action.VIEW")
|
||||
intent.setData(Uri.parse("https://example.com"))
|
||||
intent.putExtra("key", "value")
|
||||
print(intent.getAction()) # android.intent.action.VIEW
|
||||
print(intent.getData()) # https://example.com
|
||||
print(intent.getStringExtra("key")) # value
|
||||
|
||||
# java.util 集合
|
||||
lst = ArrayList()
|
||||
lst.add("hello")
|
||||
lst.add("world")
|
||||
print(lst.size()) # 2
|
||||
print(lst.get(0)) # hello
|
||||
|
||||
# JSONObject
|
||||
json = JSONObject()
|
||||
json.put("name", "test")
|
||||
json.put("version", 42)
|
||||
print(json.toString()) # {"name":"test","version":42}
|
||||
|
||||
# Linux 真实实现
|
||||
from android_pyapi import Build, LocationManager, ConnectivityManager
|
||||
print(Build.MODEL) # 真实主机名
|
||||
print(Build.MANUFACTURER) # Linux
|
||||
|
||||
loc = LocationManager().getLastKnownLocation("network")
|
||||
print(loc) # 真实经纬度
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
android_pyapi/
|
||||
├── _core.py # Stub 基类、全局上下文
|
||||
├── _linux_impl.py # Linux 真实实现层 (14个实现类)
|
||||
├── _windows_impl.py # Windows 真实实现层 (14个实现类)
|
||||
├── __init__.py # 统一导出
|
||||
├── android/ # 167 个 Android 模块
|
||||
│ ├── app.py # Activity, Service, Fragment, Notification
|
||||
│ ├── content.py # Context, Intent, ContentResolver, ClipboardManager
|
||||
│ ├── os.py # Handler, Looper, Bundle, Build, PowerManager
|
||||
│ ├── view.py # View, ViewGroup, MotionEvent, KeyEvent
|
||||
│ ├── widget.py # TextView, Button, ListView, RecyclerView, Toast
|
||||
│ ├── graphics.py # Bitmap, Canvas, Paint, Color, Path, Matrix
|
||||
│ ├── hardware.py # SensorManager, Camera, BiometricPrompt
|
||||
│ ├── net.py # ConnectivityManager, NetworkInfo
|
||||
│ ├── telephony.py # TelephonyManager, SmsManager
|
||||
│ ├── location.py # LocationManager, Location, Geocoder
|
||||
│ ├── media.py # AudioManager, MediaPlayer, MediaRecorder
|
||||
│ └── ...
|
||||
├── java/ # 35 个 Java 标准库模块
|
||||
│ ├── lang.py # String, Math, System, Thread, Exception 体系
|
||||
│ ├── io.py # File, InputStream, OutputStream, Reader, Writer
|
||||
│ ├── util.py # ArrayList, HashMap, Calendar, Collections, UUID
|
||||
│ ├── net.py # URL, HttpURLConnection, Socket
|
||||
│ └── ...
|
||||
├── javax/ # 4 个 javax 模块
|
||||
├── org/ # 5 个 org 模块
|
||||
└── dalvik/ # 1 个 dalvik 模块
|
||||
```
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 不是模拟器
|
||||
|
||||
Android-pyApi 不是 Android 模拟器。它不包含 ART 虚拟机、SurfaceFlinger、binder 等底层基础设施。
|
||||
|
||||
它是 **API 兼容层**:
|
||||
- **API 形状** — 类名、方法签名、常量值完全对齐 Android SDK
|
||||
- **系统级行为** — 在 Linux 上用真实系统调用实现
|
||||
- **框架级行为** — Activity 生命周期、View 渲染等保持存根(桌面无对应框架)
|
||||
|
||||
### 渐进式真实化
|
||||
|
||||
```
|
||||
存根 (所有平台) → Linux 真实实现 → macOS 真实实现 → Windows 真实实现
|
||||
```
|
||||
|
||||
每个 API 从"返回硬编码值"逐步演进为"调用真实系统功能"。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **Android 脚本开发** — 在桌面编写和调试依赖 Android API 的 Python 脚本
|
||||
- **跨平台工具** — 用 Android API 风格编写跨平台系统工具
|
||||
- **学习 Android API** — 不需要 Android Studio 就能探索 API
|
||||
- **自动化测试** — 在 CI/CD 中测试 Android API 调用逻辑
|
||||
- **IoT / 嵌入式** — 在 Linux 设备上用 Android API 风格操作硬件
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献!特别需要:
|
||||
|
||||
- macOS 真实实现层 (`_macos_impl.py`)
|
||||
- 更多 API 的真实行为实现
|
||||
- 测试用例
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -0,0 +1,264 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: android-pyapi
|
||||
Version: 1.0.0
|
||||
Summary: Android API for Python — 在桌面/Linux 上原生运行 Android API
|
||||
Home-page: https://gitee.com/shuiyu1123/Android-pyApi
|
||||
Author: shuiyu1123
|
||||
License: MIT
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Operating System :: MacOS
|
||||
Classifier: Operating System :: Microsoft :: Windows
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Topic :: System :: Hardware
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Provides-Extra: linux
|
||||
Requires-Dist: psutil; extra == "linux"
|
||||
Requires-Dist: requests; extra == "linux"
|
||||
Requires-Dist: Pillow; extra == "linux"
|
||||
Requires-Dist: opencv-python; extra == "linux"
|
||||
Requires-Dist: numpy; extra == "linux"
|
||||
Provides-Extra: windows
|
||||
Requires-Dist: psutil; extra == "windows"
|
||||
Requires-Dist: requests; extra == "windows"
|
||||
Requires-Dist: Pillow; extra == "windows"
|
||||
Requires-Dist: opencv-python; extra == "windows"
|
||||
Requires-Dist: numpy; extra == "windows"
|
||||
Requires-Dist: pyautogui; extra == "windows"
|
||||
Requires-Dist: pyperclip; extra == "windows"
|
||||
Requires-Dist: screeninfo; extra == "windows"
|
||||
Requires-Dist: pywin32; extra == "windows"
|
||||
Requires-Dist: wmi; extra == "windows"
|
||||
Requires-Dist: pycaw; extra == "windows"
|
||||
Requires-Dist: comtypes; extra == "windows"
|
||||
Requires-Dist: win10toast; extra == "windows"
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: pytest; extra == "dev"
|
||||
Requires-Dist: pytest-cov; extra == "dev"
|
||||
Dynamic: author
|
||||
Dynamic: classifier
|
||||
Dynamic: description
|
||||
Dynamic: description-content-type
|
||||
Dynamic: home-page
|
||||
Dynamic: license
|
||||
Dynamic: license-file
|
||||
Dynamic: provides-extra
|
||||
Dynamic: requires-python
|
||||
Dynamic: summary
|
||||
|
||||
# Android-pyApi
|
||||
|
||||
> **Android API for Python** — 在桌面/Linux 上原生运行 Android API 的 Python 库
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](#)
|
||||
|
||||
## 这是什么
|
||||
|
||||
Android-pyApi 将完整的 Android SDK API 翻译为纯 Python 实现,让你在桌面环境直接调用 Android API——不需要模拟器、不需要真机、不需要 Android Studio。
|
||||
|
||||
**213 个模块,178,000+ 行代码,覆盖 android/java/javax/org/dalvik 全包。**
|
||||
|
||||
在 Linux 上,系统级 API(网络、传感器、蓝牙、音频、位置等)使用真实系统调用,返回真实数据。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### API 覆盖
|
||||
|
||||
| 包 | 模块数 | 内容 |
|
||||
|---|---|---|
|
||||
| `android.*` | 167 | app, content, os, view, widget, graphics, media, hardware, net, telephony, provider 等 |
|
||||
| `java.*` | 35 | lang, io, util, net, nio, security, crypto, sql, math, time 等 |
|
||||
| `javax.*` | 4 | net, crypto, security, sql |
|
||||
| `org.*` | 5 | json, w3c_dom, xml_sax, xml_pull, apache_http |
|
||||
| `dalvik.*` | 1 | system |
|
||||
|
||||
### Linux 真实实现
|
||||
|
||||
在 Linux 环境下,以下 API 使用真实系统调用而非存根:
|
||||
|
||||
| API | 实现方式 | 真实行为 |
|
||||
|-----|---------|---------|
|
||||
| ConnectivityManager | `ip` 命令 + `/proc/net` | 真实 IP、接口、DNS、网关 |
|
||||
| LocationManager | IP 地理定位 | 真实经纬度、城市、ISP |
|
||||
| SensorManager | `/sys/class/hwmon` | 真实温度、CPU 频率、风扇 |
|
||||
| AudioManager | `amixer` (ALSA) | 真实音量控制 |
|
||||
| BluetoothAdapter | `bluetoothctl` (BlueZ) | 真实扫描、配对、连接 |
|
||||
| ClipboardManager | `xclip` | 真实剪贴板读写 |
|
||||
| PowerManager / BatteryManager | `psutil` + `/sys/class/power_supply` | 真实 CPU、内存、电池 |
|
||||
| TelephonyManager | MAC 地址 + 内核版本 | 真实设备 ID |
|
||||
| NotificationManager | `notify-send` | 真实桌面通知 |
|
||||
| Activity / Display | `xdotool` + X11 | 真实屏幕尺寸、截屏 |
|
||||
| ActivityManager | `psutil` + `/proc` | 真实进程列表 |
|
||||
| Shell | `subprocess` | 真实命令执行 |
|
||||
| Camera | OpenCV / V4L2 | 真实拍照 |
|
||||
| UI 自动化 | `xdotool` | 真实点击、输入、按键 |
|
||||
|
||||
### Windows 真实实现
|
||||
|
||||
在 Windows 环境下,以下 API 使用真实系统调用而非存根:
|
||||
|
||||
| API | 实现方式 | 真实行为 |
|
||||
|-----|---------|---------|
|
||||
| ConnectivityManager | `ipconfig` + `netsh` + `socket` | 真实 IP、接口、DNS、网关、WiFi 信息 |
|
||||
| LocationManager | IP 地理定位 | 真实经纬度、城市、ISP |
|
||||
| SensorManager | `wmi` + `psutil` | 真实温度、CPU 频率、风扇转速 |
|
||||
| AudioManager | `pycaw` / PowerShell | 真实音量控制、静音 |
|
||||
| BluetoothAdapter | PowerShell Bluetooth cmdlets | 真实设备列表、配对状态 |
|
||||
| ClipboardManager | `pyperclip` | 真实剪贴板读写 |
|
||||
| PowerManager / BatteryManager | `psutil` | 真实 CPU、内存、电池状态 |
|
||||
| NotificationManager | `win10toast` / PowerShell | 真实桌面通知 (Toast) |
|
||||
| Activity / Display | `pyautogui` + `screeninfo` + `win32api` | 真实屏幕尺寸、截屏 |
|
||||
| ActivityManager | `psutil` + `tasklist` | 真实进程列表 |
|
||||
| Shell | `subprocess` + PowerShell | 真实命令执行 |
|
||||
| Camera | OpenCV | 真实拍照 |
|
||||
| UI 自动化 | `pyautogui` + `win32gui` | 真实点击、输入、按键、窗口操作 |
|
||||
|
||||
### 跨平台
|
||||
|
||||
- **Linux** — 系统级 API 使用真实实现 (`_linux_impl.py`)
|
||||
- **Windows** — 系统级 API 使用真实实现 (`_windows_impl.py`)
|
||||
- **macOS** — 系统级 API 使用存根(后续支持)
|
||||
- **API 形状** — 所有平台一致,方法签名完全对齐 Android SDK
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 从源码安装
|
||||
git clone https://gitee.com/shuiyu1123/Android-pyApi.git
|
||||
cd Android-pyApi
|
||||
pip install -e .
|
||||
|
||||
# 或直接使用
|
||||
export PYTHONPATH=/path/to/Android-pyApi:$PYTHONPATH
|
||||
```
|
||||
|
||||
### 可选依赖
|
||||
|
||||
```bash
|
||||
# Python 库 (所有平台通用)
|
||||
pip install psutil requests Pillow opencv-python numpy
|
||||
|
||||
# Linux 系统工具
|
||||
sudo apt install xclip xdotool libnotify-bin alsa-utils bluez lm-sensors
|
||||
|
||||
# Windows 专用库 (推荐安装以启用完整功能)
|
||||
pip install pyautogui pyperclip screeninfo pywin32 wmi pycaw comtypes win10toast
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```python
|
||||
from android_pyapi import Intent, Uri, Bundle, File, JSONObject, ArrayList, HashMap
|
||||
|
||||
# Intent — Android 最核心的 API
|
||||
intent = Intent("android.intent.action.VIEW")
|
||||
intent.setData(Uri.parse("https://example.com"))
|
||||
intent.putExtra("key", "value")
|
||||
print(intent.getAction()) # android.intent.action.VIEW
|
||||
print(intent.getData()) # https://example.com
|
||||
print(intent.getStringExtra("key")) # value
|
||||
|
||||
# java.util 集合
|
||||
lst = ArrayList()
|
||||
lst.add("hello")
|
||||
lst.add("world")
|
||||
print(lst.size()) # 2
|
||||
print(lst.get(0)) # hello
|
||||
|
||||
# JSONObject
|
||||
json = JSONObject()
|
||||
json.put("name", "test")
|
||||
json.put("version", 42)
|
||||
print(json.toString()) # {"name":"test","version":42}
|
||||
|
||||
# Linux 真实实现
|
||||
from android_pyapi import Build, LocationManager, ConnectivityManager
|
||||
print(Build.MODEL) # 真实主机名
|
||||
print(Build.MANUFACTURER) # Linux
|
||||
|
||||
loc = LocationManager().getLastKnownLocation("network")
|
||||
print(loc) # 真实经纬度
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
android_pyapi/
|
||||
├── _core.py # Stub 基类、全局上下文
|
||||
├── _linux_impl.py # Linux 真实实现层 (14个实现类)
|
||||
├── _windows_impl.py # Windows 真实实现层 (14个实现类)
|
||||
├── __init__.py # 统一导出
|
||||
├── android/ # 167 个 Android 模块
|
||||
│ ├── app.py # Activity, Service, Fragment, Notification
|
||||
│ ├── content.py # Context, Intent, ContentResolver, ClipboardManager
|
||||
│ ├── os.py # Handler, Looper, Bundle, Build, PowerManager
|
||||
│ ├── view.py # View, ViewGroup, MotionEvent, KeyEvent
|
||||
│ ├── widget.py # TextView, Button, ListView, RecyclerView, Toast
|
||||
│ ├── graphics.py # Bitmap, Canvas, Paint, Color, Path, Matrix
|
||||
│ ├── hardware.py # SensorManager, Camera, BiometricPrompt
|
||||
│ ├── net.py # ConnectivityManager, NetworkInfo
|
||||
│ ├── telephony.py # TelephonyManager, SmsManager
|
||||
│ ├── location.py # LocationManager, Location, Geocoder
|
||||
│ ├── media.py # AudioManager, MediaPlayer, MediaRecorder
|
||||
│ └── ...
|
||||
├── java/ # 35 个 Java 标准库模块
|
||||
│ ├── lang.py # String, Math, System, Thread, Exception 体系
|
||||
│ ├── io.py # File, InputStream, OutputStream, Reader, Writer
|
||||
│ ├── util.py # ArrayList, HashMap, Calendar, Collections, UUID
|
||||
│ ├── net.py # URL, HttpURLConnection, Socket
|
||||
│ └── ...
|
||||
├── javax/ # 4 个 javax 模块
|
||||
├── org/ # 5 个 org 模块
|
||||
└── dalvik/ # 1 个 dalvik 模块
|
||||
```
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 不是模拟器
|
||||
|
||||
Android-pyApi 不是 Android 模拟器。它不包含 ART 虚拟机、SurfaceFlinger、binder 等底层基础设施。
|
||||
|
||||
它是 **API 兼容层**:
|
||||
- **API 形状** — 类名、方法签名、常量值完全对齐 Android SDK
|
||||
- **系统级行为** — 在 Linux 上用真实系统调用实现
|
||||
- **框架级行为** — Activity 生命周期、View 渲染等保持存根(桌面无对应框架)
|
||||
|
||||
### 渐进式真实化
|
||||
|
||||
```
|
||||
存根 (所有平台) → Linux 真实实现 → macOS 真实实现 → Windows 真实实现
|
||||
```
|
||||
|
||||
每个 API 从"返回硬编码值"逐步演进为"调用真实系统功能"。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **Android 脚本开发** — 在桌面编写和调试依赖 Android API 的 Python 脚本
|
||||
- **跨平台工具** — 用 Android API 风格编写跨平台系统工具
|
||||
- **学习 Android API** — 不需要 Android Studio 就能探索 API
|
||||
- **自动化测试** — 在 CI/CD 中测试 Android API 调用逻辑
|
||||
- **IoT / 嵌入式** — 在 Linux 设备上用 Android API 风格操作硬件
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献!特别需要:
|
||||
|
||||
- macOS 真实实现层 (`_macos_impl.py`)
|
||||
- 更多 API 的真实行为实现
|
||||
- 测试用例
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -0,0 +1,230 @@
|
||||
LICENSE
|
||||
README.md
|
||||
setup.py
|
||||
android_pyapi/__init__.py
|
||||
android_pyapi/_core.py
|
||||
android_pyapi/_linux_impl.py
|
||||
android_pyapi/_windows_impl.py
|
||||
android_pyapi.egg-info/PKG-INFO
|
||||
android_pyapi.egg-info/SOURCES.txt
|
||||
android_pyapi.egg-info/dependency_links.txt
|
||||
android_pyapi.egg-info/requires.txt
|
||||
android_pyapi.egg-info/top_level.txt
|
||||
android_pyapi/android/__init__.py
|
||||
android_pyapi/android/accessibilityservice.py
|
||||
android_pyapi/android/accounts.py
|
||||
android_pyapi/android/animation.py
|
||||
android_pyapi/android/app.py
|
||||
android_pyapi/android/app_actionbar.py
|
||||
android_pyapi/android/app_admin.py
|
||||
android_pyapi/android/app_assist.py
|
||||
android_pyapi/android/app_backup.py
|
||||
android_pyapi/android/app_dialog.py
|
||||
android_pyapi/android/app_fragment.py
|
||||
android_pyapi/android/app_job.py
|
||||
android_pyapi/android/app_notification.py
|
||||
android_pyapi/android/app_people.py
|
||||
android_pyapi/android/app_role.py
|
||||
android_pyapi/android/app_search.py
|
||||
android_pyapi/android/app_slice.py
|
||||
android_pyapi/android/app_tasks.py
|
||||
android_pyapi/android/app_time.py
|
||||
android_pyapi/android/app_usage.py
|
||||
android_pyapi/android/app_voiceinteractor.py
|
||||
android_pyapi/android/app_wallpaper.py
|
||||
android_pyapi/android/appwidget.py
|
||||
android_pyapi/android/bluetooth.py
|
||||
android_pyapi/android/bluetooth_le.py
|
||||
android_pyapi/android/companion.py
|
||||
android_pyapi/android/companion_ext.py
|
||||
android_pyapi/android/content.py
|
||||
android_pyapi/android/content_intent.py
|
||||
android_pyapi/android/content_om.py
|
||||
android_pyapi/android/content_pm.py
|
||||
android_pyapi/android/content_res.py
|
||||
android_pyapi/android/content_sync.py
|
||||
android_pyapi/android/credentials.py
|
||||
android_pyapi/android/database.py
|
||||
android_pyapi/android/database_ext.py
|
||||
android_pyapi/android/database_sqlite.py
|
||||
android_pyapi/android/drm.py
|
||||
android_pyapi/android/gesture.py
|
||||
android_pyapi/android/gesture_ext.py
|
||||
android_pyapi/android/graphics.py
|
||||
android_pyapi/android/graphics_drawable.py
|
||||
android_pyapi/android/graphics_drawable_ext.py
|
||||
android_pyapi/android/graphics_drawable_shapes.py
|
||||
android_pyapi/android/graphics_fonts.py
|
||||
android_pyapi/android/graphics_pdf.py
|
||||
android_pyapi/android/graphics_surface.py
|
||||
android_pyapi/android/graphics_text.py
|
||||
android_pyapi/android/hardware.py
|
||||
android_pyapi/android/hardware_biometrics.py
|
||||
android_pyapi/android/hardware_camera2.py
|
||||
android_pyapi/android/hardware_camera2_params.py
|
||||
android_pyapi/android/hardware_consumerir.py
|
||||
android_pyapi/android/hardware_display.py
|
||||
android_pyapi/android/hardware_fingerprint.py
|
||||
android_pyapi/android/hardware_hdmi.py
|
||||
android_pyapi/android/hardware_input.py
|
||||
android_pyapi/android/hardware_light.py
|
||||
android_pyapi/android/hardware_location.py
|
||||
android_pyapi/android/hardware_radio.py
|
||||
android_pyapi/android/hardware_sensor_ext.py
|
||||
android_pyapi/android/hardware_serial.py
|
||||
android_pyapi/android/hardware_thermal.py
|
||||
android_pyapi/android/hardware_usb.py
|
||||
android_pyapi/android/inputmethod.py
|
||||
android_pyapi/android/inputmethodservice.py
|
||||
android_pyapi/android/location.py
|
||||
android_pyapi/android/location_ext.py
|
||||
android_pyapi/android/location_geofence.py
|
||||
android_pyapi/android/managers.py
|
||||
android_pyapi/android/media.py
|
||||
android_pyapi/android/media_async.py
|
||||
android_pyapi/android/media_audio.py
|
||||
android_pyapi/android/media_audiofx.py
|
||||
android_pyapi/android/media_browse.py
|
||||
android_pyapi/android/media_capture.py
|
||||
android_pyapi/android/media_effect.py
|
||||
android_pyapi/android/media_face.py
|
||||
android_pyapi/android/media_image.py
|
||||
android_pyapi/android/media_midi.py
|
||||
android_pyapi/android/media_projection.py
|
||||
android_pyapi/android/media_remoteblob.py
|
||||
android_pyapi/android/media_session.py
|
||||
android_pyapi/android/media_tv.py
|
||||
android_pyapi/android/media_tv_ext.py
|
||||
android_pyapi/android/mtp.py
|
||||
android_pyapi/android/net.py
|
||||
android_pyapi/android/net_ethernet.py
|
||||
android_pyapi/android/net_ext.py
|
||||
android_pyapi/android/net_hotspot2.py
|
||||
android_pyapi/android/net_http.py
|
||||
android_pyapi/android/net_nsd.py
|
||||
android_pyapi/android/net_rtp.py
|
||||
android_pyapi/android/net_sip.py
|
||||
android_pyapi/android/net_ssl.py
|
||||
android_pyapi/android/net_vpn.py
|
||||
android_pyapi/android/net_wifi.py
|
||||
android_pyapi/android/net_wifi_aware.py
|
||||
android_pyapi/android/net_wifi_rtt.py
|
||||
android_pyapi/android/nfc.py
|
||||
android_pyapi/android/nfc_ext.py
|
||||
android_pyapi/android/opengl.py
|
||||
android_pyapi/android/os.py
|
||||
android_pyapi/android/os_health.py
|
||||
android_pyapi/android/os_power.py
|
||||
android_pyapi/android/os_storage.py
|
||||
android_pyapi/android/os_user.py
|
||||
android_pyapi/android/permission.py
|
||||
android_pyapi/android/preference.py
|
||||
android_pyapi/android/print.py
|
||||
android_pyapi/android/printservice.py
|
||||
android_pyapi/android/provider.py
|
||||
android_pyapi/android/provider_calendar.py
|
||||
android_pyapi/android/provider_contacts.py
|
||||
android_pyapi/android/provider_settings.py
|
||||
android_pyapi/android/renderscript.py
|
||||
android_pyapi/android/se.py
|
||||
android_pyapi/android/security.py
|
||||
android_pyapi/android/service_autofill.py
|
||||
android_pyapi/android/service_controls.py
|
||||
android_pyapi/android/service_dreams.py
|
||||
android_pyapi/android/service_media.py
|
||||
android_pyapi/android/service_notification.py
|
||||
android_pyapi/android/service_quicksettings.py
|
||||
android_pyapi/android/service_voice.py
|
||||
android_pyapi/android/service_vr.py
|
||||
android_pyapi/android/service_wallpaper.py
|
||||
android_pyapi/android/sip.py
|
||||
android_pyapi/android/speech.py
|
||||
android_pyapi/android/speech_tts.py
|
||||
android_pyapi/android/system.py
|
||||
android_pyapi/android/telecom.py
|
||||
android_pyapi/android/telephony.py
|
||||
android_pyapi/android/telephony_cdma.py
|
||||
android_pyapi/android/telephony_data.py
|
||||
android_pyapi/android/telephony_euicc.py
|
||||
android_pyapi/android/telephony_gsm.py
|
||||
android_pyapi/android/telephony_ims.py
|
||||
android_pyapi/android/telephony_mbms.py
|
||||
android_pyapi/android/test.py
|
||||
android_pyapi/android/text.py
|
||||
android_pyapi/android/text_format.py
|
||||
android_pyapi/android/text_method.py
|
||||
android_pyapi/android/text_style.py
|
||||
android_pyapi/android/text_util.py
|
||||
android_pyapi/android/transition.py
|
||||
android_pyapi/android/util.py
|
||||
android_pyapi/android/uwb.py
|
||||
android_pyapi/android/view.py
|
||||
android_pyapi/android/view_accessibility.py
|
||||
android_pyapi/android/view_accessibility_ext.py
|
||||
android_pyapi/android/view_animation.py
|
||||
android_pyapi/android/view_animator.py
|
||||
android_pyapi/android/view_autofill.py
|
||||
android_pyapi/android/view_dragndrop.py
|
||||
android_pyapi/android/view_ext.py
|
||||
android_pyapi/android/view_gesture.py
|
||||
android_pyapi/android/view_haptic.py
|
||||
android_pyapi/android/view_inputmethod.py
|
||||
android_pyapi/android/view_textclassifier.py
|
||||
android_pyapi/android/view_textservice.py
|
||||
android_pyapi/android/view_translation.py
|
||||
android_pyapi/android/view_window.py
|
||||
android_pyapi/android/webkit.py
|
||||
android_pyapi/android/webkit_ext.py
|
||||
android_pyapi/android/widget.py
|
||||
android_pyapi/android/widget_adapter.py
|
||||
android_pyapi/android/widget_ext.py
|
||||
android_pyapi/dalvik/__init__.py
|
||||
android_pyapi/dalvik/system.py
|
||||
android_pyapi/java/__init__.py
|
||||
android_pyapi/java/beans.py
|
||||
android_pyapi/java/crypto.py
|
||||
android_pyapi/java/io.py
|
||||
android_pyapi/java/io_ext.py
|
||||
android_pyapi/java/io_file.py
|
||||
android_pyapi/java/lang.py
|
||||
android_pyapi/java/lang_annotation.py
|
||||
android_pyapi/java/lang_invoke.py
|
||||
android_pyapi/java/lang_management.py
|
||||
android_pyapi/java/lang_ref.py
|
||||
android_pyapi/java/lang_reflect.py
|
||||
android_pyapi/java/math.py
|
||||
android_pyapi/java/net.py
|
||||
android_pyapi/java/nio.py
|
||||
android_pyapi/java/nio_byteorder.py
|
||||
android_pyapi/java/nio_channels.py
|
||||
android_pyapi/java/nio_charset.py
|
||||
android_pyapi/java/nio_file.py
|
||||
android_pyapi/java/rmi.py
|
||||
android_pyapi/java/security.py
|
||||
android_pyapi/java/security_cert.py
|
||||
android_pyapi/java/security_spec.py
|
||||
android_pyapi/java/sql.py
|
||||
android_pyapi/java/text.py
|
||||
android_pyapi/java/time.py
|
||||
android_pyapi/java/util.py
|
||||
android_pyapi/java/util_concurrent.py
|
||||
android_pyapi/java/util_concurrent_atomic.py
|
||||
android_pyapi/java/util_concurrent_locks.py
|
||||
android_pyapi/java/util_function.py
|
||||
android_pyapi/java/util_logging.py
|
||||
android_pyapi/java/util_prefs.py
|
||||
android_pyapi/java/util_regex.py
|
||||
android_pyapi/java/util_stream.py
|
||||
android_pyapi/java/util_zip.py
|
||||
android_pyapi/javax/__init__.py
|
||||
android_pyapi/javax/crypto.py
|
||||
android_pyapi/javax/net.py
|
||||
android_pyapi/javax/security.py
|
||||
android_pyapi/javax/sql.py
|
||||
android_pyapi/org/__init__.py
|
||||
android_pyapi/org/json.py
|
||||
android_pyapi/org/w3c_dom.py
|
||||
android_pyapi/org/xml_pull.py
|
||||
android_pyapi/org/xml_sax.py
|
||||
android_pyapi/org/apache/__init__.py
|
||||
android_pyapi/org/apache/http.py
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
[dev]
|
||||
pytest
|
||||
pytest-cov
|
||||
|
||||
[linux]
|
||||
psutil
|
||||
requests
|
||||
Pillow
|
||||
opencv-python
|
||||
numpy
|
||||
|
||||
[windows]
|
||||
psutil
|
||||
requests
|
||||
Pillow
|
||||
opencv-python
|
||||
numpy
|
||||
pyautogui
|
||||
pyperclip
|
||||
screeninfo
|
||||
pywin32
|
||||
wmi
|
||||
pycaw
|
||||
comtypes
|
||||
win10toast
|
||||
@@ -0,0 +1 @@
|
||||
android_pyapi
|
||||
@@ -379,6 +379,13 @@ __all__ = [
|
||||
"AccessibilityServiceInfo",
|
||||
"Account", "AccountManager",
|
||||
"Activity", "ActivityManager",
|
||||
"Fragment", "FragmentManager", "FragmentTransaction",
|
||||
"Dialog", "AlertDialog", "ProgressDialog",
|
||||
"DatePickerDialog", "TimePickerDialog",
|
||||
"Service", "Application",
|
||||
"PendingIntent", "ComponentName", "AlarmManager",
|
||||
"NotificationChannel", "NotificationChannelGroup",
|
||||
"NotificationCompat", "TaskStackBuilder",
|
||||
"AlphaAnimation",
|
||||
"Animation",
|
||||
"Animator", "ValueAnimator", "ObjectAnimator", "AnimatorSet",
|
||||
@@ -391,6 +398,14 @@ __all__ = [
|
||||
"BatteryManager",
|
||||
"BiometricPrompt",
|
||||
"Bitmap", "BitmapDrawable", "BitmapFactory",
|
||||
"Camera",
|
||||
"Region", "PathMeasure",
|
||||
"NinePatch", "Picture", "PictureDrawable",
|
||||
"PorterDuff", "PorterDuffXfermode",
|
||||
"Shader", "LinearGradient", "RadialGradient", "SweepGradient",
|
||||
"BitmapShader", "ComposeShader",
|
||||
"CornerPathEffect", "DashPathEffect", "PathDashPathEffect",
|
||||
"SumPathEffect",
|
||||
"BluetoothAdapter", "BluetoothDevice",
|
||||
"BroadcastReceiver",
|
||||
"Build",
|
||||
@@ -423,6 +438,13 @@ __all__ = [
|
||||
"Html",
|
||||
"ImageView",
|
||||
"InputDevice",
|
||||
"KeyEvent", "MotionEvent",
|
||||
"GestureDetector", "ScaleGestureDetector",
|
||||
"SurfaceHolder", "Surface", "SurfaceView", "TextureView",
|
||||
"ViewOutlineProvider",
|
||||
"WindowManager", "Window",
|
||||
"LayoutInflater", "Menu", "MenuItem", "ContextMenu",
|
||||
"Display", "DisplayMetrics",
|
||||
"InputManager",
|
||||
"InputMethodManager",
|
||||
"Intent",
|
||||
@@ -438,6 +460,7 @@ __all__ = [
|
||||
"MediaPlayer",
|
||||
"MediaProjectionManager",
|
||||
"MediaRecorder",
|
||||
"MediaMetadataRetriever",
|
||||
"MediaSession",
|
||||
"MediaStore",
|
||||
"Message",
|
||||
@@ -453,9 +476,18 @@ __all__ = [
|
||||
"PermissionManager",
|
||||
"PowerManager",
|
||||
"PrintAttributes", "PrintDocumentAdapter", "PrintManager",
|
||||
"LinearLayout", "FrameLayout", "RelativeLayout",
|
||||
"TableLayout", "TableRow",
|
||||
"BaseAdapter", "ArrayAdapter", "SimpleAdapter", "CursorAdapter",
|
||||
"ImageButton", "ToggleButton", "RadioButton", "SeekBar", "RatingBar",
|
||||
"ViewSwitcher", "TextSwitcher", "ImageSwitcher",
|
||||
"SearchView", "NumberPicker", "Chronometer",
|
||||
"TabHost", "TabWidget", "ViewAnimator",
|
||||
"ZoomControls", "DigitalClock", "AnalogClock",
|
||||
"ProgressBar",
|
||||
"RadioManager", "RadioTuner",
|
||||
"Rect", "RectF",
|
||||
"RecyclerView",
|
||||
"Resources", "AssetManager",
|
||||
"RoleManager",
|
||||
"ScrollView",
|
||||
@@ -466,9 +498,11 @@ __all__ = [
|
||||
"Slice", "SliceManager", "SliceProvider",
|
||||
"SmsManager",
|
||||
"SpannableString",
|
||||
"SpannableStringBuilder",
|
||||
"SpeechRecognizer",
|
||||
"Spinner",
|
||||
"SQLiteDatabase", "SQLiteOpenHelper",
|
||||
"SQLiteDatabase", "SQLiteOpenHelper", "SQLiteStatement",
|
||||
"MatrixCursor",
|
||||
"StorageManager", "StorageVolume",
|
||||
"StructStat",
|
||||
"Switch",
|
||||
@@ -481,6 +515,7 @@ __all__ = [
|
||||
"ThermalManager",
|
||||
"TimeManager",
|
||||
"Toast",
|
||||
"TrafficStats",
|
||||
"TranslateAnimation",
|
||||
"TranslationManager",
|
||||
"TvInputManager",
|
||||
@@ -493,6 +528,7 @@ __all__ = [
|
||||
"UwbManager",
|
||||
"View", "ViewGroup",
|
||||
"VirtualDisplay",
|
||||
"Vibrator", "VibratorEffect",
|
||||
"VpnManager", "VpnService",
|
||||
"WebView",
|
||||
"WifiAwareManager",
|
||||
@@ -503,7 +539,9 @@ __all__ = [
|
||||
"BufferedReader", "BufferedWriter",
|
||||
"InputStreamReader", "OutputStreamWriter",
|
||||
"URL", "HttpURLConnection", "Socket", "ServerSocket",
|
||||
"NetworkInterface", "InetAddress", "InetSocketAddress", "InterfaceAddress",
|
||||
"ArrayList", "HashMap", "HashSet", "LinkedList",
|
||||
"TreeMap", "LinkedHashMap", "PriorityQueue",
|
||||
"Collections", "Arrays", "Calendar", "Date", "Random",
|
||||
"UUID", "Scanner", "Timer", "TimerTask",
|
||||
"Pattern", "Matcher",
|
||||
@@ -524,6 +562,14 @@ __all__ = [
|
||||
"CloneNotSupportedException", "IllegalMonitorStateException",
|
||||
"IllegalThreadStateException", "EnumConstantNotPresentException",
|
||||
"TypeNotPresentException",
|
||||
# Java Exception 体系 (扩展)
|
||||
"ClassNotFoundException", "IllegalAccessException",
|
||||
"InstantiationException", "InvocationTargetException",
|
||||
"NoSuchElementException", "AssertionError",
|
||||
"OutOfMemoryError", "StackOverflowError", "NoClassDefFoundError",
|
||||
"UnsatisfiedLinkError", "LinkageError", "VerifyError",
|
||||
"AbstractMethodError", "IncompatibleClassChangeError",
|
||||
"ExceptionInInitializerError",
|
||||
"SimpleDateFormat", "DecimalFormat",
|
||||
"LocalDateTime", "LocalDate", "LocalTime", "Instant", "Duration", "Period",
|
||||
"ChronoUnit", "ChronoField", "DayOfWeek", "Month", "ZoneId", "ZoneOffset",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -18,6 +18,13 @@ from .._core import (
|
||||
resolve_android_path, to_android_path,
|
||||
_sdcard_root, _app_data_dir, _package_name,
|
||||
)
|
||||
from ..java.lang import IllegalStateException
|
||||
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxNotify, LinuxShell, LinuxProcess, LinuxDisplay, LinuxUIAutomation, LinuxPower
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -241,6 +248,108 @@ class Intent(Stub):
|
||||
def filterHashCode(self) -> int:
|
||||
return hash((self._action, self._data_str, self._type, self._package, self._component)) & 0x7FFFFFFF
|
||||
|
||||
def getParcelableExtra(self, name):
|
||||
return self._extras.get(name) if self._extras else None
|
||||
|
||||
def setComponent(self, component):
|
||||
self._component = component
|
||||
return self
|
||||
|
||||
def getComponent(self):
|
||||
return getattr(self, '_component', None)
|
||||
|
||||
def getDataString(self):
|
||||
data = self.getData()
|
||||
return str(data) if data else None
|
||||
|
||||
def getScheme(self):
|
||||
data = self.getData()
|
||||
if data:
|
||||
return data.getScheme()
|
||||
return None
|
||||
|
||||
def putExtras(self, src):
|
||||
if src._extras:
|
||||
if self._extras is None:
|
||||
self._extras = {}
|
||||
self._extras.update(src._extras)
|
||||
return self
|
||||
|
||||
def putIntegerArrayListExtra(self, name, value):
|
||||
if self._extras is None:
|
||||
self._extras = {}
|
||||
self._extras[name] = value
|
||||
return self
|
||||
|
||||
def putStringArrayListExtra(self, name, value):
|
||||
if self._extras is None:
|
||||
self._extras = {}
|
||||
self._extras[name] = value
|
||||
return self
|
||||
|
||||
def putParcelableArrayListExtra(self, name, value):
|
||||
if self._extras is None:
|
||||
self._extras = {}
|
||||
self._extras[name] = value
|
||||
return self
|
||||
|
||||
def getBundleExtra(self, name):
|
||||
return self._extras.get(name) if self._extras else None
|
||||
|
||||
def putBundleExtra(self, name, value):
|
||||
if self._extras is None:
|
||||
self._extras = {}
|
||||
self._extras[name] = value
|
||||
return self
|
||||
|
||||
def getParcelableArrayListExtra(self, name):
|
||||
return self._extras.get(name) if self._extras else None
|
||||
|
||||
def getStringArrayExtra(self, name):
|
||||
return self._extras.get(name) if self._extras else None
|
||||
|
||||
def getIntArrayExtra(self, name):
|
||||
return self._extras.get(name) if self._extras else None
|
||||
|
||||
def getBooleanArrayExtra(self, name):
|
||||
return self._extras.get(name) if self._extras else None
|
||||
|
||||
def getCharSequenceExtra(self, name):
|
||||
return self._extras.get(name) if self._extras else None
|
||||
|
||||
def getCharSequenceArrayListExtra(self, name):
|
||||
return self._extras.get(name) if self._extras else None
|
||||
|
||||
def removeCategory(self, category):
|
||||
if self._categories:
|
||||
self._categories.discard(category)
|
||||
return self
|
||||
|
||||
def setSelector(self, selector):
|
||||
self._selector = selector
|
||||
|
||||
def getSelector(self):
|
||||
return getattr(self, '_selector', None)
|
||||
|
||||
def setSourceBounds(self, bounds):
|
||||
self._source_bounds = bounds
|
||||
|
||||
def getSourceBounds(self):
|
||||
return getattr(self, '_source_bounds', None)
|
||||
|
||||
def makeMainActivity(self, component):
|
||||
self.addCategory("android.intent.category.LAUNCHER")
|
||||
self.setComponent(component)
|
||||
return self
|
||||
|
||||
def cloneFilter(self):
|
||||
new_intent = Intent(self.getAction())
|
||||
if self._categories:
|
||||
new_intent._categories = set(self._categories)
|
||||
if self._type:
|
||||
new_intent._type = self._type
|
||||
return new_intent
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# ComponentName (forward reference)
|
||||
@@ -732,6 +841,62 @@ class Notification(Stub):
|
||||
n._extras = dict(self._extras)
|
||||
return n
|
||||
|
||||
def setProgress(self, max, progress, indeterminate):
|
||||
self._progress_max = max
|
||||
self._progress = progress
|
||||
self._progress_indeterminate = indeterminate
|
||||
return self
|
||||
|
||||
def setShowWhen(self, show):
|
||||
self._show_when = show
|
||||
return self
|
||||
|
||||
def setOnlyAlertOnce(self, onlyAlertOnce):
|
||||
self._only_alert_once = onlyAlertOnce
|
||||
return self
|
||||
|
||||
def setLocalOnly(self, localOnly):
|
||||
self._local_only = localOnly
|
||||
return self
|
||||
|
||||
def setSound(self, sound):
|
||||
self._sound = sound
|
||||
return self
|
||||
|
||||
def setVibrate(self, pattern):
|
||||
self._vibrate = pattern
|
||||
return self
|
||||
|
||||
def setLights(self, argb, onMs, offMs):
|
||||
self._light_color = argb
|
||||
self._light_on = onMs
|
||||
self._light_off = offMs
|
||||
return self
|
||||
|
||||
def setSortKey(self, sortKey):
|
||||
self._sort_key = sortKey
|
||||
return self
|
||||
|
||||
def setTimeoutAfter(self, timeoutMs):
|
||||
self._timeout_after = timeoutMs
|
||||
return self
|
||||
|
||||
def setColorized(self, colorize):
|
||||
self._colorized = colorize
|
||||
return self
|
||||
|
||||
def setUsesChronometer(self, usesChronometer):
|
||||
self._uses_chronometer = usesChronometer
|
||||
return self
|
||||
|
||||
def setShortcutId(self, shortcutId):
|
||||
self._shortcut_id = shortcutId
|
||||
return self
|
||||
|
||||
def setContent(self, views):
|
||||
self._content_view = views
|
||||
return self
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._channel_id = ""
|
||||
@@ -834,10 +999,16 @@ class NotificationManager(Stub):
|
||||
|
||||
def notify(self, id_: int, notification: Notification):
|
||||
self._notifications[id_] = notification
|
||||
sys.stderr.write(
|
||||
f"[NotificationManager] notify id={id_}: "
|
||||
f"title={notification._title} text={notification._text}\n"
|
||||
)
|
||||
if _USE_LINUX:
|
||||
title = getattr(notification, "_title", "") or ""
|
||||
text = getattr(notification, "_text", "") or ""
|
||||
if title or text:
|
||||
LinuxNotify.send(title, text)
|
||||
else:
|
||||
sys.stderr.write(
|
||||
f"[NotificationManager] notify id={id_}: "
|
||||
f"title={notification._title} text={notification._text}\n"
|
||||
)
|
||||
|
||||
def notifyAsPackage(self, package_name: str, tag: Optional[str], id_: int, notification: Notification):
|
||||
self.notify(id_, notification)
|
||||
@@ -1343,6 +1514,90 @@ class Fragment(Stub):
|
||||
def unregisterForContextMenu(self, view):
|
||||
pass
|
||||
|
||||
def getChildFragmentManager(self):
|
||||
if not hasattr(self, '_child_fragment_manager'):
|
||||
from .app import FragmentManager
|
||||
self._child_fragment_manager = FragmentManager()
|
||||
return self._child_fragment_manager
|
||||
|
||||
def getParentFragment(self):
|
||||
return getattr(self, '_parent_fragment', None)
|
||||
|
||||
def onCreateOptionsMenu(self, menu, inflater):
|
||||
return False
|
||||
|
||||
def onPrepareOptionsMenu(self, menu):
|
||||
return False
|
||||
|
||||
def onOptionsItemSelected(self, item):
|
||||
return False
|
||||
|
||||
def onCreateContextMenu(self, menu, v, menuInfo):
|
||||
pass
|
||||
|
||||
def onContextItemSelected(self, item):
|
||||
return False
|
||||
|
||||
def onContextMenuClosed(self, menu):
|
||||
pass
|
||||
|
||||
def onConfigurationChanged(self, newConfig):
|
||||
pass
|
||||
|
||||
def onLowMemory(self):
|
||||
pass
|
||||
|
||||
def onHiddenChanged(self, hidden):
|
||||
self._hidden = hidden
|
||||
|
||||
def getLayoutInflater(self):
|
||||
return getattr(self, '_layout_inflater', None)
|
||||
|
||||
def onGetLayoutInflater(self, savedInstanceState):
|
||||
return self.getLayoutInflater()
|
||||
|
||||
def requireContext(self):
|
||||
if not hasattr(self, '_context') or self._context is None:
|
||||
raise IllegalStateException("Fragment " + self.__class__.__name__ + " not attached to a context")
|
||||
return self._context
|
||||
|
||||
def requireActivity(self):
|
||||
if not hasattr(self, '_activity') or self._activity is None:
|
||||
raise IllegalStateException("Fragment " + self.__class__.__name__ + " not attached to an activity")
|
||||
return self._activity
|
||||
|
||||
def requireView(self):
|
||||
if not hasattr(self, '_view') or self._view is None:
|
||||
raise IllegalStateException("Fragment " + self.__class__.__name__ + " view is null")
|
||||
return self._view
|
||||
|
||||
def requireFragmentManager(self):
|
||||
fm = getattr(self, '_fragment_manager', None)
|
||||
if fm is None:
|
||||
raise IllegalStateException("Fragment " + self.__class__.__name__ + " not associated with a fragment manager")
|
||||
return fm
|
||||
|
||||
def setMenuVisibility(self, menuVisible):
|
||||
self._menu_visible = menuVisible
|
||||
|
||||
def onMenuVisibilityChanged(self, menuVisible):
|
||||
pass
|
||||
|
||||
def postponeEnterTransition(self):
|
||||
pass
|
||||
|
||||
def startPostponedEnterTransition(self):
|
||||
pass
|
||||
|
||||
def getEnterTransition(self):
|
||||
return getattr(self, '_enter_transition', None)
|
||||
|
||||
def setEnterTransition(self, transition):
|
||||
self._enter_transition = transition
|
||||
|
||||
def onInflate(self, context, attrs, savedInstanceState):
|
||||
pass
|
||||
|
||||
|
||||
class FragmentTransaction(Stub):
|
||||
"""FragmentTransaction — 管理 Fragment 事务"""
|
||||
@@ -1821,6 +2076,144 @@ class Activity(Stub):
|
||||
def getTheme(self):
|
||||
return getattr(self, '_theme', 0)
|
||||
|
||||
def getDeviceInfo(self):
|
||||
"""获取设备信息"""
|
||||
if _USE_LINUX:
|
||||
return LinuxProcess.get_system_info()
|
||||
return {}
|
||||
|
||||
def executeShell(self, command):
|
||||
"""执行 Shell 命令"""
|
||||
if _USE_LINUX:
|
||||
return LinuxShell.exec(command)
|
||||
return {"success": False, "output": "Shell not available"}
|
||||
|
||||
def screenshot(self, path=None):
|
||||
"""截屏"""
|
||||
if _USE_LINUX:
|
||||
return LinuxDisplay.screenshot(path)
|
||||
return None
|
||||
|
||||
def getScreenSize(self):
|
||||
"""获取屏幕尺寸"""
|
||||
if _USE_LINUX:
|
||||
return LinuxDisplay.get_screen_size()
|
||||
return (1920, 1080)
|
||||
|
||||
def click(self, x, y):
|
||||
"""点击屏幕坐标"""
|
||||
if _USE_LINUX:
|
||||
return LinuxUIAutomation.click(x, y)
|
||||
return False
|
||||
|
||||
def typeText(self, text):
|
||||
"""输入文本"""
|
||||
if _USE_LINUX:
|
||||
return LinuxUIAutomation.type_text(text)
|
||||
return False
|
||||
|
||||
def pressKey(self, key):
|
||||
"""按键"""
|
||||
if _USE_LINUX:
|
||||
return LinuxUIAutomation.press_key(key)
|
||||
return False
|
||||
|
||||
def requestWindowFeature(self, featureId):
|
||||
self._window_features = getattr(self, '_window_features', set())
|
||||
self._window_features.add(featureId)
|
||||
return True
|
||||
|
||||
def setTitle(self, title):
|
||||
self._title = title
|
||||
|
||||
def getTitle(self):
|
||||
return getattr(self, '_title', '')
|
||||
|
||||
def onKeyDown(self, keyCode, event):
|
||||
return False
|
||||
|
||||
def onKeyUp(self, keyCode, event):
|
||||
return False
|
||||
|
||||
def onTouchEvent(self, event):
|
||||
return False
|
||||
|
||||
def enterPictureInPictureMode(self):
|
||||
pass
|
||||
|
||||
def getReferrer(self):
|
||||
return getattr(self, '_referrer', None)
|
||||
|
||||
def setReferrer(self, referrer):
|
||||
self._referrer = referrer
|
||||
|
||||
def isTaskRoot(self):
|
||||
return True
|
||||
|
||||
def getChangingConfigurations(self):
|
||||
return getattr(self, '_changing_config', 0)
|
||||
|
||||
def navigateUpTo(self, intent):
|
||||
return True
|
||||
|
||||
def shouldUpRecreateTask(self, intent):
|
||||
return False
|
||||
|
||||
def getParentActivityIntent(self):
|
||||
return None
|
||||
|
||||
def onOptionsMenuClosed(self, menu):
|
||||
pass
|
||||
|
||||
def setVolumeControlStream(self, streamType):
|
||||
self._volume_control_stream = streamType
|
||||
|
||||
def getVolumeControlStream(self):
|
||||
return getattr(self, '_volume_control_stream', 0)
|
||||
|
||||
def getLocalClassName(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def finishAndRemoveTask(self):
|
||||
self.finish()
|
||||
|
||||
def getLoaderManager(self):
|
||||
if not hasattr(self, '_loader_manager'):
|
||||
self._loader_manager = type('LoaderManager', (), {
|
||||
'initLoader': lambda *a: None,
|
||||
'restartLoader': lambda *a: None,
|
||||
'destroyLoader': lambda *a: None,
|
||||
'getLoader': lambda *a: None,
|
||||
})()
|
||||
return self._loader_manager
|
||||
|
||||
def onRetainNonConfigurationInstance(self):
|
||||
return None
|
||||
|
||||
def getLastNonConfigurationInstance(self):
|
||||
return getattr(self, '_last_non_config_instance', None)
|
||||
|
||||
def setProgressBarVisibility(self, visible):
|
||||
pass
|
||||
|
||||
def setProgress(self, progress):
|
||||
pass
|
||||
|
||||
def setSecondaryProgress(self, secondaryProgress):
|
||||
pass
|
||||
|
||||
def onSearchRequested(self):
|
||||
return False
|
||||
|
||||
def startSearch(self, initialQuery, selectInitial, appSearchData, globalSearch):
|
||||
pass
|
||||
|
||||
def startIntentSender(self, intent, fillInIntent, flagsMask, flagsValues, extraFlags):
|
||||
pass
|
||||
|
||||
def startIntentSenderForResult(self, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags):
|
||||
pass
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Service
|
||||
@@ -2009,9 +2402,13 @@ class ActivityManager(Stub):
|
||||
self._memory_info = self.MemoryInfo()
|
||||
|
||||
def getMemoryInfo(self) -> "ActivityManager.MemoryInfo":
|
||||
if _USE_LINUX:
|
||||
return LinuxPower.get_memory_info()
|
||||
return self._memory_info
|
||||
|
||||
def getRunningAppProcesses(self) -> Optional[List["ActivityManager.RunningAppProcessInfo"]]:
|
||||
if _USE_LINUX:
|
||||
return LinuxProcess.list_processes()
|
||||
return []
|
||||
|
||||
def killBackgroundProcesses(self, package_name: str):
|
||||
|
||||
@@ -11,9 +11,10 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Callable
|
||||
from enum import IntEnum
|
||||
|
||||
from .._core import Stub, AndroidOnlyError
|
||||
from ..java.lang import RuntimeException
|
||||
|
||||
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
|
||||
from ..java.lang import RuntimeException, IllegalArgumentException, IllegalStateException
|
||||
from .app import Fragment as _BaseFragment
|
||||
from .app import FragmentManager as _BaseFragmentManager
|
||||
from .app import FragmentTransaction as _BaseFragmentTransaction
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -64,8 +65,8 @@ POP_BACK_STACK_INCLUSIVE = 1
|
||||
# Fragment
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class Fragment(Stub):
|
||||
"""Fragment — 片段基类 (桌面存根)"""
|
||||
class Fragment(_BaseFragment):
|
||||
"""Fragment — 片段基类 (桌面存根,继承 app.Fragment 的所有方法)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -424,17 +425,12 @@ class Fragment(Stub):
|
||||
return f"Fragment({self._mTag or hex(id(self))})"
|
||||
|
||||
|
||||
class IllegalStateException(RuntimeException):
|
||||
"""IllegalStateException — 非法状态异常"""
|
||||
pass
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# FragmentManager
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class FragmentManager(Stub):
|
||||
"""FragmentManager — 片段管理器"""
|
||||
class FragmentManager(_BaseFragmentManager):
|
||||
"""FragmentManager — 片段管理器 (继承 app.FragmentManager 的所有方法)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -545,8 +541,8 @@ class FragmentManager(Stub):
|
||||
# FragmentTransaction
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class FragmentTransaction(Stub):
|
||||
"""FragmentTransaction — 片段事务"""
|
||||
class FragmentTransaction(_BaseFragmentTransaction):
|
||||
"""FragmentTransaction — 片段事务 (继承 app.FragmentTransaction 的所有方法)"""
|
||||
|
||||
TRANSIT_ENTER_MASK = TRANSIT_ENTER_MASK
|
||||
TRANSIT_EXIT_MASK = TRANSIT_EXIT_MASK
|
||||
|
||||
@@ -13,6 +13,12 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from .._core import Stub, AndroidOnlyError
|
||||
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxProcess, LinuxPower
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
__all__ = [
|
||||
"TaskManager", "TaskInfo", "TaskStackListener", "TaskSnapshot",
|
||||
"TaskDescription", "ActivityManager",
|
||||
@@ -908,9 +914,18 @@ class ActivityManager(Stub):
|
||||
return list(self._services.values())[:maxNum]
|
||||
|
||||
def getRunningAppProcesses(self) -> List[RunningAppProcessInfo]:
|
||||
if _USE_LINUX:
|
||||
return LinuxProcess.list_processes()
|
||||
return list(self._processes.values())
|
||||
|
||||
def getMemoryInfo(self, outInfo: MemoryInfo):
|
||||
if _USE_LINUX:
|
||||
mem = LinuxPower.get_memory_info()
|
||||
outInfo.availMem = mem.get("available", 0)
|
||||
outInfo.totalMem = mem.get("total", 0)
|
||||
outInfo.threshold = int(mem.get("total", 0) * 0.1)
|
||||
outInfo.lowMemory = mem.get("percent", 0) > 90.0
|
||||
return outInfo
|
||||
import psutil
|
||||
try:
|
||||
import psutil
|
||||
|
||||
@@ -14,6 +14,13 @@ from .._core import (
|
||||
_sdcard_root, _app_data_dir, _package_name,
|
||||
)
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxBluetooth, HAS_BLUETOOTHCTL
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# BluetoothProfile
|
||||
@@ -645,22 +652,36 @@ class BluetoothAdapter(Stub):
|
||||
return BluetoothAdapter._default_adapter
|
||||
|
||||
def isEnabled(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
return LinuxBluetooth.get_state() == LinuxBluetooth.STATE_ON
|
||||
return self._enabled
|
||||
|
||||
def enable(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
return LinuxBluetooth.enable()
|
||||
self._enabled = True
|
||||
self._state = self.STATE_ON
|
||||
return True
|
||||
|
||||
def disable(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
return LinuxBluetooth.disable()
|
||||
self._enabled = False
|
||||
self._state = self.STATE_OFF
|
||||
return True
|
||||
|
||||
def getAddress(self) -> str:
|
||||
if _USE_LINUX:
|
||||
addr = LinuxBluetooth.get_address()
|
||||
if addr:
|
||||
return addr
|
||||
return self._address
|
||||
|
||||
def getName(self) -> str:
|
||||
if _USE_LINUX:
|
||||
name = LinuxBluetooth.get_name()
|
||||
if name:
|
||||
return name
|
||||
return self._name
|
||||
|
||||
def setName(self, name: str) -> bool:
|
||||
@@ -668,13 +689,36 @@ class BluetoothAdapter(Stub):
|
||||
return True
|
||||
|
||||
def getBondedDevices(self) -> Set[BluetoothDevice]:
|
||||
if _USE_LINUX:
|
||||
devices = set()
|
||||
for d in LinuxBluetooth.get_bonded_devices():
|
||||
dev = BluetoothDevice(d.get("address", "00:00:00:00:00:00"))
|
||||
dev._name = d.get("name", "Unknown")
|
||||
dev._bond_state = BluetoothDevice.BOND_BONDED
|
||||
devices.add(dev)
|
||||
return devices
|
||||
return set(self._bonded_devices)
|
||||
|
||||
def getBondedDeviceAddresses(self) -> Set[str]:
|
||||
"""获取已配对设备地址集合"""
|
||||
if _USE_LINUX:
|
||||
return {d.get("address", "") for d in LinuxBluetooth.get_bonded_devices()}
|
||||
return {d.getAddress() for d in self._bonded_devices}
|
||||
|
||||
def startDiscovery(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
try:
|
||||
LinuxBluetooth.scan(timeout=5)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
self._discovering = True
|
||||
return True
|
||||
|
||||
def cancelDiscovery(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
# bluetoothctl scan off 在 LinuxBluetooth.scan 内部已处理
|
||||
return True
|
||||
self._discovering = False
|
||||
return True
|
||||
|
||||
@@ -694,6 +738,8 @@ class BluetoothAdapter(Stub):
|
||||
pass
|
||||
|
||||
def getState(self) -> int:
|
||||
if _USE_LINUX:
|
||||
return LinuxBluetooth.get_state()
|
||||
return self._state
|
||||
|
||||
def listenUsingRfcommWithServiceRecord(self, name: str, uuid_str: str) -> BluetoothServerSocket:
|
||||
@@ -704,6 +750,18 @@ class BluetoothAdapter(Stub):
|
||||
return BluetoothServerSocket()
|
||||
|
||||
def getRemoteDevice(self, address: str) -> Optional[BluetoothDevice]:
|
||||
if _USE_LINUX:
|
||||
# 从已配对设备中查找
|
||||
for d in LinuxBluetooth.get_bonded_devices():
|
||||
if d.get("address", "").upper() == address.upper():
|
||||
dev = BluetoothDevice(d.get("address", address))
|
||||
dev._name = d.get("name", "Unknown")
|
||||
dev._bond_state = BluetoothDevice.BOND_BONDED
|
||||
return dev
|
||||
# 未配对但地址合法则返回一个设备对象
|
||||
if BluetoothAdapter.checkBluetoothAddress(address):
|
||||
return BluetoothDevice(address)
|
||||
return None
|
||||
if not BluetoothAdapter.checkBluetoothAddress(address):
|
||||
return None
|
||||
return BluetoothDevice(address)
|
||||
|
||||
@@ -4,6 +4,7 @@ Android Content 组件 — Context, ContentResolver, Uri, SharedPreferences 等
|
||||
"""
|
||||
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
@@ -16,6 +17,12 @@ from .._core import (
|
||||
_sdcard_root, _app_data_dir, _package_name,
|
||||
)
|
||||
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxClipboard, LinuxNotify, LinuxLocation, LinuxProcess
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Uri
|
||||
@@ -366,34 +373,134 @@ class ContentResolver(Stub):
|
||||
super().__init__()
|
||||
self._observers: Dict[str, List[Any]] = {}
|
||||
|
||||
@staticmethod
|
||||
def _uri_to_file_path(uri) -> Optional[str]:
|
||||
"""将 file:// 或纯路径 Uri 转换为本地文件系统路径"""
|
||||
try:
|
||||
uri_str = str(uri)
|
||||
except Exception:
|
||||
return None
|
||||
# 处理字符串输入
|
||||
if isinstance(uri, str):
|
||||
if uri.startswith("file://"):
|
||||
return uri[7:]
|
||||
if "://" not in uri:
|
||||
return uri if uri else None
|
||||
return None
|
||||
scheme = getattr(uri, "_scheme", "") or ""
|
||||
path = getattr(uri, "_path", "") or ""
|
||||
if scheme == "file":
|
||||
return path if path else None
|
||||
if scheme == "" and path:
|
||||
return path
|
||||
return None
|
||||
|
||||
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
|
||||
selection_args: Optional[List[str]], sort_order: Optional[str],
|
||||
cancellation_signal: Any = None) -> Any:
|
||||
"""Query content provider"""
|
||||
sys.stderr.write(f"[ContentResolver] query: uri={uri}\n")
|
||||
"""Query content provider. 对 file:// URI 返回基于目录列表的 Cursor。"""
|
||||
path = self._uri_to_file_path(uri)
|
||||
if path is not None and os.path.isdir(path):
|
||||
try:
|
||||
from .database import Cursor, MatrixCursor
|
||||
cols = projection or ["_id", "name", "size", "last_modified"]
|
||||
cursor = MatrixCursor(cols)
|
||||
for idx, name in enumerate(sorted(os.listdir(path))):
|
||||
full = os.path.join(path, name)
|
||||
try:
|
||||
st = os.stat(full)
|
||||
size = st.st_size
|
||||
mtime = int(st.st_mtime * 1000)
|
||||
except OSError:
|
||||
size = 0
|
||||
mtime = 0
|
||||
cursor.addRow([idx, name, size, mtime])
|
||||
return cursor
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
|
||||
sys.stderr.write(f"[ContentResolver] insert: uri={uri}\n")
|
||||
"""Insert. 对 file:// URI 将 ContentValues 写为 JSON 文件。"""
|
||||
path = self._uri_to_file_path(uri)
|
||||
if path is not None and isinstance(values, ContentValues):
|
||||
try:
|
||||
os.makedirs(path if os.path.isdir(path) else os.path.dirname(path) or ".", exist_ok=True)
|
||||
target = path if not os.path.isdir(path) else os.path.join(path, "insert.json")
|
||||
data = values._values if hasattr(values, "_values") else {}
|
||||
with open(target, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, default=str)
|
||||
return Uri.withAppendedPath(uri, os.path.basename(target))
|
||||
except OSError:
|
||||
pass
|
||||
return uri
|
||||
|
||||
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
|
||||
selection_args: Optional[List[str]]) -> int:
|
||||
sys.stderr.write(f"[ContentResolver] update: uri={uri}\n")
|
||||
"""Update. 对 file:// URI 更新对应 JSON 文件。"""
|
||||
path = self._uri_to_file_path(uri)
|
||||
if path is not None and isinstance(values, ContentValues) and os.path.isfile(path):
|
||||
try:
|
||||
data = values._values if hasattr(values, "_values") else {}
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
existing = json.load(f)
|
||||
if isinstance(existing, dict):
|
||||
existing.update(data)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(existing, f, ensure_ascii=False, default=str)
|
||||
return 1
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
def delete(self, uri: Uri, selection: Optional[str],
|
||||
selection_args: Optional[List[str]]) -> int:
|
||||
sys.stderr.write(f"[ContentResolver] delete: uri={uri}\n")
|
||||
"""Delete. 对 file:// URI 删除对应文件或目录。"""
|
||||
path = self._uri_to_file_path(uri)
|
||||
if path is not None:
|
||||
try:
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
return 1
|
||||
if os.path.isdir(path):
|
||||
import shutil
|
||||
shutil.rmtree(path)
|
||||
return 1
|
||||
except OSError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def applyBatch(self, authority: str, operations: List[Any]) -> List[Any]:
|
||||
sys.stderr.write(f"[ContentResolver] applyBatch: authority={authority}\n")
|
||||
return []
|
||||
"""Apply batch operations, returning per-op results."""
|
||||
results: List[Any] = []
|
||||
for op in operations:
|
||||
try:
|
||||
op_type = getattr(op, "_type", None)
|
||||
uri = getattr(op, "_uri", None)
|
||||
values = getattr(op, "_values", None)
|
||||
if op_type == 1 and uri is not None: # INSERT
|
||||
results.append(ContentProviderResult(uri=self.insert(uri, values or ContentValues())))
|
||||
elif op_type == 2 and uri is not None: # UPDATE
|
||||
count = self.update(uri, values or ContentValues(),
|
||||
getattr(op, "_selection", None),
|
||||
getattr(op, "_selection_args", None))
|
||||
results.append(ContentProviderResult(count=count))
|
||||
elif op_type == 3 and uri is not None: # DELETE
|
||||
count = self.delete(uri, getattr(op, "_selection", None),
|
||||
getattr(op, "_selection_args", None))
|
||||
results.append(ContentProviderResult(count=count))
|
||||
else:
|
||||
results.append(ContentProviderResult(count=0))
|
||||
except Exception:
|
||||
results.append(ContentProviderResult(count=0))
|
||||
return results
|
||||
|
||||
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
|
||||
sys.stderr.write(f"[ContentResolver] bulkInsert: uri={uri}\n")
|
||||
return len(values) if values else 0
|
||||
count = 0
|
||||
for v in values or []:
|
||||
if self.insert(uri, v) is not None:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def call(self, uri: Uri, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
|
||||
return None
|
||||
@@ -420,7 +527,17 @@ class ContentResolver(Stub):
|
||||
if not self._observers[uri_str]:
|
||||
del self._observers[uri_str]
|
||||
|
||||
def getType(self, uri: Uri) -> Optional[str]:
|
||||
def getType(self, uri) -> Optional[str]:
|
||||
"""返回 URI 指向资源的 MIME 类型。"""
|
||||
path = self._uri_to_file_path(uri)
|
||||
if path:
|
||||
mime, _ = mimetypes.guess_type(path)
|
||||
return mime or "application/octet-stream"
|
||||
scheme = getattr(uri, "_scheme", "") or ""
|
||||
if isinstance(uri, str) and "://" in uri:
|
||||
scheme = uri.split("://")[0]
|
||||
if scheme == "content":
|
||||
return "vnd.android.cursor.dir/vnd.android.content"
|
||||
return None
|
||||
|
||||
def acquireContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]:
|
||||
@@ -430,12 +547,40 @@ class ContentResolver(Stub):
|
||||
return ContentProviderClient(uri)
|
||||
|
||||
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
|
||||
return None
|
||||
"""返回 URI 支持的流 MIME 类型列表。"""
|
||||
mime = self.getType(uri)
|
||||
if mime is None:
|
||||
return None
|
||||
if mime_type_filter and mime_type_filter.endswith("/*"):
|
||||
prefix = mime_type_filter[:-1]
|
||||
if mime.startswith(prefix):
|
||||
return [mime]
|
||||
return None
|
||||
if mime == mime_type_filter:
|
||||
return [mime]
|
||||
return [mime] if mime else None
|
||||
|
||||
def openInputStream(self, uri: Uri) -> Any:
|
||||
"""打开输入流。对 file:// URI 返回真实文件流。"""
|
||||
path = self._uri_to_file_path(uri)
|
||||
if path and os.path.isfile(path):
|
||||
try:
|
||||
return open(path, "rb")
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def openOutputStream(self, uri: Uri) -> Any:
|
||||
"""打开输出流。对 file:// URI 返回真实文件流。"""
|
||||
path = self._uri_to_file_path(uri)
|
||||
if path:
|
||||
try:
|
||||
d = os.path.dirname(path)
|
||||
if d:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return open(path, "wb")
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
@@ -466,6 +611,7 @@ class Context(Stub):
|
||||
INPUT_METHOD_SERVICE = "input_method"
|
||||
CAMERA_SERVICE = "camera"
|
||||
BATTERY_SERVICE = "battery"
|
||||
CLIPBOARD_SERVICE = "clipboard"
|
||||
|
||||
# Mode 常量
|
||||
MODE_PRIVATE = 0x0000
|
||||
@@ -587,10 +733,16 @@ class Context(Stub):
|
||||
return PackageManager()
|
||||
|
||||
def getResources(self):
|
||||
return None
|
||||
from .content_res import Resources, AssetManager
|
||||
if not hasattr(self, "_resources") or self._resources is None:
|
||||
self._resources = Resources(self.getAssets())
|
||||
return self._resources
|
||||
|
||||
def getAssets(self):
|
||||
return None
|
||||
from .content_res import AssetManager
|
||||
if not hasattr(self, "_assets") or self._assets is None:
|
||||
self._assets = AssetManager()
|
||||
return self._assets
|
||||
|
||||
def getMainLooper(self):
|
||||
from .os import Looper
|
||||
@@ -642,6 +794,8 @@ class Context(Stub):
|
||||
if name == Context.BATTERY_SERVICE:
|
||||
from .os import BatteryManager
|
||||
return BatteryManager()
|
||||
if name == Context.CLIPBOARD_SERVICE:
|
||||
return ClipboardManager()
|
||||
return self._services.get(name)
|
||||
|
||||
def startService(self, intent: Any) -> Optional[Any]:
|
||||
@@ -659,15 +813,34 @@ class Context(Stub):
|
||||
pass
|
||||
|
||||
def getString(self, res_id: int, *args) -> str:
|
||||
"""返回资源字符串。优先委托给 Resources,否则返回合理默认值。"""
|
||||
try:
|
||||
res = self.getResources()
|
||||
val = res.getString(res_id, *args)
|
||||
if val is not None:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return str(res_id)
|
||||
|
||||
def getColor(self, color_id: int) -> int:
|
||||
return 0
|
||||
"""返回资源颜色。优先委托给 Resources,否则返回不透明黑色。"""
|
||||
try:
|
||||
res = self.getResources()
|
||||
return res.getColor(color_id)
|
||||
except Exception:
|
||||
return 0xFF000000
|
||||
|
||||
def getDrawable(self, drawable_id: int):
|
||||
return None
|
||||
"""返回 Drawable。优先委托给 Resources。"""
|
||||
try:
|
||||
res = self.getResources()
|
||||
return res.getDrawable(drawable_id)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def getWallpaper(self):
|
||||
"""返回壁纸 Drawable。桌面端返回 None(无系统壁纸)。"""
|
||||
return None
|
||||
|
||||
def getWallpaperDesiredMinimumWidth(self) -> int:
|
||||
@@ -786,18 +959,55 @@ class Context(Stub):
|
||||
self._theme = resid
|
||||
|
||||
def getText(self, resId):
|
||||
"""返回资源文本。优先委托给 Resources。"""
|
||||
try:
|
||||
res = self.getResources()
|
||||
val = res.getText(resId)
|
||||
if val is not None:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return str(resId)
|
||||
|
||||
def getQuantityString(self, resId, quantity, *formatArgs):
|
||||
return str(resId)
|
||||
"""返回复数字符串。优先委托给 Resources。"""
|
||||
try:
|
||||
res = self.getResources()
|
||||
return res.getQuantityString(resId, quantity, *formatArgs)
|
||||
except Exception:
|
||||
return str(resId)
|
||||
|
||||
def getStringArray(self, resId):
|
||||
"""返回字符串数组。优先委托给 Resources。"""
|
||||
try:
|
||||
res = self.getResources()
|
||||
val = res.getStringArray(resId)
|
||||
if val is not None:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
def getTextArray(self, resId):
|
||||
"""返回文本数组。优先委托给 Resources。"""
|
||||
try:
|
||||
res = self.getResources()
|
||||
val = res.getTextArray(resId)
|
||||
if val is not None:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
def getIntArray(self, resId):
|
||||
"""返回整型数组。优先委托给 Resources。"""
|
||||
try:
|
||||
res = self.getResources()
|
||||
val = res.getIntArray(resId)
|
||||
if val is not None:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
# ─── Context 派生 / 回调 (扩展) ───────────────────────────
|
||||
@@ -1107,10 +1317,16 @@ class SharedPreferences(Stub):
|
||||
return SharedPreferencesEditor(self)
|
||||
|
||||
def registerOnSharedPreferenceChangeListener(self, listener):
|
||||
pass
|
||||
if not hasattr(self, '_listeners'):
|
||||
self._listeners = []
|
||||
self._listeners.append(listener)
|
||||
|
||||
def unregisterOnSharedPreferenceChangeListener(self, listener):
|
||||
pass
|
||||
if hasattr(self, '_listeners'):
|
||||
try:
|
||||
self._listeners.remove(listener)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def getSharedPrefsFile(self):
|
||||
return self._file_path
|
||||
@@ -1177,6 +1393,12 @@ class SharedPreferencesEditor(Stub):
|
||||
self._prefs._data.pop(key, None)
|
||||
self._prefs._data.update(self._modified)
|
||||
self._prefs._save()
|
||||
# 触发监听器
|
||||
sp = getattr(self, '_prefs', None)
|
||||
if sp:
|
||||
for key in self._modified:
|
||||
for listener in getattr(sp, '_listeners', []):
|
||||
listener.onSharedPreferenceChanged(sp, key)
|
||||
self._modified.clear()
|
||||
self._removed.clear()
|
||||
self._clear_all = False
|
||||
@@ -1365,29 +1587,55 @@ class ContentProvider(Stub):
|
||||
|
||||
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
|
||||
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]:
|
||||
"""默认实现:委托给关联 Context 的 ContentResolver。"""
|
||||
if self._context is not None:
|
||||
return self._context.getContentResolver().query(
|
||||
uri, projection, selection, selection_args, sort_order)
|
||||
return None
|
||||
|
||||
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
|
||||
if self._context is not None:
|
||||
return self._context.getContentResolver().insert(uri, values)
|
||||
return None
|
||||
|
||||
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
|
||||
selection_args: Optional[List[str]]) -> int:
|
||||
if self._context is not None:
|
||||
return self._context.getContentResolver().update(uri, values, selection, selection_args)
|
||||
return 0
|
||||
|
||||
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int:
|
||||
if self._context is not None:
|
||||
return self._context.getContentResolver().delete(uri, selection, selection_args)
|
||||
return 0
|
||||
|
||||
def getType(self, uri: Uri) -> Optional[str]:
|
||||
"""返回 URI 的 MIME 类型。"""
|
||||
path = ContentResolver._uri_to_file_path(uri)
|
||||
if path:
|
||||
mime, _ = mimetypes.guess_type(path)
|
||||
return mime or "application/octet-stream"
|
||||
return None
|
||||
|
||||
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
|
||||
return None
|
||||
mime = self.getType(uri)
|
||||
if mime is None:
|
||||
return None
|
||||
return [mime]
|
||||
|
||||
def openFile(self, uri: Uri, mode: str) -> Any:
|
||||
"""打开文件。对 file:// URI 返回真实文件对象。"""
|
||||
path = ContentResolver._uri_to_file_path(uri)
|
||||
if path and os.path.exists(path):
|
||||
try:
|
||||
fmode = "rb" if "r" in mode else "wb"
|
||||
return open(path, fmode)
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def openAssetFile(self, uri: Uri, mode: str) -> Any:
|
||||
return None
|
||||
return self.openFile(uri, mode)
|
||||
|
||||
def attachInfo(self, context: Context, info: Any):
|
||||
self._context = context
|
||||
@@ -1423,29 +1671,44 @@ class ContentProviderClient(Stub):
|
||||
|
||||
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
|
||||
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]:
|
||||
return None
|
||||
"""委托给 ContentResolver 处理 file:// URI。"""
|
||||
resolver = ContentResolver()
|
||||
return resolver.query(uri, projection, selection, selection_args, sort_order)
|
||||
|
||||
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
|
||||
return uri
|
||||
resolver = ContentResolver()
|
||||
return resolver.insert(uri, values)
|
||||
|
||||
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
|
||||
selection_args: Optional[List[str]]) -> int:
|
||||
return 0
|
||||
resolver = ContentResolver()
|
||||
return resolver.update(uri, values, selection, selection_args)
|
||||
|
||||
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int:
|
||||
return 0
|
||||
resolver = ContentResolver()
|
||||
return resolver.delete(uri, selection, selection_args)
|
||||
|
||||
def applyBatch(self, operations: List[Any]) -> List[Any]:
|
||||
return []
|
||||
resolver = ContentResolver()
|
||||
return resolver.applyBatch("", operations)
|
||||
|
||||
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
|
||||
return len(values) if values else 0
|
||||
resolver = ContentResolver()
|
||||
return resolver.bulkInsert(uri, values)
|
||||
|
||||
def getType(self, uri: Uri) -> Optional[str]:
|
||||
"""返回 URI 的 MIME 类型。"""
|
||||
path = ContentResolver._uri_to_file_path(uri)
|
||||
if path:
|
||||
mime, _ = mimetypes.guess_type(path)
|
||||
return mime or "application/octet-stream"
|
||||
return None
|
||||
|
||||
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
|
||||
return None
|
||||
mime = self.getType(uri)
|
||||
if mime is None:
|
||||
return None
|
||||
return [mime]
|
||||
|
||||
def call(self, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
|
||||
return None
|
||||
@@ -1457,13 +1720,21 @@ class ContentProviderClient(Stub):
|
||||
pass
|
||||
|
||||
def openFile(self, uri: Uri, mode: str) -> Any:
|
||||
"""打开文件。对 file:// URI 返回真实文件对象。"""
|
||||
path = ContentResolver._uri_to_file_path(uri)
|
||||
if path and os.path.exists(path):
|
||||
try:
|
||||
fmode = "rb" if "r" in mode else "wb"
|
||||
return open(path, fmode)
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def openAssetFile(self, uri: Uri, mode: str) -> Any:
|
||||
return None
|
||||
return self.openFile(uri, mode)
|
||||
|
||||
def openTypedAssetFile(self, uri: Uri, mime_type: str, opts: Optional[ContentValues]) -> Any:
|
||||
return None
|
||||
return self.openFile(uri, "r")
|
||||
|
||||
def getLocalContentProvider(self) -> Optional[ContentProvider]:
|
||||
return None
|
||||
@@ -1637,6 +1908,14 @@ class CursorLoader(Loader):
|
||||
self._sort_order = sort_order
|
||||
|
||||
def loadInBackground(self):
|
||||
"""在后台加载数据,委托给 ContentResolver.query。"""
|
||||
if self._context is not None:
|
||||
try:
|
||||
resolver = self._context.getContentResolver()
|
||||
return resolver.query(self._uri, self._projection, self._selection,
|
||||
self._selection_args, self._sort_order)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@@ -1708,6 +1987,29 @@ class SyncResult(Stub):
|
||||
return f"SyncResult({self.stats.numEntries} entries)"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# ClipboardManager
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ClipboardManager(Stub):
|
||||
"""ClipboardManager — 剪贴板管理器"""
|
||||
|
||||
def getText(self):
|
||||
if _USE_LINUX:
|
||||
return LinuxClipboard.read()
|
||||
return ""
|
||||
|
||||
def setText(self, text):
|
||||
if _USE_LINUX:
|
||||
return LinuxClipboard.write(text)
|
||||
return False
|
||||
|
||||
def hasText(self):
|
||||
if _USE_LINUX:
|
||||
return len(LinuxClipboard.read()) > 0
|
||||
return False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 替换 _core.py 中的 _GlobalContext
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -1748,4 +2050,5 @@ __all__ = [
|
||||
"DialogInterface",
|
||||
"SyncAdapter",
|
||||
"SyncResult",
|
||||
"ClipboardManager",
|
||||
]
|
||||
@@ -13,6 +13,9 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from .._core import Stub, AndroidOnlyError
|
||||
from ..java.lang import RuntimeException
|
||||
from .app import Intent as _BaseIntent
|
||||
from .app import PendingIntent as _BasePendingIntent
|
||||
from .content import IntentFilter as _BaseIntentFilter
|
||||
|
||||
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
|
||||
|
||||
@@ -680,8 +683,8 @@ MIME_TYPE_ALL = "*/*"
|
||||
# Intent — 完整扩展
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class Intent(Stub):
|
||||
"""Android Intent — 完整扩展 (桌面存根)"""
|
||||
class Intent(_BaseIntent):
|
||||
"""Android Intent — 完整扩展 (桌面存根,继承 app.Intent 的所有方法)"""
|
||||
|
||||
# ── ShortcutIconResource inner class ──
|
||||
|
||||
@@ -761,16 +764,36 @@ class Intent(Stub):
|
||||
def getData(self) -> Optional[str]:
|
||||
return self._data
|
||||
|
||||
def setData(self, data: str) -> "Intent":
|
||||
def setData(self, data) -> "Intent":
|
||||
self._data = data
|
||||
self._type = None
|
||||
# 从 Uri 提取 scheme
|
||||
if data is not None:
|
||||
if hasattr(data, 'getScheme'):
|
||||
self._scheme = data.getScheme() or ""
|
||||
elif isinstance(data, str):
|
||||
from urllib.parse import urlparse
|
||||
self._scheme = urlparse(data).scheme
|
||||
return self
|
||||
|
||||
def getDataString(self) -> Optional[str]:
|
||||
return self._data
|
||||
if self._data is None:
|
||||
return None
|
||||
if isinstance(self._data, str):
|
||||
return self._data
|
||||
return str(self._data)
|
||||
|
||||
def getScheme(self) -> str:
|
||||
return self._scheme
|
||||
if self._scheme:
|
||||
return self._scheme
|
||||
# 从 data 动态提取
|
||||
if self._data is not None:
|
||||
if hasattr(self._data, 'getScheme'):
|
||||
return self._data.getScheme() or ""
|
||||
elif isinstance(self._data, str):
|
||||
from urllib.parse import urlparse
|
||||
return urlparse(self._data).scheme
|
||||
return ""
|
||||
|
||||
# ── Type ──
|
||||
|
||||
@@ -1256,8 +1279,8 @@ class IntentReceiver(Stub):
|
||||
# PendingIntent
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class PendingIntent(Stub):
|
||||
"""PendingIntent — 延迟 Intent"""
|
||||
class PendingIntent(_BasePendingIntent):
|
||||
"""PendingIntent — 延迟 Intent (继承 app.PendingIntent 的所有方法)"""
|
||||
|
||||
FLAG_ONE_SHOT = 1 << 30
|
||||
FLAG_NO_CREATE = 1 << 29
|
||||
@@ -1268,7 +1291,7 @@ class PendingIntent(Stub):
|
||||
|
||||
def __init__(self, intent: Optional[Intent] = None, requestCode: int = 0,
|
||||
flags: int = 0):
|
||||
super().__init__()
|
||||
super().__init__(intent, requestCode, flags)
|
||||
self._intent = intent
|
||||
self._request_code = requestCode
|
||||
self._flags = flags
|
||||
@@ -1455,8 +1478,8 @@ class IntentService(Stub):
|
||||
# IntentFilter
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class IntentFilter(Stub):
|
||||
"""IntentFilter — Intent 过滤器 (桌面存根)"""
|
||||
class IntentFilter(_BaseIntentFilter):
|
||||
"""IntentFilter — Intent 过滤器 (桌面存根,继承 content.IntentFilter 的所有方法)"""
|
||||
|
||||
class AuthorityEntry(Stub):
|
||||
"""AuthorityEntry — 权限条目"""
|
||||
|
||||
@@ -36,6 +36,8 @@ class Resources(Stub):
|
||||
self._drawables: Dict[str, Any] = {}
|
||||
self._booleans: Dict[str, bool] = {}
|
||||
self._integers: Dict[str, int] = {}
|
||||
self._string_arrays: Dict[str, List[str]] = {}
|
||||
self._int_arrays: Dict[str, List[int]] = {}
|
||||
self._identifiers: Dict[str, int] = {}
|
||||
self._next_id = 0x7f010000
|
||||
self._configuration = Configuration()
|
||||
@@ -125,6 +127,27 @@ class Resources(Stub):
|
||||
return val
|
||||
return 0
|
||||
|
||||
# ── Arrays ──
|
||||
|
||||
def getStringArray(self, id: Union[int, str]) -> List[str]:
|
||||
if isinstance(id, str):
|
||||
return self._string_arrays.get(id, [])
|
||||
for key, val in self._string_arrays.items():
|
||||
if self._identifiers.get(key) == id:
|
||||
return val
|
||||
return []
|
||||
|
||||
def getTextArray(self, id: Union[int, str]) -> List[str]:
|
||||
return self.getStringArray(id)
|
||||
|
||||
def getIntArray(self, id: Union[int, str]) -> List[int]:
|
||||
if isinstance(id, str):
|
||||
return self._int_arrays.get(id, [])
|
||||
for key, val in self._int_arrays.items():
|
||||
if self._identifiers.get(key) == id:
|
||||
return val
|
||||
return []
|
||||
|
||||
# ── Layout / Animation / Xml ──
|
||||
|
||||
def getLayout(self, id: Union[int, str]) -> Any:
|
||||
|
||||
@@ -331,21 +331,31 @@ class Cursor(Stub):
|
||||
return self.moveToPosition(self._pos + offset)
|
||||
|
||||
def moveToPosition(self, position: int) -> bool:
|
||||
if position < -1 or position >= self.getCount():
|
||||
count = self.getCount()
|
||||
if position >= count:
|
||||
self._pos = count
|
||||
return False
|
||||
if position < -1:
|
||||
self._pos = -1
|
||||
return False
|
||||
self._pos = position
|
||||
return True
|
||||
|
||||
def isFirst(self) -> bool:
|
||||
return self._pos == 0
|
||||
return self._pos == 0 and self.getCount() > 0
|
||||
|
||||
def isLast(self) -> bool:
|
||||
return self._pos == self.getCount() - 1
|
||||
count = self.getCount()
|
||||
return count > 0 and self._pos == count - 1
|
||||
|
||||
def isBeforeFirst(self) -> bool:
|
||||
if self.getCount() == 0:
|
||||
return True
|
||||
return self._pos < 0
|
||||
|
||||
def isAfterLast(self) -> bool:
|
||||
if self.getCount() == 0:
|
||||
return True
|
||||
return self._pos >= self.getCount()
|
||||
|
||||
def getColumnIndex(self, column_name: str) -> int:
|
||||
@@ -370,6 +380,9 @@ class Cursor(Stub):
|
||||
def getColumnNames(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def getColumnCount(self) -> int:
|
||||
return len(self.getColumnNames())
|
||||
|
||||
def getType(self, column_index: int) -> int:
|
||||
return self.FIELD_TYPE_NULL
|
||||
|
||||
@@ -759,6 +772,9 @@ class MatrixCursor(AbstractCursor):
|
||||
def getLong(self, column_index: int) -> int:
|
||||
return self.getInt(column_index)
|
||||
|
||||
def getShort(self, column_index: int) -> int:
|
||||
return self.getInt(column_index)
|
||||
|
||||
def getFloat(self, column_index: int) -> float:
|
||||
v = self.getString(column_index)
|
||||
if v is None:
|
||||
|
||||
@@ -181,9 +181,13 @@ class SQLiteProgram(SQLiteClosable):
|
||||
def clearBindings(self):
|
||||
self._bindings.clear()
|
||||
|
||||
def _ordered_bind_args(self) -> list:
|
||||
"""按绑定索引升序返回参数列表(1-based 索引排序)。"""
|
||||
return [self._bindings[k] for k in sorted(self._bindings.keys())]
|
||||
|
||||
def _bind_parameters(self, cursor: _sqlite3.Cursor):
|
||||
for idx, val in sorted(self._bindings.items()):
|
||||
cursor.execute(f"PRAGMA dummy={idx}")
|
||||
for idx in sorted(self._bindings.keys()):
|
||||
cursor.bind(idx, self._bindings[idx])
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -199,16 +203,18 @@ class SQLiteStatement(SQLiteProgram):
|
||||
def execute(self):
|
||||
try:
|
||||
conn = self._db._acquire_connection()
|
||||
cursor = conn.execute(self._sql, list(self._bindings.values()))
|
||||
conn.commit()
|
||||
conn.execute(self._sql, self._ordered_bind_args())
|
||||
if not self._db._in_transaction:
|
||||
conn.commit()
|
||||
except _sqlite3.DatabaseError as e:
|
||||
raise _map_sqlite_error(str(e), self._sql)
|
||||
|
||||
def executeUpdateDelete(self) -> int:
|
||||
try:
|
||||
conn = self._db._acquire_connection()
|
||||
cursor = conn.execute(self._sql, list(self._bindings.values()))
|
||||
conn.commit()
|
||||
cursor = conn.execute(self._sql, self._ordered_bind_args())
|
||||
if not self._db._in_transaction:
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
except _sqlite3.DatabaseError as e:
|
||||
raise _map_sqlite_error(str(e), self._sql)
|
||||
@@ -216,8 +222,9 @@ class SQLiteStatement(SQLiteProgram):
|
||||
def executeInsert(self) -> int:
|
||||
try:
|
||||
conn = self._db._acquire_connection()
|
||||
cursor = conn.execute(self._sql, list(self._bindings.values()))
|
||||
conn.commit()
|
||||
cursor = conn.execute(self._sql, self._ordered_bind_args())
|
||||
if not self._db._in_transaction:
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
except _sqlite3.DatabaseError as e:
|
||||
raise _map_sqlite_error(str(e), self._sql)
|
||||
@@ -225,9 +232,10 @@ class SQLiteStatement(SQLiteProgram):
|
||||
def simpleQueryForLong(self) -> int:
|
||||
try:
|
||||
conn = self._db._acquire_connection()
|
||||
cursor = conn.execute(self._sql, list(self._bindings.values()))
|
||||
cursor = conn.execute(self._sql, self._ordered_bind_args())
|
||||
row = cursor.fetchone()
|
||||
conn.commit()
|
||||
if not self._db._in_transaction:
|
||||
conn.commit()
|
||||
if row:
|
||||
return int(row[0]) if row[0] is not None else 0
|
||||
raise SQLiteDoneException("no results")
|
||||
@@ -237,15 +245,43 @@ class SQLiteStatement(SQLiteProgram):
|
||||
def simpleQueryForString(self) -> Optional[str]:
|
||||
try:
|
||||
conn = self._db._acquire_connection()
|
||||
cursor = conn.execute(self._sql, list(self._bindings.values()))
|
||||
cursor = conn.execute(self._sql, self._ordered_bind_args())
|
||||
row = cursor.fetchone()
|
||||
conn.commit()
|
||||
if not self._db._in_transaction:
|
||||
conn.commit()
|
||||
if row:
|
||||
return str(row[0]) if row[0] is not None else None
|
||||
raise SQLiteDoneException("no results")
|
||||
except _sqlite3.DatabaseError as e:
|
||||
raise _map_sqlite_error(str(e), self._sql)
|
||||
|
||||
def simpleQueryForBlobString(self) -> Optional[bytes]:
|
||||
try:
|
||||
conn = self._db._acquire_connection()
|
||||
cursor = conn.execute(self._sql, self._ordered_bind_args())
|
||||
row = cursor.fetchone()
|
||||
if not self._db._in_transaction:
|
||||
conn.commit()
|
||||
if row:
|
||||
v = row[0]
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, bytes):
|
||||
return v
|
||||
if isinstance(v, str):
|
||||
return v.encode("utf-8")
|
||||
return bytes(str(v), "utf-8")
|
||||
raise SQLiteDoneException("no results")
|
||||
except _sqlite3.DatabaseError as e:
|
||||
raise _map_sqlite_error(str(e), self._sql)
|
||||
|
||||
def execQuery(self) -> "SQLiteCursor":
|
||||
"""执行查询 SQL 并返回 SQLiteCursor。"""
|
||||
query = SQLiteQuery(self._db, self._sql)
|
||||
for idx, val in self._bindings.items():
|
||||
query.bind(idx, val)
|
||||
return SQLiteCursor(self._db, query)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# SQLiteQuery
|
||||
@@ -313,10 +349,17 @@ class SQLiteCursor(AbstractCursor):
|
||||
|
||||
def moveToPosition(self, position: int) -> bool:
|
||||
self._load()
|
||||
if position < -1 or position >= len(self._rows):
|
||||
count = len(self._rows)
|
||||
if position >= count:
|
||||
self._pos = count
|
||||
self._current_row = None
|
||||
return False
|
||||
if position < -1:
|
||||
self._pos = -1
|
||||
self._current_row = None
|
||||
return False
|
||||
self._pos = position
|
||||
if 0 <= position < len(self._rows):
|
||||
if 0 <= position < count:
|
||||
self._current_row = self._rows[position]
|
||||
else:
|
||||
self._current_row = None
|
||||
@@ -499,6 +542,39 @@ def _map_sqlite_error(msg: str, sql: str = "") -> SQLiteException:
|
||||
return SQLiteException(msg)
|
||||
|
||||
|
||||
def _extract_value_pairs(values: Any) -> List[Tuple[str, Any]]:
|
||||
"""从 ContentValues / dict / 类似对象中提取 (列名, 值) 列表。
|
||||
|
||||
注意: 本实现中 ContentValues.valueSet() 返回的是键集合而非 (键, 值) 对,
|
||||
因此需要通过 get() 取值。
|
||||
"""
|
||||
if values is None:
|
||||
return []
|
||||
if isinstance(values, dict):
|
||||
return list(values.items())
|
||||
# ContentValues 内部使用 _values dict
|
||||
inner = getattr(values, "_values", None)
|
||||
if isinstance(inner, dict):
|
||||
return list(inner.items())
|
||||
# ContentValues.valueSet() 在本实现中返回键集合
|
||||
if hasattr(values, "valueSet") and hasattr(values, "get"):
|
||||
try:
|
||||
return [(str(k), values.get(k)) for k in values.valueSet()]
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(values, "keySet") and hasattr(values, "get"):
|
||||
try:
|
||||
return [(str(k), values.get(k)) for k in values.keySet()]
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(values, "items"):
|
||||
try:
|
||||
return [(str(k), v) for k, v in values.items()]
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
class SQLiteDatabase(SQLiteClosable):
|
||||
"""SQLiteDatabase — 数据库操作类"""
|
||||
|
||||
@@ -536,10 +612,10 @@ class SQLiteDatabase(SQLiteClosable):
|
||||
try:
|
||||
db._conn = _sqlite3.connect(path)
|
||||
db._conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn_cursor = db._conn.execute("PRAGMA schema_version")
|
||||
conn_cursor = db._conn.execute("PRAGMA user_version")
|
||||
row = conn_cursor.fetchone()
|
||||
if row:
|
||||
db._version = row[0]
|
||||
db._version = int(row[0])
|
||||
except _sqlite3.DatabaseError as e:
|
||||
raise _map_sqlite_error(str(e))
|
||||
SQLiteDatabase._db_registry[path] = db
|
||||
@@ -610,23 +686,37 @@ class SQLiteDatabase(SQLiteClosable):
|
||||
self._ensure_open()
|
||||
if values is None:
|
||||
return -1
|
||||
cols = []
|
||||
vals = []
|
||||
if hasattr(values, "valueSet"):
|
||||
items = list(values.valueSet())
|
||||
elif hasattr(values, "key_set"):
|
||||
items = [(k, getattr(values, f"get{k[0].upper()}{k[1:]}", lambda: None)())
|
||||
for k in values.key_set()]
|
||||
elif isinstance(values, dict):
|
||||
items = list(values.items())
|
||||
else:
|
||||
return -1
|
||||
for k, v in items:
|
||||
cols.append(str(k))
|
||||
vals.append(v)
|
||||
if len(cols) == 0:
|
||||
items = _extract_value_pairs(values)
|
||||
if not items:
|
||||
cols = ["_id"]
|
||||
vals = [None]
|
||||
vals: List[Any] = [None]
|
||||
else:
|
||||
cols = [str(k) for k, _ in items]
|
||||
vals = [v for _, v in items]
|
||||
col_str = ", ".join(cols)
|
||||
placeholders = ", ".join(["?"] * len(vals))
|
||||
sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})"
|
||||
try:
|
||||
conn = self._acquire_connection()
|
||||
cursor = conn.execute(sql, vals)
|
||||
if not self._in_transaction:
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
except _sqlite3.DatabaseError:
|
||||
return -1
|
||||
|
||||
def insertOrThrow(self, table: str, null_column_hack: Optional[str],
|
||||
values: Any) -> int:
|
||||
self._ensure_open()
|
||||
if values is None:
|
||||
raise SQLiteException("null values not allowed")
|
||||
items = _extract_value_pairs(values)
|
||||
if not items:
|
||||
cols = ["_id"]
|
||||
vals: List[Any] = [None]
|
||||
else:
|
||||
cols = [str(k) for k, _ in items]
|
||||
vals = [v for _, v in items]
|
||||
col_str = ", ".join(cols)
|
||||
placeholders = ", ".join(["?"] * len(vals))
|
||||
sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})"
|
||||
@@ -639,23 +729,41 @@ class SQLiteDatabase(SQLiteClosable):
|
||||
except _sqlite3.DatabaseError as e:
|
||||
raise _map_sqlite_error(str(e), sql)
|
||||
|
||||
def replace(self, table: str, null_column_hack: Optional[str],
|
||||
values: Any) -> int:
|
||||
self._ensure_open()
|
||||
if values is None:
|
||||
return -1
|
||||
items = _extract_value_pairs(values)
|
||||
if not items:
|
||||
cols = ["_id"]
|
||||
vals: List[Any] = [None]
|
||||
else:
|
||||
cols = [str(k) for k, _ in items]
|
||||
vals = [v for _, v in items]
|
||||
col_str = ", ".join(cols)
|
||||
placeholders = ", ".join(["?"] * len(vals))
|
||||
sql = f"INSERT OR REPLACE INTO {table} ({col_str}) VALUES ({placeholders})"
|
||||
try:
|
||||
conn = self._acquire_connection()
|
||||
cursor = conn.execute(sql, vals)
|
||||
if not self._in_transaction:
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
except _sqlite3.DatabaseError:
|
||||
return -1
|
||||
|
||||
def update(self, table: str, values: Any,
|
||||
where_clause: Optional[str] = None,
|
||||
where_args: Optional[List[Any]] = None) -> int:
|
||||
self._ensure_open()
|
||||
if values is None:
|
||||
return 0
|
||||
set_pairs = []
|
||||
vals = []
|
||||
if isinstance(values, dict):
|
||||
items = list(values.items())
|
||||
elif hasattr(values, "valueSet"):
|
||||
items = list(values.valueSet())
|
||||
else:
|
||||
items = _extract_value_pairs(values)
|
||||
if not items:
|
||||
return 0
|
||||
for k, v in items:
|
||||
set_pairs.append(f"{k} = ?")
|
||||
vals.append(v)
|
||||
set_pairs = [f"{k} = ?" for k, _ in items]
|
||||
vals: List[Any] = [v for _, v in items]
|
||||
set_str = ", ".join(set_pairs)
|
||||
sql = f"UPDATE {table} SET {set_str}"
|
||||
if where_clause:
|
||||
@@ -748,13 +856,24 @@ class SQLiteDatabase(SQLiteClosable):
|
||||
return self._path
|
||||
|
||||
def getVersion(self) -> int:
|
||||
if self._conn:
|
||||
try:
|
||||
cursor = self._conn.execute("PRAGMA user_version")
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
self._version = int(row[0])
|
||||
return self._version
|
||||
except _sqlite3.DatabaseError:
|
||||
pass
|
||||
return self._version
|
||||
|
||||
def setVersion(self, version: int):
|
||||
self._version = version
|
||||
self._version = int(version)
|
||||
if self._conn:
|
||||
try:
|
||||
self._conn.execute(f"PRAGMA schema_version = {version}")
|
||||
self._conn.execute(f"PRAGMA user_version = {int(version)}")
|
||||
if not self._in_transaction:
|
||||
self._conn.commit()
|
||||
except _sqlite3.DatabaseError:
|
||||
pass
|
||||
|
||||
@@ -784,20 +903,54 @@ class SQLiteDatabase(SQLiteClosable):
|
||||
return self._enable_wal
|
||||
|
||||
def getMaximumSize(self) -> int:
|
||||
if self._conn:
|
||||
try:
|
||||
page_size = self.getPageSize()
|
||||
if page_size <= 0:
|
||||
page_size = 4096
|
||||
cursor = self._conn.execute("PRAGMA max_page_count")
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
self._max_size = int(row[0]) * page_size
|
||||
return self._max_size
|
||||
except _sqlite3.DatabaseError:
|
||||
pass
|
||||
return self._max_size
|
||||
|
||||
def getPageSize(self) -> int:
|
||||
if self._conn:
|
||||
try:
|
||||
cursor = self._conn.execute("PRAGMA page_size")
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
self._page_size = int(row[0])
|
||||
return self._page_size
|
||||
except _sqlite3.DatabaseError:
|
||||
pass
|
||||
return self._page_size
|
||||
|
||||
def setMaximumSize(self, size: int) -> int:
|
||||
self._max_size = size
|
||||
return size
|
||||
|
||||
def setPageSize(self, size: int):
|
||||
self._page_size = size
|
||||
self._max_size = int(size)
|
||||
if self._conn:
|
||||
try:
|
||||
self._conn.execute(f"PRAGMA page_size = {size}")
|
||||
page_size = self.getPageSize()
|
||||
if page_size <= 0:
|
||||
page_size = 4096
|
||||
max_pages = max(1, int(size) // page_size)
|
||||
cursor = self._conn.execute(f"PRAGMA max_page_count = {max_pages}")
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
self._max_size = int(row[0]) * page_size
|
||||
return self._max_size
|
||||
except _sqlite3.DatabaseError:
|
||||
pass
|
||||
return self._max_size
|
||||
|
||||
def setPageSize(self, size: int):
|
||||
self._page_size = int(size)
|
||||
if self._conn:
|
||||
try:
|
||||
self._conn.execute(f"PRAGMA page_size = {int(size)}")
|
||||
except _sqlite3.DatabaseError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import math
|
||||
import struct
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
from enum import IntEnum
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
@@ -732,6 +733,42 @@ class Path(Stub):
|
||||
pass
|
||||
|
||||
def op(self, other: "Path", op: int) -> bool:
|
||||
"""对两个路径执行布尔运算。基于包围盒的近似实现。"""
|
||||
if other is None:
|
||||
return False
|
||||
self_bounds = self.computeBounds()
|
||||
other_bounds = other.computeBounds()
|
||||
if op == Path.Op.UNION:
|
||||
# 合并:将 other 的段追加到 self
|
||||
self.addPath(other)
|
||||
return True
|
||||
if op == Path.Op.INTERSECT:
|
||||
# 交集:仅当包围盒相交时保留相交区域
|
||||
if not RectF.intersects(self_bounds, other_bounds):
|
||||
self.reset()
|
||||
return False
|
||||
# 用相交包围盒作为近似结果
|
||||
inter = RectF(self_bounds.left, self_bounds.top,
|
||||
self_bounds.right, self_bounds.bottom)
|
||||
inter.intersect(other_bounds.left, other_bounds.top,
|
||||
other_bounds.right, other_bounds.bottom)
|
||||
self.reset()
|
||||
self.addRect(inter.left, inter.top, inter.right, inter.bottom)
|
||||
return True
|
||||
if op == Path.Op.DIFFERENCE:
|
||||
# 差集:若包围盒不相交则 self 不变,否则标记(近似)
|
||||
if not RectF.intersects(self_bounds, other_bounds):
|
||||
return True
|
||||
# 近似:无法精确计算,保留 self 不变
|
||||
return True
|
||||
if op == Path.Op.REVERSE_DIFFERENCE:
|
||||
# 反向差集:用 other 替换 self
|
||||
self.set(other)
|
||||
return True
|
||||
if op == Path.Op.XOR:
|
||||
# 异或:合并两者
|
||||
self.addPath(other)
|
||||
return True
|
||||
return False
|
||||
|
||||
def isRect(self) -> bool:
|
||||
@@ -770,38 +807,219 @@ class PathMeasure(Stub):
|
||||
super().__init__()
|
||||
self._path = path
|
||||
self._force_closed = forceClosed
|
||||
self._contours: List[List[Tuple[float, float, float]]] = []
|
||||
self._contour_lengths: List[float] = []
|
||||
self._total_length: float = 0.0
|
||||
if path is not None:
|
||||
self._flatten()
|
||||
|
||||
def _flatten(self):
|
||||
"""将路径展平为线段列表,计算累积长度。"""
|
||||
self._contours = []
|
||||
self._contour_lengths = []
|
||||
self._total_length = 0.0
|
||||
if not self._path:
|
||||
return
|
||||
segs = self._path._segments
|
||||
cur_x = 0.0
|
||||
cur_y = 0.0
|
||||
start_x = 0.0
|
||||
start_y = 0.0
|
||||
contour: List[Tuple[float, float, float]] = []
|
||||
contour_len = 0.0
|
||||
for seg in segs:
|
||||
t = seg.get("type")
|
||||
if t == "move":
|
||||
if contour:
|
||||
self._contours.append(contour)
|
||||
self._contour_lengths.append(contour_len)
|
||||
self._total_length += contour_len
|
||||
cur_x = seg.get("x", 0.0)
|
||||
cur_y = seg.get("y", 0.0)
|
||||
start_x = cur_x
|
||||
start_y = cur_y
|
||||
contour = [(cur_x, cur_y, 0.0)]
|
||||
contour_len = 0.0
|
||||
elif t == "line":
|
||||
nx = seg.get("x", cur_x)
|
||||
ny = seg.get("y", cur_y)
|
||||
d = math.hypot(nx - cur_x, ny - cur_y)
|
||||
contour_len += d
|
||||
contour.append((nx, ny, contour_len))
|
||||
cur_x = nx
|
||||
cur_y = ny
|
||||
elif t == "quad":
|
||||
x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y)
|
||||
x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y)
|
||||
steps = 16
|
||||
prev_x, prev_y = cur_x, cur_y
|
||||
for i in range(1, steps + 1):
|
||||
tt = i / steps
|
||||
mt = 1 - tt
|
||||
nx = mt * mt * cur_x + 2 * mt * tt * x1 + tt * tt * x2
|
||||
ny = mt * mt * cur_y + 2 * mt * tt * y1 + tt * tt * y2
|
||||
d = math.hypot(nx - prev_x, ny - prev_y)
|
||||
contour_len += d
|
||||
contour.append((nx, ny, contour_len))
|
||||
prev_x, prev_y = nx, ny
|
||||
cur_x = x2
|
||||
cur_y = y2
|
||||
elif t == "cubic":
|
||||
x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y)
|
||||
x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y)
|
||||
x3, y3 = seg.get("x3", cur_x), seg.get("y3", cur_y)
|
||||
steps = 24
|
||||
prev_x, prev_y = cur_x, cur_y
|
||||
for i in range(1, steps + 1):
|
||||
tt = i / steps
|
||||
mt = 1 - tt
|
||||
nx = mt ** 3 * cur_x + 3 * mt ** 2 * tt * x1 + 3 * mt * tt ** 2 * x2 + tt ** 3 * x3
|
||||
ny = mt ** 3 * cur_y + 3 * mt ** 2 * tt * y1 + 3 * mt * tt ** 2 * y2 + tt ** 3 * y3
|
||||
d = math.hypot(nx - prev_x, ny - prev_y)
|
||||
contour_len += d
|
||||
contour.append((nx, ny, contour_len))
|
||||
prev_x, prev_y = nx, ny
|
||||
cur_x = x3
|
||||
cur_y = y3
|
||||
elif t == "close":
|
||||
if contour:
|
||||
d = math.hypot(start_x - cur_x, start_y - cur_y)
|
||||
if d > 1e-10:
|
||||
contour_len += d
|
||||
contour.append((start_x, start_y, contour_len))
|
||||
cur_x = start_x
|
||||
cur_y = start_y
|
||||
if contour:
|
||||
if self._force_closed and segs and segs[-1].get("type") != "close":
|
||||
d = math.hypot(start_x - cur_x, start_y - cur_y)
|
||||
if d > 1e-10:
|
||||
contour_len += d
|
||||
contour.append((start_x, start_y, contour_len))
|
||||
self._contours.append(contour)
|
||||
self._contour_lengths.append(contour_len)
|
||||
self._total_length += contour_len
|
||||
|
||||
def setPath(self, path: Path, forceClosed: bool):
|
||||
self._path = path
|
||||
self._force_closed = forceClosed
|
||||
self._contours = []
|
||||
self._contour_lengths = []
|
||||
self._total_length = 0.0
|
||||
if path is not None:
|
||||
self._flatten()
|
||||
|
||||
def getLength(self) -> float:
|
||||
if not self._path:
|
||||
return 0.0
|
||||
bounds = self._path.computeBounds()
|
||||
return math.sqrt(bounds.width() ** 2 + bounds.height() ** 2)
|
||||
"""返回当前轮廓的长度。"""
|
||||
if self._contour_lengths:
|
||||
return self._contour_lengths[0]
|
||||
return 0.0
|
||||
|
||||
def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool:
|
||||
if not self._path:
|
||||
"""获取指定距离处的坐标和切线方向。"""
|
||||
if not self._contours:
|
||||
return False
|
||||
contour = self._contours[0]
|
||||
length = self._contour_lengths[0]
|
||||
if length <= 0:
|
||||
return False
|
||||
# 钳制距离
|
||||
d = max(0.0, min(distance, length))
|
||||
# 二分查找
|
||||
lo, hi = 0, len(contour) - 1
|
||||
while lo < hi:
|
||||
mid = (lo + hi) // 2
|
||||
if contour[mid][2] < d:
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid
|
||||
idx = max(1, lo)
|
||||
px, py, pd = contour[idx - 1]
|
||||
nx, ny, nd = contour[idx]
|
||||
seg_len = nd - pd
|
||||
if seg_len < 1e-10:
|
||||
t = 0.0
|
||||
else:
|
||||
t = (d - pd) / seg_len
|
||||
x = px + (nx - px) * t
|
||||
y = py + (ny - py) * t
|
||||
dx = nx - px
|
||||
dy = ny - py
|
||||
dl = math.hypot(dx, dy)
|
||||
if dl < 1e-10:
|
||||
tx, ty = 1.0, 0.0
|
||||
else:
|
||||
tx, ty = dx / dl, dy / dl
|
||||
if len(pos) >= 2:
|
||||
pos[0] = 0.0
|
||||
pos[1] = 0.0
|
||||
pos[0] = x
|
||||
pos[1] = y
|
||||
if len(tan) >= 2:
|
||||
tan[0] = 1.0
|
||||
tan[1] = 0.0
|
||||
tan[0] = tx
|
||||
tan[1] = ty
|
||||
return True
|
||||
|
||||
def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool:
|
||||
return False
|
||||
"""获取指定距离处的变换矩阵(平移+旋转)。"""
|
||||
pos = [0.0, 0.0]
|
||||
tan = [0.0, 0.0]
|
||||
if not self.getPosTan(distance, pos, tan):
|
||||
return False
|
||||
angle = math.degrees(math.atan2(tan[1], tan[0]))
|
||||
matrix.reset()
|
||||
matrix.postRotate(angle)
|
||||
matrix.postTranslate(pos[0], pos[1])
|
||||
return True
|
||||
|
||||
def getSegment(self, startD: float, stopD: float, dst: Path, startWithMoveTo: bool) -> bool:
|
||||
return False
|
||||
"""提取路径中 [startD, stopD] 区间的子路径。"""
|
||||
if not self._contours:
|
||||
return False
|
||||
contour = self._contours[0]
|
||||
length = self._contour_lengths[0]
|
||||
if length <= 0:
|
||||
return False
|
||||
startD = max(0.0, min(startD, length))
|
||||
stopD = max(0.0, min(stopD, length))
|
||||
if startD >= stopD:
|
||||
return False
|
||||
first = True
|
||||
for i in range(1, len(contour)):
|
||||
px, py, pd = contour[i - 1]
|
||||
nx, ny, nd = contour[i]
|
||||
if nd < startD:
|
||||
continue
|
||||
if pd > stopD:
|
||||
break
|
||||
seg_len = nd - pd
|
||||
if seg_len < 1e-10:
|
||||
continue
|
||||
# 计算线段两端在 [startD, stopD] 内的参数
|
||||
t0 = max(0.0, (startD - pd) / seg_len)
|
||||
t1 = min(1.0, (stopD - pd) / seg_len)
|
||||
x0 = px + (nx - px) * t0
|
||||
y0 = py + (ny - py) * t0
|
||||
x1 = px + (nx - px) * t1
|
||||
y1 = py + (ny - py) * t1
|
||||
if first and startWithMoveTo:
|
||||
dst.moveTo(x0, y0)
|
||||
else:
|
||||
dst.lineTo(x0, y0)
|
||||
dst.lineTo(x1, y1)
|
||||
first = False
|
||||
return True
|
||||
|
||||
def isClosed(self) -> bool:
|
||||
return self._force_closed
|
||||
if not self._path or not self._path._segments:
|
||||
return self._force_closed
|
||||
if self._force_closed:
|
||||
return True
|
||||
return any(s.get("type") == "close" for s in self._path._segments)
|
||||
|
||||
def nextContour(self) -> bool:
|
||||
"""移动到下一个轮廓。"""
|
||||
if len(self._contours) > 1:
|
||||
self._contours.pop(0)
|
||||
self._contour_lengths.pop(0)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -1556,35 +1774,99 @@ class Camera(Stub):
|
||||
self._location_x = 0.0
|
||||
self._location_y = 0.0
|
||||
self._location_z = -8.0
|
||||
self._rot_x = 0.0
|
||||
self._rot_y = 0.0
|
||||
self._rot_z = 0.0
|
||||
self._trans_x = 0.0
|
||||
self._trans_y = 0.0
|
||||
self._trans_z = 0.0
|
||||
self._stack: List[Tuple[float, float, float, float, float, float, float, float, float]] = []
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
"""保存当前变换状态到栈。"""
|
||||
self._stack.append((self._rot_x, self._rot_y, self._rot_z,
|
||||
self._trans_x, self._trans_y, self._trans_z,
|
||||
self._location_x, self._location_y, self._location_z))
|
||||
|
||||
def restore(self):
|
||||
pass
|
||||
"""从栈恢复上次保存的变换状态。"""
|
||||
if self._stack:
|
||||
(self._rot_x, self._rot_y, self._rot_z,
|
||||
self._trans_x, self._trans_y, self._trans_z,
|
||||
self._location_x, self._location_y, self._location_z) = self._stack.pop()
|
||||
|
||||
def translate(self, x: float, y: float, z: float):
|
||||
self._location_x += x
|
||||
self._location_y += y
|
||||
self._location_z += z
|
||||
self._trans_x += x
|
||||
self._trans_y += y
|
||||
self._trans_z += z
|
||||
|
||||
def rotateX(self, deg: float):
|
||||
pass
|
||||
"""绕 X 轴旋转(累积角度)。"""
|
||||
self._rot_x += deg
|
||||
|
||||
def rotateY(self, deg: float):
|
||||
pass
|
||||
"""绕 Y 轴旋转(累积角度)。"""
|
||||
self._rot_y += deg
|
||||
|
||||
def rotateZ(self, deg: float):
|
||||
pass
|
||||
"""绕 Z 轴旋转(累积角度)。"""
|
||||
self._rot_z += deg
|
||||
|
||||
def rotate(self, x: float, y: float, z: float):
|
||||
pass
|
||||
"""同时绕 X/Y/Z 轴旋转。"""
|
||||
self._rot_x += x
|
||||
self._rot_y += y
|
||||
self._rot_z += z
|
||||
|
||||
def _compute_matrix(self) -> List[float]:
|
||||
"""计算 4x4 变换矩阵并投影为 3x3 仿射矩阵。"""
|
||||
rx = math.radians(self._rot_x)
|
||||
ry = math.radians(self._rot_y)
|
||||
rz = math.radians(self._rot_z)
|
||||
cx, sx = math.cos(rx), math.sin(rx)
|
||||
cy, sy = math.cos(ry), math.sin(ry)
|
||||
cz, sz = math.cos(rz), math.sin(rz)
|
||||
# 旋转矩阵 R = Rz * Ry * Rx
|
||||
r00 = cz * cy
|
||||
r01 = cz * sy * sx - sz * cx
|
||||
r02 = cz * sy * cx + sz * sx
|
||||
r10 = sz * cy
|
||||
r11 = sz * sy * sx + cz * cx
|
||||
r12 = sz * sy * cx - cz * sx
|
||||
r20 = -sy
|
||||
r21 = cy * sx
|
||||
r22 = cy * cx
|
||||
# 应用平移
|
||||
tx = self._trans_x
|
||||
ty = self._trans_y
|
||||
tz = self._trans_z
|
||||
# 相机位置(z 越负越远)
|
||||
cam_z = self._location_z
|
||||
# 透视投影:z' = z + cam_z,当 cam_z != 0 时做透视除法
|
||||
# 简化为:将 3D 点投影到 2D
|
||||
# 点 (x,y,z) -> (x*r00+y*r01+z*r02+tx, x*r10+y*r11+z*r12+ty, x*r20+y*r21+z*r22+tz)
|
||||
# 透视:screen_x = x' * d / (d - z'), screen_y = y' * d / (d - z')
|
||||
d = -cam_z # 相机到原点距离
|
||||
if abs(d) < 1e-10:
|
||||
# 正交投影
|
||||
return [r00, r01, tx, r10, r11, ty, 0.0, 0.0, 1.0]
|
||||
# 透视投影矩阵(近似):将 z 影响缩放到 x,y
|
||||
# m[0]=r00, m[1]=r01, m[2]=tx, m[3]=r10, m[4]=r11, m[5]=ty
|
||||
# 透视项:m[6]=r20/d, m[7]=r21/d, m[8]=1 - tz/d
|
||||
return [r00, r01, tx,
|
||||
r10, r11, ty,
|
||||
r20 / d, r21 / d, 1.0 - tz / d]
|
||||
|
||||
def getMatrix(self, matrix: Matrix):
|
||||
matrix.reset()
|
||||
"""计算变换矩阵并写入给定的 Matrix。"""
|
||||
matrix._values = self._compute_matrix()
|
||||
|
||||
def applyToCanvas(self, canvas: "Canvas"):
|
||||
pass
|
||||
"""将相机变换应用到 Canvas。"""
|
||||
m = Matrix()
|
||||
self.getMatrix(m)
|
||||
if hasattr(canvas, "concat"):
|
||||
canvas.concat(m)
|
||||
|
||||
def setLocation(self, x: float, y: float, z: float):
|
||||
self._location_x = x
|
||||
@@ -1592,7 +1874,17 @@ class Camera(Stub):
|
||||
self._location_z = z
|
||||
|
||||
def dotWithNormal(self, dx: float, dy: float, dz: float) -> float:
|
||||
return 0.0
|
||||
"""计算向量与变换后 Z 轴法线的点积。"""
|
||||
rx = math.radians(self._rot_x)
|
||||
ry = math.radians(self._rot_y)
|
||||
rz = math.radians(self._rot_z)
|
||||
cy, sy = math.cos(ry), math.sin(ry)
|
||||
cx, sx = math.cos(rx), math.sin(rx)
|
||||
# 变换后的 Z 轴法线
|
||||
nz_x = sy
|
||||
nz_y = -cy * sx
|
||||
nz_z = cy * cx
|
||||
return dx * nz_x + dy * nz_y + dz * nz_z
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -1660,8 +1952,15 @@ class Picture(Stub):
|
||||
self._canvas = None
|
||||
|
||||
def draw(self, canvas: "Canvas"):
|
||||
"""回放录制的绘制命令到指定 Canvas。"""
|
||||
for cmd in self._recorded:
|
||||
pass
|
||||
method = cmd.get("method")
|
||||
args = cmd.get("args", [])
|
||||
if method and hasattr(canvas, method):
|
||||
try:
|
||||
getattr(canvas, method)(*args)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def getWidth(self) -> int:
|
||||
return self._canvas.getWidth() if hasattr(self, "_canvas") and self._canvas else 0
|
||||
@@ -1690,7 +1989,9 @@ class PictureDrawable(Stub):
|
||||
self._picture = picture
|
||||
|
||||
def draw(self, canvas: "Canvas"):
|
||||
pass
|
||||
"""将 Picture 回放到指定 Canvas。"""
|
||||
if self._picture is not None:
|
||||
self._picture.draw(canvas)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -1704,6 +2005,10 @@ class SurfaceTexture(Stub):
|
||||
super().__init__()
|
||||
self._tex_name = texName
|
||||
self._timestamp = 0
|
||||
self._buffer_width = 0
|
||||
self._buffer_height = 0
|
||||
self._attached = texName != 0
|
||||
self._released = False
|
||||
|
||||
def getTimestamp(self) -> int:
|
||||
return self._timestamp
|
||||
@@ -1712,16 +2017,24 @@ class SurfaceTexture(Stub):
|
||||
self._timestamp = int(time.time() * 1e9)
|
||||
|
||||
def release(self):
|
||||
pass
|
||||
"""释放 SurfaceTexture 资源。"""
|
||||
self._released = True
|
||||
self._attached = False
|
||||
|
||||
def setDefaultBufferSize(self, width: int, height: int):
|
||||
pass
|
||||
"""设置默认缓冲区大小。"""
|
||||
self._buffer_width = width
|
||||
self._buffer_height = height
|
||||
|
||||
def attachToGLContext(self, texName: int):
|
||||
pass
|
||||
"""将 SurfaceTexture 附加到 GL 纹理。"""
|
||||
self._tex_name = texName
|
||||
self._attached = True
|
||||
self._released = False
|
||||
|
||||
def detachFromGLContext(self):
|
||||
pass
|
||||
"""从 GL 纹理分离 SurfaceTexture。"""
|
||||
self._attached = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -1768,6 +2081,59 @@ class Region(Stub):
|
||||
return self._rect.contains(x, y)
|
||||
|
||||
def op(self, *args) -> bool:
|
||||
"""对区域执行布尔运算。支持 op(rect, op) / op(region, op) / op(left,top,right,bottom,op)。"""
|
||||
if len(args) == 2:
|
||||
operand, op_code = args
|
||||
if isinstance(operand, Rect):
|
||||
other = Rect(operand.left, operand.top, operand.right, operand.bottom)
|
||||
elif isinstance(operand, Region):
|
||||
other = Rect(operand._rect.left, operand._rect.top,
|
||||
operand._rect.right, operand._rect.bottom)
|
||||
else:
|
||||
return False
|
||||
elif len(args) == 5:
|
||||
left, top, right, bottom, op_code = args
|
||||
other = Rect(left, top, right, bottom)
|
||||
else:
|
||||
return False
|
||||
self_r = self._rect
|
||||
if op_code == Region.OP_INTERSECT:
|
||||
nL = max(self_r.left, other.left)
|
||||
nT = max(self_r.top, other.top)
|
||||
nR = min(self_r.right, other.right)
|
||||
nB = min(self_r.bottom, other.bottom)
|
||||
if nL < nR and nT < nB:
|
||||
self_r.set(nL, nT, nR, nB)
|
||||
return True
|
||||
self_r.setEmpty()
|
||||
return False
|
||||
if op_code == Region.OP_UNION:
|
||||
if self_r.isEmpty():
|
||||
self_r.set(other.left, other.top, other.right, other.bottom)
|
||||
else:
|
||||
self_r.left = min(self_r.left, other.left)
|
||||
self_r.top = min(self_r.top, other.top)
|
||||
self_r.right = max(self_r.right, other.right)
|
||||
self_r.bottom = max(self_r.bottom, other.bottom)
|
||||
return True
|
||||
if op_code == Region.OP_REPLACE:
|
||||
self_r.set(other.left, other.top, other.right, other.bottom)
|
||||
return True
|
||||
if op_code == Region.OP_DIFFERENCE:
|
||||
# 差集:若不相交则不变;若完全包含则清空;否则近似为不变
|
||||
if not Rect.intersects(self_r, other):
|
||||
return True
|
||||
if other.contains(self_r.left, self_r.top) and other.contains(self_r.right - 1, self_r.bottom - 1):
|
||||
self_r.setEmpty()
|
||||
return False
|
||||
return True
|
||||
if op_code == Region.OP_REVERSE_DIFFERENCE:
|
||||
self_r.set(other.left, other.top, other.right, other.bottom)
|
||||
return True
|
||||
if op_code == Region.OP_XOR:
|
||||
if not Rect.intersects(self_r, other):
|
||||
self_r.union(other.left, other.top, other.right, other.bottom)
|
||||
return True
|
||||
return False
|
||||
|
||||
def isRect(self) -> bool:
|
||||
@@ -1894,10 +2260,32 @@ class Paint(Stub):
|
||||
|
||||
def setTextSize(self, textSize: float):
|
||||
self._text_size = textSize
|
||||
self._font_cache = None
|
||||
|
||||
def getTextSize(self) -> float:
|
||||
return self._text_size
|
||||
|
||||
def _get_font(self):
|
||||
"""获取 PIL ImageFont 对象(带缓存)。"""
|
||||
if not HAS_PIL:
|
||||
return None
|
||||
cache_key = (self._text_size, id(self._typeface) if self._typeface else None)
|
||||
if getattr(self, "_font_cache_key", None) == cache_key and getattr(self, "_font_cache", None):
|
||||
return self._font_cache
|
||||
try:
|
||||
from PIL import ImageFont
|
||||
font_path = None
|
||||
if self._typeface and hasattr(self._typeface, "_path") and self._typeface._path:
|
||||
font_path = self._typeface._path
|
||||
if font_path and os.path.exists(font_path):
|
||||
self._font_cache = ImageFont.truetype(font_path, int(self._text_size))
|
||||
else:
|
||||
self._font_cache = ImageFont.load_default()
|
||||
self._font_cache_key = cache_key
|
||||
return self._font_cache
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def setTypeface(self, typeface: Optional[Typeface]):
|
||||
self._typeface = typeface
|
||||
|
||||
@@ -2004,13 +2392,31 @@ class Paint(Stub):
|
||||
self._shadow_layer = None
|
||||
|
||||
def measureText(self, text: str) -> float:
|
||||
"""测量文本宽度。优先使用 PIL ImageFont 获取精确宽度。"""
|
||||
font = self._get_font()
|
||||
if font is not None:
|
||||
try:
|
||||
if hasattr(font, "getlength"):
|
||||
return float(font.getlength(text))
|
||||
bbox = font.getbbox(text)
|
||||
return float(bbox[2] - bbox[0])
|
||||
except Exception:
|
||||
pass
|
||||
return len(text) * self._text_size * 0.5
|
||||
|
||||
def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int:
|
||||
"""在最大宽度内截断文本,返回可容纳的字符数。"""
|
||||
font = self._get_font()
|
||||
width = 0.0
|
||||
count = 0
|
||||
for i, ch in enumerate(text):
|
||||
cw = self._text_size * 0.5
|
||||
if font is not None:
|
||||
try:
|
||||
cw = font.getlength(ch) if hasattr(font, "getlength") else (font.getbbox(ch)[2])
|
||||
except Exception:
|
||||
cw = self._text_size * 0.5
|
||||
else:
|
||||
cw = self._text_size * 0.5
|
||||
if width + cw > maxWidth:
|
||||
break
|
||||
width += cw
|
||||
@@ -2019,9 +2425,16 @@ class Paint(Stub):
|
||||
return count
|
||||
|
||||
def getTextWidths(self, text: str, widths: List[float]) -> int:
|
||||
cw = self._text_size * 0.5
|
||||
"""获取每个字符的宽度。"""
|
||||
font = self._get_font()
|
||||
for i, ch in enumerate(text):
|
||||
widths[i] = cw
|
||||
if font is not None:
|
||||
try:
|
||||
widths[i] = float(font.getlength(ch)) if hasattr(font, "getlength") else float(font.getbbox(ch)[2])
|
||||
except Exception:
|
||||
widths[i] = self._text_size * 0.5
|
||||
else:
|
||||
widths[i] = self._text_size * 0.5
|
||||
return len(text)
|
||||
|
||||
def getTextPath(self, text: str, start: int, end: int, x: float, y: float, path: Path):
|
||||
@@ -2030,7 +2443,22 @@ class Paint(Stub):
|
||||
path.lineTo(x + (i - start + 1) * self._text_size * 0.3, y)
|
||||
|
||||
def getFontMetrics(self) -> Dict[str, float]:
|
||||
"""返回字体度量信息。优先使用 PIL ImageFont 获取真实度量。"""
|
||||
size = self._text_size
|
||||
font = self._get_font()
|
||||
if font is not None:
|
||||
try:
|
||||
if hasattr(font, "getmetrics"):
|
||||
ascent, descent = font.getmetrics()
|
||||
return {
|
||||
"ascent": -float(ascent),
|
||||
"descent": float(descent),
|
||||
"top": -float(ascent) - 2.0,
|
||||
"bottom": float(descent) + 2.0,
|
||||
"leading": 0.0,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"ascent": -size * 0.8,
|
||||
"descent": size * 0.2,
|
||||
@@ -2049,6 +2477,82 @@ class Paint(Stub):
|
||||
def descent(self) -> float:
|
||||
return self._text_size * 0.2
|
||||
|
||||
def setStrokeCap(self, cap):
|
||||
self._stroke_cap = cap
|
||||
|
||||
def getStrokeCap(self):
|
||||
return getattr(self, '_stroke_cap', 0)
|
||||
|
||||
def setStrokeJoin(self, join):
|
||||
self._stroke_join = join
|
||||
|
||||
def getStrokeJoin(self):
|
||||
return getattr(self, '_stroke_join', 0)
|
||||
|
||||
def setStrokeMiter(self, miter):
|
||||
self._stroke_miter = miter
|
||||
|
||||
def getStrokeMiter(self):
|
||||
return getattr(self, '_stroke_miter', 4.0)
|
||||
|
||||
def getTextBounds(self, text, start, end, bounds):
|
||||
"""返回文本边界矩形。优先使用 PIL ImageFont 获取真实边界。"""
|
||||
font = self._get_font()
|
||||
if font is not None:
|
||||
try:
|
||||
sub = text[start:end]
|
||||
bbox = font.getbbox(sub)
|
||||
bounds.left = int(bbox[0])
|
||||
bounds.top = int(bbox[1])
|
||||
bounds.right = int(bbox[2])
|
||||
bounds.bottom = int(bbox[3])
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(self, '_text_size'):
|
||||
w = len(text[start:end]) * self._text_size * 0.5
|
||||
h = self._text_size
|
||||
else:
|
||||
w = len(text[start:end]) * 12
|
||||
h = 24
|
||||
bounds.left = 0
|
||||
bounds.top = -int(h * 0.8)
|
||||
bounds.right = int(w)
|
||||
bounds.bottom = int(h * 0.2)
|
||||
|
||||
def setBlendMode(self, blendmode):
|
||||
self._blend_mode = blendmode
|
||||
|
||||
def getBlendMode(self):
|
||||
return getattr(self, '_blend_mode', None)
|
||||
|
||||
def setLetterSpacing(self, letterSpacing):
|
||||
self._letter_spacing = letterSpacing
|
||||
|
||||
def getLetterSpacing(self):
|
||||
return getattr(self, '_letter_spacing', 0.0)
|
||||
|
||||
def setFontFeatureSettings(self, settings):
|
||||
self._font_feature_settings = settings
|
||||
|
||||
def getFontFeatureSettings(self):
|
||||
return getattr(self, '_font_feature_settings', None)
|
||||
|
||||
def setHinting(self, mode):
|
||||
self._hinting = mode
|
||||
|
||||
def getHinting(self):
|
||||
return getattr(self, '_hinting', 2) # HINTING_ON
|
||||
|
||||
def setFakeBoldText(self, fakeBoldText):
|
||||
self._fake_bold_text = fakeBoldText
|
||||
|
||||
def isFakeBoldText(self):
|
||||
return getattr(self, '_fake_bold_text', False)
|
||||
|
||||
def hasGlyph(self, string):
|
||||
return len(string) > 0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Bitmap
|
||||
@@ -2249,13 +2753,30 @@ class Bitmap(Stub):
|
||||
return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8)
|
||||
|
||||
def sameAs(self, other: "Bitmap") -> bool:
|
||||
"""比较两个 Bitmap 的像素是否完全相同。"""
|
||||
if not isinstance(other, Bitmap):
|
||||
return False
|
||||
if self._width != other._width or self._height != other._height:
|
||||
return False
|
||||
if self._config != other._config:
|
||||
return False
|
||||
if self._recycled or other._recycled:
|
||||
return False
|
||||
# 使用 PIL 比较像素
|
||||
if HAS_PIL and hasattr(self, "_image") and hasattr(other, "_image"):
|
||||
try:
|
||||
from PIL import ImageChops
|
||||
diff = ImageChops.difference(self._image, other._image)
|
||||
return diff.getbbox() is None
|
||||
except Exception:
|
||||
return list(self._image.getdata()) == list(other._image.getdata())
|
||||
# 回退到字节比较
|
||||
if hasattr(self, "_pixels") and hasattr(other, "_pixels"):
|
||||
return bytes(self._pixels) == bytes(other._pixels)
|
||||
return True
|
||||
|
||||
def prepareToDraw(self):
|
||||
"""提示将位图数据上传到 GPU 缓存。桌面端为空操作。"""
|
||||
pass
|
||||
|
||||
def hasAlpha(self) -> bool:
|
||||
@@ -2344,6 +2865,29 @@ class BitmapFactory(Stub):
|
||||
|
||||
@staticmethod
|
||||
def decodeResource(res, resId: int, opts: Options = None) -> Optional[Bitmap]:
|
||||
"""从资源解码位图。尝试通过 Resources 打开原始资源流。"""
|
||||
# 尝试通过 Resources 获取资源路径或流
|
||||
if res is not None:
|
||||
try:
|
||||
# 尝试 openRawResource
|
||||
stream = res.openRawResource(resId)
|
||||
if stream is not None:
|
||||
return BitmapFactory.decodeStream(stream, None, opts)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
# 尝试通过资源名查找文件
|
||||
name = res.getResourceEntryName(resId) if hasattr(res, "getResourceEntryName") else None
|
||||
if name and name != "unknown":
|
||||
for ext in (".png", ".jpg", ".jpeg", ".webp", ".bmp"):
|
||||
candidate = resolve_android_path("res/drawable/" + name + ext)
|
||||
if os.path.exists(candidate):
|
||||
return BitmapFactory.decodeFile(candidate, opts)
|
||||
candidate = resolve_android_path("res/drawable-xxhdpi/" + name + ext)
|
||||
if os.path.exists(candidate):
|
||||
return BitmapFactory.decodeFile(candidate, opts)
|
||||
except Exception:
|
||||
pass
|
||||
return Bitmap(100, 100, Bitmap.Config.ARGB_8888)
|
||||
|
||||
@staticmethod
|
||||
@@ -2547,7 +3091,22 @@ class Canvas(Stub):
|
||||
return Rect(r.left, r.top, r.right, r.bottom)
|
||||
|
||||
def quickReject(self, *args) -> bool:
|
||||
return False
|
||||
"""判断给定矩形是否完全在裁剪区域之外。"""
|
||||
clip = self._clip_stack[-1]
|
||||
if len(args) == 1:
|
||||
r = args[0]
|
||||
if isinstance(r, Rect):
|
||||
left, top, right, bottom = r.left, r.top, r.right, r.bottom
|
||||
elif isinstance(r, RectF):
|
||||
left, top, right, bottom = r.left, r.top, r.right, r.bottom
|
||||
else:
|
||||
return False
|
||||
elif len(args) == 4:
|
||||
left, top, right, bottom = args
|
||||
else:
|
||||
return False
|
||||
return (right <= clip.left or left >= clip.right or
|
||||
bottom <= clip.top or top >= clip.bottom)
|
||||
|
||||
def drawColor(self, color: int):
|
||||
if HAS_PIL and self._draw is not None:
|
||||
@@ -2747,7 +3306,37 @@ class Canvas(Stub):
|
||||
self._draw.text((x, y), text, fill=fill_color)
|
||||
|
||||
def drawTextOnPath(self, text: str, path: Path, hOffset: float, vOffset: float, paint: Paint):
|
||||
pass
|
||||
"""沿路径绘制文本。使用 PathMeasure 逐字符放置。"""
|
||||
if not text or not HAS_PIL or self._draw is None:
|
||||
return
|
||||
pm = PathMeasure(path, False)
|
||||
total = pm.getLength()
|
||||
if total <= 0:
|
||||
return
|
||||
c = paint.getColor()
|
||||
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
|
||||
dist = hOffset
|
||||
pos = [0.0, 0.0]
|
||||
tan = [0.0, 0.0]
|
||||
for ch in text:
|
||||
cw = paint.measureText(ch)
|
||||
if dist > total:
|
||||
break
|
||||
if pm.getPosTan(dist, pos, tan):
|
||||
angle = math.degrees(math.atan2(tan[1], tan[0]))
|
||||
# 使用 PIL 旋转绘制每个字符
|
||||
try:
|
||||
from PIL import Image as _PILImage
|
||||
char_img = _PILImage.new("RGBA", (int(cw) + 4, int(paint._text_size) + 4), (0, 0, 0, 0))
|
||||
from PIL import ImageDraw as _PILDraw
|
||||
cd = _PILDraw.Draw(char_img)
|
||||
cd.text((0, 0), ch, fill=fill_color)
|
||||
rotated = char_img.rotate(-angle, expand=True, resample=_PILImage.BICUBIC)
|
||||
self._image.paste(rotated, (int(pos[0] + vOffset * tan[1]), int(pos[1] - vOffset * tan[0])),
|
||||
rotated)
|
||||
except Exception:
|
||||
self._draw.text((pos[0], pos[1]), ch, fill=fill_color)
|
||||
dist += cw
|
||||
|
||||
def drawPosText(self, text: str, pos: List[float], paint: Paint):
|
||||
if HAS_PIL and self._draw is not None:
|
||||
@@ -2758,14 +3347,44 @@ class Canvas(Stub):
|
||||
self._draw.text((pos[i * 2], pos[i * 2 + 1]), ch, fill=fill_color)
|
||||
|
||||
def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None):
|
||||
pass
|
||||
"""回放 Picture 中录制的绘制命令。"""
|
||||
if picture is None:
|
||||
return
|
||||
recorded = getattr(picture, "_recorded", [])
|
||||
if not recorded:
|
||||
return
|
||||
for cmd in recorded:
|
||||
method = cmd.get("method")
|
||||
args = cmd.get("args", [])
|
||||
if method and hasattr(self, method):
|
||||
try:
|
||||
getattr(self, method)(*args)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None):
|
||||
pass
|
||||
"""绘制 NinePatch(拉伸位图)到目标矩形。"""
|
||||
if patch is None or patch._bitmap is None:
|
||||
return
|
||||
bitmap = patch._bitmap
|
||||
if dst is None:
|
||||
self.drawBitmap(bitmap, 0, 0, paint)
|
||||
return
|
||||
# 简单缩放绘制
|
||||
if isinstance(dst, (Rect, RectF)):
|
||||
dw = dst.right - dst.left
|
||||
dh = dst.bottom - dst.top
|
||||
else:
|
||||
dw, dh = dst[2] - dst[0], dst[3] - dst[1]
|
||||
scaled = Bitmap.createScaledBitmap(bitmap, int(dw), int(dh), True)
|
||||
self.drawBitmap(scaled, dst.left if hasattr(dst, "left") else dst[0],
|
||||
dst.top if hasattr(dst, "top") else dst[1], paint)
|
||||
|
||||
def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float,
|
||||
inner: RectF, innerRx: float, innerRy: float, paint: Paint):
|
||||
pass
|
||||
"""绘制双层圆角矩形(外层和内层)。"""
|
||||
self.drawRoundRect(outer, outerRx, outerRy, paint)
|
||||
self.drawRoundRect(inner, innerRx, innerRy, paint)
|
||||
|
||||
def drawBitmapMesh(self, bitmap: Bitmap, meshWidth: int, meshHeight: int,
|
||||
verts: List[float], vertOffset: int, colors: List[int],
|
||||
@@ -2785,10 +3404,12 @@ class Canvas(Stub):
|
||||
return False
|
||||
|
||||
def setDensity(self, density: int):
|
||||
pass
|
||||
"""设置画布密度(DPI)。"""
|
||||
self._density = density
|
||||
|
||||
def getDensity(self) -> int:
|
||||
return 0
|
||||
"""返回画布密度(DPI),默认 0 表示未知。"""
|
||||
return getattr(self, "_density", 0)
|
||||
|
||||
def toString(self) -> str:
|
||||
return f"Canvas({self._width}x{self._height})"
|
||||
|
||||
@@ -10,6 +10,19 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxBluetooth, LinuxSensors, HAS_BLUETOOTHCTL, LinuxPower
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxBluetooth, LinuxSensors, HAS_BLUETOOTHCTL
|
||||
_USE_LINUX = IS_LINUX
|
||||
LinuxPower = None
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
LinuxPower = None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Sensor
|
||||
@@ -251,14 +264,89 @@ class SensorManager(Stub):
|
||||
self._sensors[type_] = Sensor(type_)
|
||||
|
||||
def getDefaultSensor(self, type_: int) -> Optional[Sensor]:
|
||||
if _USE_LINUX:
|
||||
sensor = self._build_linux_sensor(type_)
|
||||
if sensor is not None:
|
||||
return sensor
|
||||
return self._sensors.get(type_)
|
||||
|
||||
def _build_linux_sensor(self, type_: int) -> Optional[Sensor]:
|
||||
"""根据 Linux 真实传感器数据构造 Sensor 对象"""
|
||||
try:
|
||||
if type_ == Sensor.TYPE_AMBIENT_TEMPERATURE:
|
||||
temps = LinuxSensors.get_temperatures()
|
||||
if temps:
|
||||
t = temps[0]
|
||||
return Sensor(Sensor.TYPE_AMBIENT_TEMPERATURE,
|
||||
name=t.get("name", "Linux Temperature"),
|
||||
vendor="Linux", resolution=0.01,
|
||||
max_range=125.0, power=0.0, min_delay=1000)
|
||||
return None
|
||||
if type_ == Sensor.TYPE_LIGHT:
|
||||
light = LinuxSensors.get_light_level()
|
||||
if light is not None:
|
||||
return Sensor(Sensor.TYPE_LIGHT, name="Linux Light Sensor",
|
||||
vendor="Linux", resolution=0.001,
|
||||
max_range=40000.0, power=0.0, min_delay=1000)
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def getSensorList(self, type_: int) -> List[Sensor]:
|
||||
if _USE_LINUX:
|
||||
return self._get_linux_sensor_list(type_)
|
||||
if type_ == -1: # ALL
|
||||
return list(self._sensors.values())
|
||||
sensor = self._sensors.get(type_)
|
||||
return [sensor] if sensor else []
|
||||
|
||||
def _get_linux_sensor_list(self, type_: int) -> List[Sensor]:
|
||||
"""从 Linux 真实传感器数据生成传感器列表"""
|
||||
sensors: List[Sensor] = []
|
||||
try:
|
||||
if type_ == -1 or type_ == Sensor.TYPE_AMBIENT_TEMPERATURE:
|
||||
for t in LinuxSensors.get_temperatures():
|
||||
sensors.append(Sensor(
|
||||
Sensor.TYPE_AMBIENT_TEMPERATURE,
|
||||
name=t.get("name", "Linux Temperature"),
|
||||
vendor="Linux", resolution=0.01,
|
||||
max_range=125.0, power=0.0, min_delay=1000))
|
||||
if type_ == -1 or type_ == Sensor.TYPE_LIGHT:
|
||||
light = LinuxSensors.get_light_level()
|
||||
if light is not None:
|
||||
sensors.append(Sensor(
|
||||
Sensor.TYPE_LIGHT, name="Linux Light Sensor",
|
||||
vendor="Linux", resolution=0.001,
|
||||
max_range=40000.0, power=0.0, min_delay=1000))
|
||||
# CPU 频率作为附加传感器 (映射到压力传感器类型以承载真实数据)
|
||||
if type_ == -1 or type_ == Sensor.TYPE_PRESSURE:
|
||||
for f in LinuxSensors.get_cpu_frequency():
|
||||
sensors.append(Sensor(
|
||||
Sensor.TYPE_PRESSURE,
|
||||
name=f"CPU{f.get('cpu', 0)} Frequency",
|
||||
vendor="Linux", resolution=1.0,
|
||||
max_range=10000.0, power=0.0, min_delay=1000))
|
||||
# 风扇转速作为附加传感器 (映射到相对湿度类型以承载真实数据)
|
||||
if type_ == -1 or type_ == Sensor.TYPE_RELATIVE_HUMIDITY:
|
||||
for fan in LinuxSensors.get_fan_speeds():
|
||||
sensors.append(Sensor(
|
||||
Sensor.TYPE_RELATIVE_HUMIDITY,
|
||||
name=f"Fan {fan.get('name', '')}",
|
||||
vendor="Linux", resolution=1.0,
|
||||
max_range=10000.0, power=0.0, min_delay=1000))
|
||||
except Exception:
|
||||
pass
|
||||
# 若未取到真实数据则回退到模拟传感器
|
||||
if not sensors:
|
||||
if type_ == -1:
|
||||
sensors = list(self._sensors.values())
|
||||
else:
|
||||
sensor = self._sensors.get(type_)
|
||||
if sensor:
|
||||
sensors = [sensor]
|
||||
return sensors
|
||||
|
||||
def getSensors(self) -> List[Sensor]:
|
||||
return list(self._sensors.values())
|
||||
|
||||
@@ -297,18 +385,56 @@ class SensorManager(Stub):
|
||||
@staticmethod
|
||||
def getRotationMatrix(R: List[float], I: List[float],
|
||||
gravity: List[float], geomagnetic: List[float]) -> bool:
|
||||
"""根据重力和地磁向量计算旋转矩阵 R 和倾斜矩阵 I。
|
||||
算法对齐 Android SensorManager.getRotationMatrix。"""
|
||||
if len(R) < 9:
|
||||
R.extend([0] * (9 - len(R)))
|
||||
if len(R) >= 9:
|
||||
R[0] = 1.0
|
||||
R[1] = 0.0
|
||||
R[2] = 0.0
|
||||
R[3] = 0.0
|
||||
R[4] = 1.0
|
||||
R[5] = 0.0
|
||||
R[6] = 0.0
|
||||
R[7] = 0.0
|
||||
R[8] = 1.0
|
||||
R.extend([0.0] * (9 - len(R)))
|
||||
gx, gy, gz = gravity[0], gravity[1], gravity[2]
|
||||
mx, my, mz = geomagnetic[0], geomagnetic[1], geomagnetic[2]
|
||||
# 归一化重力向量 -> H
|
||||
g_len = math.sqrt(gx * gx + gy * gy + gz * gz)
|
||||
if g_len < 1e-10:
|
||||
return False
|
||||
hx = gx / g_len
|
||||
hy = gy / g_len
|
||||
hz = gz / g_len
|
||||
# 叉积 A = H x M (东方向)
|
||||
ax = hy * mz - hz * my
|
||||
ay = hz * mx - hx * mz
|
||||
az = hx * my - hy * mx
|
||||
a_len = math.sqrt(ax * ax + ay * ay + az * az)
|
||||
if a_len < 1e-10:
|
||||
return False
|
||||
ax /= a_len
|
||||
ay /= a_len
|
||||
az /= a_len
|
||||
# 重新计算 M = A x H
|
||||
ex = ay * hz - az * hy
|
||||
ey = az * hx - ax * hz
|
||||
ez = ax * hy - ay * hx
|
||||
# 填充旋转矩阵 R (列优先,Android 格式)
|
||||
R[0] = ax
|
||||
R[1] = ay
|
||||
R[2] = az
|
||||
R[3] = ex
|
||||
R[4] = ey
|
||||
R[5] = ez
|
||||
R[6] = hx
|
||||
R[7] = hy
|
||||
R[8] = hz
|
||||
# 填充倾斜矩阵 I
|
||||
if I is not None and len(I) >= 9:
|
||||
inv_h = 1.0 / g_len
|
||||
inv_m = 1.0 / math.sqrt(mx * mx + my * my + mz * mz) if (mx * mx + my * my + mz * mz) > 0 else 0
|
||||
I[0] = inv_h * hx
|
||||
I[1] = inv_h * hy
|
||||
I[2] = inv_h * hz
|
||||
I[3] = inv_m * mx
|
||||
I[4] = inv_m * my
|
||||
I[5] = inv_m * mz
|
||||
I[6] = inv_h * ex
|
||||
I[7] = inv_h * ey
|
||||
I[8] = inv_h * ez
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
@@ -317,17 +443,69 @@ class SensorManager(Stub):
|
||||
|
||||
@staticmethod
|
||||
def getInclination(I: List[float]) -> float:
|
||||
"""根据倾斜矩阵 I 计算地磁倾角(弧度)。"""
|
||||
if len(I) >= 9:
|
||||
# I[5] = cos(el), I[2] = sin(el)
|
||||
# 倾角 = atan2(I[2], I[5])
|
||||
return math.atan2(I[2], I[5])
|
||||
return 0.0
|
||||
|
||||
@staticmethod
|
||||
def remapCoordinateSystem(inR: List[float], X: int, Y: int,
|
||||
outR: List[float]) -> bool:
|
||||
if len(inR) >= 9 and len(outR) >= 9:
|
||||
for i in range(9):
|
||||
outR[i] = inR[i]
|
||||
"""根据指定的 X/Y 轴重映射坐标系。"""
|
||||
if len(inR) < 9 or len(outR) < 9:
|
||||
return False
|
||||
# Android 轴常量
|
||||
AXIS_X = 1
|
||||
AXIS_Y = 2
|
||||
AXIS_Z = 3
|
||||
AXIS_MINUS_X = -1
|
||||
AXIS_MINUS_Y = -2
|
||||
AXIS_MINUS_Z = -3
|
||||
# 将轴常量映射到列索引和符号
|
||||
axis_map = {
|
||||
AXIS_X: (0, 1), AXIS_Y: (1, 1), AXIS_Z: (2, 1),
|
||||
AXIS_MINUS_X: (0, -1), AXIS_MINUS_Y: (1, -1), AXIS_MINUS_Z: (2, -1),
|
||||
}
|
||||
if X not in axis_map or Y not in axis_map:
|
||||
return False
|
||||
x_col, x_sign = axis_map[X]
|
||||
y_col, y_sign = axis_map[Y]
|
||||
# Z = X x Y
|
||||
z_col = 3 - x_col - y_col
|
||||
z_sign = x_sign * y_sign
|
||||
# 如果 X 和 Y 不正交
|
||||
if abs(x_col - y_col) != 1 and not (x_col == 0 and y_col == 2) and not (x_col == 2 and y_col == 0):
|
||||
z_sign = -z_sign
|
||||
# 填充输出矩阵(列优先)
|
||||
for row in range(3):
|
||||
outR[row * 3 + 0] = inR[row * 3 + x_col] * x_sign
|
||||
outR[row * 3 + 1] = inR[row * 3 + y_col] * y_sign
|
||||
outR[row * 3 + 2] = inR[row * 3 + z_col] * z_sign
|
||||
return True
|
||||
|
||||
def getDeviceId(self) -> str:
|
||||
"""返回传感器设备 ID。尝试从 /sys 读取真实设备标识。"""
|
||||
import os as _os
|
||||
# 尝试从 /sys/class/sensor 读取
|
||||
try:
|
||||
base = "/sys/class/sensor"
|
||||
if _os.path.isdir(base):
|
||||
names = _os.listdir(base)
|
||||
if names:
|
||||
return names[0]
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
# 尝试从 /sys/bus/iio/devices 读取
|
||||
try:
|
||||
base = "/sys/bus/iio/devices"
|
||||
if _os.path.isdir(base):
|
||||
names = _os.listdir(base)
|
||||
if names:
|
||||
return names[0]
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return "sensor_hub_0"
|
||||
|
||||
|
||||
@@ -470,6 +648,29 @@ class BatteryManager(Stub):
|
||||
super().__init__()
|
||||
|
||||
def getIntProperty(self, id: int) -> int:
|
||||
"""返回电池整型属性。优先使用 LinuxPower 读取真实数据。"""
|
||||
if _USE_LINUX and LinuxPower is not None:
|
||||
try:
|
||||
info = LinuxPower.get_battery_info() or {}
|
||||
if id == BatteryManager.BATTERY_PROPERTY_CAPACITY:
|
||||
level = info.get("level", -1)
|
||||
return int(level) if isinstance(level, (int, float)) and level >= 0 else -1
|
||||
if id == BatteryManager.BATTERY_PROPERTY_STATUS:
|
||||
status_map = {
|
||||
"charging": BatteryManager.BATTERY_STATUS_CHARGING,
|
||||
"discharging": BatteryManager.BATTERY_STATUS_DISCHARGING,
|
||||
"not charging": BatteryManager.BATTERY_STATUS_NOT_CHARGING,
|
||||
"full": BatteryManager.BATTERY_STATUS_FULL,
|
||||
"unknown": BatteryManager.BATTERY_STATUS_UNKNOWN,
|
||||
}
|
||||
return status_map.get(str(info.get("status", "unknown")).lower(),
|
||||
BatteryManager.BATTERY_STATUS_UNKNOWN)
|
||||
if id == BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER:
|
||||
voltage = info.get("voltage", 0)
|
||||
return int(voltage * 1000) if isinstance(voltage, (int, float)) else 0
|
||||
except Exception:
|
||||
pass
|
||||
# 回退到默认值
|
||||
id_values = {
|
||||
BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER: 3500,
|
||||
BatteryManager.BATTERY_PROPERTY_CAPACITY: 85,
|
||||
@@ -478,7 +679,19 @@ class BatteryManager(Stub):
|
||||
return id_values.get(id, 0)
|
||||
|
||||
def getLongProperty(self, id: int) -> int:
|
||||
return 0
|
||||
"""返回电池长整型属性。优先使用 LinuxPower 读取真实数据。"""
|
||||
if _USE_LINUX and LinuxPower is not None:
|
||||
try:
|
||||
info = LinuxPower.get_battery_info() or {}
|
||||
if id == BatteryManager.BATTERY_PROPERTY_CURRENT_NOW:
|
||||
current = info.get("current_now", 0)
|
||||
return int(current) if isinstance(current, (int, float)) else 0
|
||||
if id == BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER:
|
||||
voltage = info.get("voltage", 0)
|
||||
return int(voltage * 3600000) if isinstance(voltage, (int, float)) else 0
|
||||
except Exception:
|
||||
pass
|
||||
return self.getIntProperty(id)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -13,6 +13,10 @@ import random
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from .._core import Stub, AndroidOnlyError
|
||||
from .hardware import SensorManager as _BaseSensorManager
|
||||
from .hardware import Sensor as _BaseSensor
|
||||
from .hardware import SensorEvent as _BaseSensorEvent
|
||||
from .hardware import SensorEventListener as _BaseSensorEventListener
|
||||
|
||||
__all__ = [
|
||||
"Sensor", "SensorEvent", "SensorEventListener",
|
||||
@@ -101,8 +105,8 @@ AXIS_MINUS_Z = 6
|
||||
# Sensor (扩展)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class Sensor(Stub):
|
||||
"""Sensor — 传感器 (完整版)"""
|
||||
class Sensor(_BaseSensor):
|
||||
"""Sensor — 传感器 (完整版,继承 hardware.Sensor 的所有方法)"""
|
||||
|
||||
TYPE_ACCELEROMETER = TYPE_ACCELEROMETER
|
||||
TYPE_MAGNETIC_FIELD = TYPE_MAGNETIC_FIELD
|
||||
@@ -153,7 +157,8 @@ class Sensor(Stub):
|
||||
fifo_reserved_event_count: int = 0,
|
||||
fifo_max_event_count: int = 0,
|
||||
id_: int = 0):
|
||||
super().__init__()
|
||||
super().__init__(type_, name, vendor, version, resolution,
|
||||
max_range, power, min_delay)
|
||||
self._type = type_
|
||||
self._name = name
|
||||
self._vendor = vendor
|
||||
@@ -248,14 +253,17 @@ class Sensor(Stub):
|
||||
# SensorEvent
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class SensorEvent(Stub):
|
||||
"""SensorEvent — 传感器事件"""
|
||||
class SensorEvent(_BaseSensorEvent):
|
||||
"""SensorEvent — 传感器事件 (继承 hardware.SensorEvent 的所有方法)"""
|
||||
|
||||
def __init__(self, sensor: Optional[Sensor] = None,
|
||||
values: List[float] = None,
|
||||
accuracy: int = SENSOR_STATUS_ACCURACY_HIGH,
|
||||
timestamp: int = 0):
|
||||
super().__init__()
|
||||
if sensor is not None:
|
||||
super().__init__(sensor, accuracy)
|
||||
else:
|
||||
Stub.__init__(self)
|
||||
self.sensor = sensor
|
||||
self.values = values or [0.0, 0.0, 0.0]
|
||||
self.accuracy = accuracy
|
||||
@@ -290,8 +298,8 @@ class SensorEvent(Stub):
|
||||
# SensorEventListener
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class SensorEventListener(Stub):
|
||||
"""SensorEventListener — 传感器事件监听器"""
|
||||
class SensorEventListener(_BaseSensorEventListener):
|
||||
"""SensorEventListener — 传感器事件监听器 (继承 hardware.SensorEventListener 的所有方法)"""
|
||||
|
||||
def onSensorChanged(self, event: SensorEvent):
|
||||
pass
|
||||
@@ -313,8 +321,8 @@ class SensorEventListener(Stub):
|
||||
# SensorManager (扩展)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class SensorManager(Stub):
|
||||
"""SensorManager — 传感器管理器 (完整版)"""
|
||||
class SensorManager(_BaseSensorManager):
|
||||
"""SensorManager — 传感器管理器 (完整版,继承 hardware.SensorManager)"""
|
||||
|
||||
AXIS_X = AXIS_X
|
||||
AXIS_Y = AXIS_Y
|
||||
|
||||
@@ -16,6 +16,12 @@ from .._core import (
|
||||
_sdcard_root, _app_data_dir, _package_name,
|
||||
)
|
||||
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxLocation
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Location
|
||||
@@ -614,6 +620,9 @@ class GnssStatus(Stub):
|
||||
return 0.0
|
||||
|
||||
def getBasebandCn0DbHz(self, index: int) -> float:
|
||||
"""返回指定卫星的基带载噪比(dB-Hz)。"""
|
||||
if 0 <= index < len(self._satellites):
|
||||
return self._satellites[index].get("baseband_cn0", 0.0)
|
||||
return 0.0
|
||||
|
||||
def getCn0DbHz(self, index: int) -> float:
|
||||
@@ -867,6 +876,42 @@ class Address(Stub):
|
||||
def setFeatureName(self, name: str):
|
||||
self._feature_name = name
|
||||
|
||||
def setAdminArea(self, area: str):
|
||||
self._admin_area = area
|
||||
|
||||
def setSubAdminArea(self, area: str):
|
||||
self._sub_admin_area = area
|
||||
|
||||
def setLocality(self, locality: str):
|
||||
self._locality = locality
|
||||
|
||||
def setSubLocality(self, sub_locality: str):
|
||||
self._sub_locality = sub_locality
|
||||
|
||||
def setPremises(self, premises: str):
|
||||
self._premises = premises
|
||||
|
||||
def setThoroughfare(self, thoroughfare: str):
|
||||
self._thoroughfare = thoroughfare
|
||||
|
||||
def setSubThoroughfare(self, sub_thoroughfare: str):
|
||||
self._sub_thoroughfare = sub_thoroughfare
|
||||
|
||||
def setPostalCode(self, postal_code: str):
|
||||
self._postal_code = postal_code
|
||||
|
||||
def setCountryName(self, country_name: str):
|
||||
self._country_name = country_name
|
||||
|
||||
def setCountryCode(self, country_code: str):
|
||||
self._country_code = country_code
|
||||
|
||||
def setPhone(self, phone: str):
|
||||
self._phone = phone
|
||||
|
||||
def setUrl(self, url: str):
|
||||
self._url = url
|
||||
|
||||
def setLatitude(self, latitude: float):
|
||||
self._latitude = latitude
|
||||
self._has_lat_lon = True
|
||||
@@ -892,29 +937,144 @@ class Address(Stub):
|
||||
class Geocoder(Stub):
|
||||
"""Geocoder — 地理编码/逆地理编码"""
|
||||
|
||||
# Nominatim (OpenStreetMap) 免费 API
|
||||
_NOMINATIM_URL = "https://nominatim.openstreetmap.org"
|
||||
_USER_AGENT = "Android-pyApi/1.0"
|
||||
|
||||
def __init__(self, context: Any = None, locale: Any = None):
|
||||
super().__init__()
|
||||
self._context = context
|
||||
self._locale = locale
|
||||
|
||||
def _get_locale_str(self) -> str:
|
||||
if self._locale is None:
|
||||
return "en"
|
||||
if hasattr(self._locale, "getLanguage"):
|
||||
lang = self._locale.getLanguage()
|
||||
country = self._locale.getCountry()
|
||||
return f"{lang}-{country}" if country else lang
|
||||
return str(self._locale)
|
||||
|
||||
@staticmethod
|
||||
def _http_get_json(url: str) -> Optional[Any]:
|
||||
"""使用 urllib 发送 GET 请求并解析 JSON。"""
|
||||
try:
|
||||
import urllib.request
|
||||
import json
|
||||
req = urllib.request.Request(url, headers={
|
||||
"User-Agent": Geocoder._USER_AGENT,
|
||||
"Accept-Language": "en",
|
||||
})
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def getFromLocation(self, latitude: float, longitude: float,
|
||||
max_results: int = 1) -> List[Address]:
|
||||
"""逆地理编码:根据经纬度获取地址。优先使用 Nominatim API。"""
|
||||
results: List[Address] = []
|
||||
# 尝试在线逆地理编码
|
||||
try:
|
||||
url = (f"{self._NOMINATIM_URL}/reverse?"
|
||||
f"lat={latitude}&lon={longitude}&format=jsonv2"
|
||||
f"&addressdetails=1&limit={max_results}")
|
||||
data = self._http_get_json(url)
|
||||
if data and isinstance(data, dict) and "address" in data:
|
||||
addr = self._parse_nominatim_reverse(data)
|
||||
results.append(addr)
|
||||
except Exception:
|
||||
pass
|
||||
# 回退到默认地址
|
||||
if not results:
|
||||
addr = Address()
|
||||
addr.setLatitude(latitude)
|
||||
addr.setLongitude(longitude)
|
||||
addr.setFeatureName("Unknown Location")
|
||||
addr.setLocality("Unknown")
|
||||
addr.setCountryName("Unknown")
|
||||
results.append(addr)
|
||||
return results[:max_results] if max_results > 0 else []
|
||||
|
||||
def _parse_nominatim_reverse(self, data: dict) -> Address:
|
||||
"""解析 Nominatim 逆地理编码响应。"""
|
||||
addr = Address()
|
||||
addr.setLatitude(latitude)
|
||||
addr.setLongitude(longitude)
|
||||
addr.setFeatureName("Unknown Location")
|
||||
addr.setLocality("Unknown")
|
||||
addr.setCountryName("Unknown")
|
||||
return [addr] if max_results > 0 else []
|
||||
address = data.get("address", {})
|
||||
lat = data.get("lat")
|
||||
lon = data.get("lon")
|
||||
if lat is not None:
|
||||
addr.setLatitude(float(lat))
|
||||
if lon is not None:
|
||||
addr.setLongitude(float(lon))
|
||||
addr.setFeatureName(address.get("road") or address.get("building")
|
||||
or data.get("name") or "Unknown")
|
||||
addr.setLocality(address.get("city") or address.get("town")
|
||||
or address.get("village") or address.get("county") or "Unknown")
|
||||
addr.setAdminArea(address.get("state") or "")
|
||||
addr.setSubAdminArea(address.get("state_district") or "")
|
||||
addr.setCountryName(address.get("country") or "Unknown")
|
||||
addr.setCountryCode(address.get("country_code", "").upper())
|
||||
addr.setPostalCode(address.get("postcode") or "")
|
||||
addr.setAddressLine(0, data.get("display_name", ""))
|
||||
return addr
|
||||
|
||||
def getFromLocationName(self, location_name: str, max_results: int = 1,
|
||||
lower_left_lat: Optional[float] = None,
|
||||
lower_left_lon: Optional[float] = None,
|
||||
upper_right_lat: Optional[float] = None,
|
||||
upper_right_lon: Optional[float] = None) -> List[Address]:
|
||||
"""地理编码:根据地名获取经纬度。优先使用 Nominatim API。"""
|
||||
results: List[Address] = []
|
||||
# 尝试在线地理编码
|
||||
try:
|
||||
import urllib.parse
|
||||
params = {
|
||||
"q": location_name,
|
||||
"format": "jsonv2",
|
||||
"addressdetails": "1",
|
||||
"limit": str(max_results),
|
||||
}
|
||||
# 添加边界框
|
||||
if all(v is not None for v in (lower_left_lat, lower_left_lon,
|
||||
upper_right_lat, upper_right_lon)):
|
||||
params["viewbox"] = f"{lower_left_lon},{upper_right_lat},{upper_right_lon},{lower_left_lat}"
|
||||
params["bounded"] = "1"
|
||||
query = urllib.parse.urlencode(params)
|
||||
url = f"{self._NOMINATIM_URL}/search?{query}"
|
||||
data = self._http_get_json(url)
|
||||
if data and isinstance(data, list):
|
||||
for item in data[:max_results]:
|
||||
addr = self._parse_nominatim_search(item)
|
||||
results.append(addr)
|
||||
except Exception:
|
||||
pass
|
||||
# 回退到默认地址
|
||||
if not results:
|
||||
addr = Address()
|
||||
addr.setFeatureName(location_name)
|
||||
addr.setLocality(location_name)
|
||||
results.append(addr)
|
||||
return results[:max_results] if max_results > 0 else []
|
||||
|
||||
def _parse_nominatim_search(self, item: dict) -> Address:
|
||||
"""解析 Nominatim 搜索响应。"""
|
||||
addr = Address()
|
||||
addr.setFeatureName(location_name)
|
||||
addr.setLocality(location_name)
|
||||
return [addr] if max_results > 0 else []
|
||||
lat = item.get("lat")
|
||||
lon = item.get("lon")
|
||||
if lat is not None:
|
||||
addr.setLatitude(float(lat))
|
||||
if lon is not None:
|
||||
addr.setLongitude(float(lon))
|
||||
address = item.get("address", {})
|
||||
addr.setFeatureName(item.get("name") or address.get("road") or "Unknown")
|
||||
addr.setLocality(address.get("city") or address.get("town")
|
||||
or address.get("village") or address.get("county") or "Unknown")
|
||||
addr.setAdminArea(address.get("state") or "")
|
||||
addr.setCountryName(address.get("country") or "Unknown")
|
||||
addr.setCountryCode(address.get("country_code", "").upper())
|
||||
addr.setPostalCode(address.get("postcode") or "")
|
||||
addr.setAddressLine(0, item.get("display_name", ""))
|
||||
return addr
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -950,8 +1110,20 @@ class LocationManager(Stub):
|
||||
self._last_known_locations: Dict[str, Location] = {}
|
||||
self._enabled_providers = {self.GPS_PROVIDER, self.NETWORK_PROVIDER,
|
||||
self.PASSIVE_PROVIDER, self.FUSED_PROVIDER}
|
||||
self._test_providers: Dict[str, LocationProvider] = {}
|
||||
self._test_locations: Dict[str, Location] = {}
|
||||
self._proximity_alerts: List[Dict[str, Any]] = []
|
||||
|
||||
def getLastKnownLocation(self, provider: str) -> Optional[Location]:
|
||||
if _USE_LINUX:
|
||||
data = LinuxLocation.get_location()
|
||||
loc = Location(provider or "network")
|
||||
loc.setLatitude(data.get("latitude", 0.0))
|
||||
loc.setLongitude(data.get("longitude", 0.0))
|
||||
loc.setAccuracy(data.get("accuracy", 0))
|
||||
loc.setTime(int(datetime.now().timestamp() * 1000))
|
||||
loc.setProvider(provider or "network")
|
||||
return loc
|
||||
if provider in self._last_known_locations:
|
||||
return self._last_known_locations[provider]
|
||||
loc = Location(provider)
|
||||
@@ -969,6 +1141,18 @@ class LocationManager(Stub):
|
||||
provider = provider_or_request
|
||||
else:
|
||||
provider = self.GPS_PROVIDER
|
||||
if _USE_LINUX and listener_or_callback is not None:
|
||||
data = LinuxLocation.get_location()
|
||||
loc = Location(provider or "network")
|
||||
loc.setLatitude(data.get("latitude", 0.0))
|
||||
loc.setLongitude(data.get("longitude", 0.0))
|
||||
loc.setAccuracy(data.get("accuracy", 0))
|
||||
loc.setTime(int(datetime.now().timestamp() * 1000))
|
||||
loc.setProvider(provider or "network")
|
||||
try:
|
||||
listener_or_callback.onLocationChanged(loc)
|
||||
except Exception:
|
||||
pass
|
||||
if listener_or_callback is not None:
|
||||
if provider not in self._listeners:
|
||||
self._listeners[provider] = []
|
||||
@@ -984,9 +1168,13 @@ class LocationManager(Stub):
|
||||
del self._listeners[provider]
|
||||
|
||||
def isProviderEnabled(self, provider: str) -> bool:
|
||||
if _USE_LINUX:
|
||||
return provider == "network"
|
||||
return provider in self._enabled_providers
|
||||
|
||||
def getProviders(self, criteria: Optional[Criteria] = None, enabled_only: bool = False) -> List[str]:
|
||||
if _USE_LINUX:
|
||||
return ["network"]
|
||||
providers = list(self._providers.keys())
|
||||
if enabled_only:
|
||||
providers = [p for p in providers if p in self._enabled_providers]
|
||||
@@ -1005,19 +1193,42 @@ class LocationManager(Stub):
|
||||
|
||||
def addProximityAlert(self, latitude: float, longitude: float,
|
||||
radius: float, expiration: int, intent: Any):
|
||||
pass
|
||||
"""添加接近警报。记录警报信息,在位置更新时检查是否进入/离开区域。"""
|
||||
import time as _time
|
||||
alert = {
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"radius": radius,
|
||||
"expiration": expiration,
|
||||
"intent": intent,
|
||||
"created_at": _time.time(),
|
||||
"last_state": None, # None=未触发, True=在区域内, False=在区域外
|
||||
}
|
||||
self._proximity_alerts.append(alert)
|
||||
|
||||
def sendExtraCommand(self, provider: str, command: str, extras: Any) -> bool:
|
||||
"""向位置提供者发送额外命令。桌面端无实际效果,返回成功。"""
|
||||
return True
|
||||
|
||||
def clearTestProviderLocation(self, provider: str):
|
||||
pass
|
||||
"""清除测试提供者的位置数据。"""
|
||||
self._test_locations.pop(provider, None)
|
||||
|
||||
def setTestProviderLocation(self, provider: str, location: Location):
|
||||
pass
|
||||
"""设置测试提供者的位置。"""
|
||||
self._test_locations[provider] = location
|
||||
# 通知该提供者的监听器
|
||||
if provider in self._listeners:
|
||||
for listener in self._listeners[provider]:
|
||||
try:
|
||||
listener.onLocationChanged(location)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def removeTestProvider(self, provider: str):
|
||||
pass
|
||||
"""移除测试提供者。"""
|
||||
self._test_providers.pop(provider, None)
|
||||
self._test_locations.pop(provider, None)
|
||||
|
||||
def addTestProvider(self, name: str, requires_network: bool, requires_satellite: bool,
|
||||
requires_cell: bool, has_monetary_cost: bool, supports_altitude: bool,
|
||||
|
||||
@@ -20,6 +20,13 @@ from .._core import Stub, AndroidOnlyError, _app_data_dir, _sdcard_root
|
||||
|
||||
from ..java.lang import RuntimeException
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
|
||||
|
||||
|
||||
@@ -517,14 +524,58 @@ class ConnectivityManager(Stub):
|
||||
self._active_network: Optional[Any] = None
|
||||
self._all_networks: List[Any] = []
|
||||
|
||||
def _get_linux_active_info(self):
|
||||
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
|
||||
try:
|
||||
from .net import NetworkInfo
|
||||
interfaces = LinuxNetwork.get_interfaces()
|
||||
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
|
||||
connected = any(
|
||||
(iface.get("ifname") or iface.get("name", "")) != "lo"
|
||||
and iface.get("operstate", "").lower() == "up"
|
||||
for iface in interfaces
|
||||
)
|
||||
if not connected:
|
||||
connected = LinuxNetwork.get_connectivity()
|
||||
ip = LinuxNetwork.get_ip_address()
|
||||
iface_name = ""
|
||||
net_type = ConnectivityManager.TYPE_ETHERNET
|
||||
for iface in interfaces:
|
||||
name = iface.get("ifname") or iface.get("name", "")
|
||||
state = iface.get("operstate", "")
|
||||
if name and name != "lo" and state.lower() == "up":
|
||||
iface_name = iface_name or name
|
||||
if name and name.startswith(("wl", "wlan", "wlp")):
|
||||
net_type = ConnectivityManager.TYPE_WIFI
|
||||
iface_name = name
|
||||
data = {
|
||||
"connected": connected,
|
||||
"ip": ip,
|
||||
"interface": iface_name,
|
||||
"type_name": "WIFI" if net_type == ConnectivityManager.TYPE_WIFI else "ETHERNET",
|
||||
}
|
||||
return NetworkInfo.from_linux_data(data, network_type=net_type)
|
||||
except Exception:
|
||||
return self._active_network_info
|
||||
|
||||
def getActiveNetworkInfo(self) -> Optional[Any]:
|
||||
if _USE_LINUX:
|
||||
return self._get_linux_active_info()
|
||||
return self._active_network_info
|
||||
|
||||
def getNetworkInfo(self, networkType: int) -> Optional[Any]:
|
||||
if _USE_LINUX:
|
||||
info = self._get_linux_active_info()
|
||||
if info is not None and info.getType() == networkType:
|
||||
return info
|
||||
return None
|
||||
return None
|
||||
|
||||
def getNetworkInfo(self, network: Any) -> Optional[Any]:
|
||||
return None
|
||||
def getConnectivityStatus(self) -> bool:
|
||||
"""返回当前网络连通性 (True=已连接)"""
|
||||
if _USE_LINUX:
|
||||
return LinuxNetwork.get_connectivity()
|
||||
return False
|
||||
|
||||
def getAllNetworkInfo(self) -> List[Any]:
|
||||
return self._all_networks
|
||||
|
||||
+513
-37
@@ -4,8 +4,10 @@ Android Media 组件 — MediaPlayer, MediaRecorder, SoundPool, AudioManager 等
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import struct
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
@@ -13,6 +15,120 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, resolve_android_path
|
||||
from .graphics import PointF
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxAudio, HAS_AMIXER
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# ffprobe / ffmpeg 辅助函数
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def _ffprobe_metadata(path: str) -> Optional[Dict[str, Any]]:
|
||||
"""用 ffprobe 获取媒体文件元数据,返回解析后的 JSON 字典。
|
||||
失败时返回 None。"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ffprobe", "-v", "quiet", "-print_format", "json",
|
||||
"-show_format", "-show_streams", path],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (FileNotFoundError, subprocess.SubprocessError,
|
||||
json.JSONDecodeError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def _ffprobe_duration_ms(path: str) -> int:
|
||||
"""用 ffprobe 获取时长(毫秒),失败返回 0。"""
|
||||
data = _ffprobe_metadata(path)
|
||||
if not data:
|
||||
return 0
|
||||
try:
|
||||
fmt = data.get("format", {})
|
||||
duration = fmt.get("duration")
|
||||
if duration is not None:
|
||||
return int(float(duration) * 1000)
|
||||
# 回退:从流中找
|
||||
for stream in data.get("streams", []):
|
||||
d = stream.get("duration")
|
||||
if d is not None:
|
||||
return int(float(d) * 1000)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def _ffprobe_video_size(path: str) -> Tuple[int, int]:
|
||||
"""用 ffprobe 获取视频分辨率 (width, height),失败返回 (0, 0)。"""
|
||||
data = _ffprobe_metadata(path)
|
||||
if not data:
|
||||
return (0, 0)
|
||||
try:
|
||||
for stream in data.get("streams", []):
|
||||
if stream.get("codec_type") == "video":
|
||||
w = int(stream.get("width", 0))
|
||||
h = int(stream.get("height", 0))
|
||||
if w > 0 and h > 0:
|
||||
return (w, h)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return (0, 0)
|
||||
|
||||
|
||||
def _ffmpeg_extract_frame(path: str, time_us: int = -1,
|
||||
width: int = 0, height: int = 0) -> Optional[Any]:
|
||||
"""用 ffmpeg 提取指定时间点的视频帧,返回 PIL Image。
|
||||
time_us < 0 表示提取第一帧。失败返回 None。"""
|
||||
if not HAS_PIL:
|
||||
return None
|
||||
try:
|
||||
from PIL import Image
|
||||
cmd = ["ffmpeg", "-v", "quiet"]
|
||||
if time_us is not None and time_us >= 0:
|
||||
cmd += ["-ss", str(time_us / 1_000_000.0)]
|
||||
cmd += ["-i", path, "-frames:v", "1", "-f", "image2pipe",
|
||||
"-vcodec", "png", "-"]
|
||||
if width > 0 and height > 0:
|
||||
# 在解码后缩放
|
||||
cmd = ["ffmpeg", "-v", "quiet"]
|
||||
if time_us is not None and time_us >= 0:
|
||||
cmd += ["-ss", str(time_us / 1_000_000.0)]
|
||||
cmd += ["-i", path, "-frames:v", "1", "-vf",
|
||||
f"scale={width}:{height}", "-f", "image2pipe",
|
||||
"-vcodec", "png", "-"]
|
||||
result = subprocess.run(cmd, capture_output=True, timeout=30)
|
||||
if result.returncode != 0 or not result.stdout:
|
||||
return None
|
||||
return Image.open(io.BytesIO(result.stdout))
|
||||
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _ffmpeg_extract_cover(path: str) -> Optional[bytes]:
|
||||
"""用 ffmpeg 提取嵌入的封面图片,返回原始字节。
|
||||
失败返回 None。"""
|
||||
try:
|
||||
# 尝试提取附加图片流(封面)
|
||||
cmd = ["ffmpeg", "-v", "quiet", "-i", path,
|
||||
"-map", "0:v", "-frames:v", "1", "-f", "image2pipe",
|
||||
"-vcodec", "png", "-"]
|
||||
result = subprocess.run(cmd, capture_output=True, timeout=30)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
return result.stdout
|
||||
return None
|
||||
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# AudioAttributes
|
||||
@@ -236,6 +352,7 @@ class MediaPlayer(Stub):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._data_source = None
|
||||
self._resolved_path = None
|
||||
self._prepared = False
|
||||
self._playing = False
|
||||
self._looping = False
|
||||
@@ -250,6 +367,8 @@ class MediaPlayer(Stub):
|
||||
self._audio_attributes = None
|
||||
self._display = None
|
||||
self._surface = None
|
||||
self._released = False
|
||||
self._start_time = 0.0 # 用于模拟播放进度
|
||||
|
||||
# Listeners
|
||||
self._on_prepared_listener = None
|
||||
@@ -260,43 +379,126 @@ class MediaPlayer(Stub):
|
||||
self._on_video_size_changed_listener = None
|
||||
|
||||
def setDataSource(self, path: str = None, uri=None, headers: Dict = None):
|
||||
self._data_source = path or str(uri)
|
||||
self._data_source = path or str(uri) if uri is not None else path
|
||||
if self._data_source:
|
||||
self._resolved_path = resolve_android_path(self._data_source)
|
||||
|
||||
def _load_media_info(self):
|
||||
"""用 ffprobe 加载时长和分辨率信息(try/except 保护)。"""
|
||||
if not self._resolved_path:
|
||||
return
|
||||
if not os.path.exists(self._resolved_path):
|
||||
return
|
||||
try:
|
||||
duration = _ffprobe_duration_ms(self._resolved_path)
|
||||
if duration > 0:
|
||||
self._duration = duration
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
w, h = _ffprobe_video_size(self._resolved_path)
|
||||
if w > 0 and h > 0:
|
||||
self._video_width = w
|
||||
self._video_height = h
|
||||
if self._on_video_size_changed_listener:
|
||||
self._on_video_size_changed_listener.onVideoSizeChanged(
|
||||
self, w, h)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def prepare(self):
|
||||
if self._released:
|
||||
return
|
||||
if self._resolved_path and not os.path.exists(self._resolved_path):
|
||||
if self._on_error_listener:
|
||||
self._on_error_listener.onError(
|
||||
self, MediaPlayer.MEDIA_ERROR_UNKNOWN, -1004)
|
||||
raise IOError(f"数据源文件不存在: {self._resolved_path}")
|
||||
self._load_media_info()
|
||||
self._prepared = True
|
||||
|
||||
def prepareAsync(self):
|
||||
if self._released:
|
||||
return
|
||||
if self._resolved_path and not os.path.exists(self._resolved_path):
|
||||
if self._on_error_listener:
|
||||
self._on_error_listener.onError(
|
||||
self, MediaPlayer.MEDIA_ERROR_UNKNOWN, -1004)
|
||||
return
|
||||
self._load_media_info()
|
||||
self._prepared = True
|
||||
if self._on_prepared_listener:
|
||||
self._on_prepared_listener.onPrepared(self)
|
||||
|
||||
def start(self):
|
||||
if self._released:
|
||||
return
|
||||
self._playing = True
|
||||
self._start_time = time.time()
|
||||
|
||||
def stop(self):
|
||||
self._playing = False
|
||||
|
||||
def pause(self):
|
||||
if self._playing:
|
||||
# 记录已播放的进度
|
||||
elapsed = int((time.time() - self._start_time) * 1000)
|
||||
self._current_position = min(
|
||||
self._current_position + elapsed, self._duration)
|
||||
self._playing = False
|
||||
|
||||
def reset(self):
|
||||
self._playing = False
|
||||
self._prepared = False
|
||||
self._current_position = 0
|
||||
self._duration = 0
|
||||
self._video_width = 0
|
||||
self._video_height = 0
|
||||
self._data_source = None
|
||||
self._resolved_path = None
|
||||
|
||||
def release(self):
|
||||
self._playing = False
|
||||
self._prepared = False
|
||||
self._released = True
|
||||
self._data_source = None
|
||||
self._resolved_path = None
|
||||
self._on_prepared_listener = None
|
||||
self._on_completion_listener = None
|
||||
self._on_error_listener = None
|
||||
self._on_info_listener = None
|
||||
self._on_seek_complete_listener = None
|
||||
self._on_video_size_changed_listener = None
|
||||
|
||||
def seekTo(self, msec: int):
|
||||
self._current_position = max(0, min(msec, self._duration))
|
||||
self._start_time = time.time()
|
||||
if self._on_seek_complete_listener:
|
||||
self._on_seek_complete_listener.onSeekComplete(self)
|
||||
|
||||
def getCurrentPosition(self) -> int:
|
||||
if self._playing and self._start_time > 0:
|
||||
elapsed = int((time.time() - self._start_time) * 1000)
|
||||
pos = self._current_position + elapsed
|
||||
if self._duration > 0 and pos >= self._duration:
|
||||
pos = self._duration
|
||||
if self._looping:
|
||||
self._current_position = 0
|
||||
self._start_time = time.time()
|
||||
pos = 0
|
||||
else:
|
||||
self._playing = False
|
||||
if self._on_completion_listener:
|
||||
self._on_completion_listener.onCompletion(self)
|
||||
return pos
|
||||
return self._current_position
|
||||
|
||||
def getDuration(self) -> int:
|
||||
if self._duration == 0 and self._resolved_path:
|
||||
try:
|
||||
self._duration = _ffprobe_duration_ms(self._resolved_path)
|
||||
except Exception:
|
||||
pass
|
||||
return self._duration
|
||||
|
||||
def isPlaying(self) -> bool:
|
||||
@@ -328,9 +530,25 @@ class MediaPlayer(Stub):
|
||||
self._display = surface_holder
|
||||
|
||||
def getVideoWidth(self) -> int:
|
||||
if self._video_width == 0 and self._resolved_path:
|
||||
try:
|
||||
w, h = _ffprobe_video_size(self._resolved_path)
|
||||
if w > 0:
|
||||
self._video_width = w
|
||||
self._video_height = h
|
||||
except Exception:
|
||||
pass
|
||||
return self._video_width
|
||||
|
||||
def getVideoHeight(self) -> int:
|
||||
if self._video_height == 0 and self._resolved_path:
|
||||
try:
|
||||
w, h = _ffprobe_video_size(self._resolved_path)
|
||||
if h > 0:
|
||||
self._video_width = w
|
||||
self._video_height = h
|
||||
except Exception:
|
||||
pass
|
||||
return self._video_height
|
||||
|
||||
def setOnPreparedListener(self, listener: "MediaPlayer.OnPreparedListener"):
|
||||
@@ -427,6 +645,15 @@ class MediaRecorder(Stub):
|
||||
self._output_file = None
|
||||
self._preview_display = None
|
||||
self._recording = False
|
||||
self._prepared = False
|
||||
self._released = False
|
||||
self._video_width = 0
|
||||
self._video_height = 0
|
||||
self._video_frame_rate = 0
|
||||
self._video_encoding_bit_rate = 0
|
||||
self._audio_encoding_bit_rate = 0
|
||||
self._max_duration = 0
|
||||
self._max_filesize = 0
|
||||
|
||||
def setAudioSource(self, audio_source: int):
|
||||
self._audio_source = audio_source
|
||||
@@ -446,13 +673,36 @@ class MediaRecorder(Stub):
|
||||
def setOutputFile(self, path: str):
|
||||
self._output_file = path
|
||||
|
||||
def setVideoSize(self, width: int, height: int):
|
||||
self._video_width = width
|
||||
self._video_height = height
|
||||
|
||||
def setVideoFrameRate(self, rate: int):
|
||||
self._video_frame_rate = rate
|
||||
|
||||
def setVideoEncodingBitRate(self, bit_rate: int):
|
||||
self._video_encoding_bit_rate = bit_rate
|
||||
|
||||
def setAudioEncodingBitRate(self, bit_rate: int):
|
||||
self._audio_encoding_bit_rate = bit_rate
|
||||
|
||||
def setMaxDuration(self, max_duration_ms: int):
|
||||
self._max_duration = max_duration_ms
|
||||
|
||||
def setMaxFileSize(self, max_filesize_bytes: int):
|
||||
self._max_filesize = max_filesize_bytes
|
||||
|
||||
def setPreviewDisplay(self, surface_holder):
|
||||
self._preview_display = surface_holder
|
||||
|
||||
def prepare(self):
|
||||
pass
|
||||
if self._released:
|
||||
return
|
||||
self._prepared = True
|
||||
|
||||
def start(self):
|
||||
if self._released or not self._prepared:
|
||||
return
|
||||
self._recording = True
|
||||
|
||||
def stop(self):
|
||||
@@ -460,11 +710,29 @@ class MediaRecorder(Stub):
|
||||
|
||||
def reset(self):
|
||||
self._recording = False
|
||||
self._prepared = False
|
||||
self._audio_source = 0
|
||||
self._video_source = 0
|
||||
self._output_format = 0
|
||||
self._audio_encoder = 0
|
||||
self._video_encoder = 0
|
||||
self._output_file = None
|
||||
self._video_width = 0
|
||||
self._video_height = 0
|
||||
self._video_frame_rate = 0
|
||||
self._video_encoding_bit_rate = 0
|
||||
|
||||
def release(self):
|
||||
self._recording = False
|
||||
self._prepared = False
|
||||
self._released = True
|
||||
self._output_file = None
|
||||
self._preview_display = None
|
||||
|
||||
def getMaxAmplitude(self) -> int:
|
||||
if self._recording:
|
||||
import random
|
||||
return random.randint(0, 32767)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -596,15 +864,37 @@ class AudioManager(Stub):
|
||||
self._parameters: Dict[str, str] = {}
|
||||
|
||||
def getStreamVolume(self, streamType: int) -> int:
|
||||
if _USE_LINUX:
|
||||
# STREAM_MUSIC 及其它流类型映射到 Linux 主音量
|
||||
return LinuxAudio.get_volume()
|
||||
return self._stream_volumes.get(streamType, 0)
|
||||
|
||||
def getStreamMaxVolume(self, streamType: int) -> int:
|
||||
if _USE_LINUX:
|
||||
return 100
|
||||
return 15
|
||||
|
||||
def setStreamVolume(self, streamType: int, index: int, flags: int = 0):
|
||||
if _USE_LINUX:
|
||||
# STREAM_MUSIC 及其它流类型映射到 Linux 主音量
|
||||
LinuxAudio.set_volume(index)
|
||||
return
|
||||
if streamType in self._stream_volumes:
|
||||
self._stream_volumes[streamType] = max(0, min(15, index))
|
||||
|
||||
def isMicrophoneMute(self) -> bool:
|
||||
"""麦克风是否静音"""
|
||||
if _USE_LINUX:
|
||||
return LinuxAudio.get_mute()
|
||||
return False
|
||||
|
||||
def setMicrophoneMute(self, on: bool):
|
||||
"""设置麦克风静音"""
|
||||
if _USE_LINUX:
|
||||
LinuxAudio.set_mute(on)
|
||||
return
|
||||
# 存根: 无操作
|
||||
|
||||
def adjustVolume(self, direction: int, flags: int = 0):
|
||||
pass
|
||||
|
||||
@@ -996,53 +1286,221 @@ class MediaMetadataRetriever(Stub):
|
||||
METADATA_KEY_BITRATE = "bitrate"
|
||||
METADATA_KEY_LOCATION = "location"
|
||||
|
||||
# 提取帧选项
|
||||
OPTION_CLOSEST = 0x01
|
||||
OPTION_CLOSEST_SYNC = 0x02
|
||||
OPTION_NEXT_SYNC = 0x03
|
||||
OPTION_PREVIOUS_SYNC = 0x04
|
||||
|
||||
# ffprobe format_name 到 MIME 类型映射
|
||||
_MIME_MAP = {
|
||||
"mov,mp4,m4a,3gp,3g2,mj2": "video/mp4",
|
||||
"matroska,webm": "video/x-matroska",
|
||||
"avi": "video/x-msvideo",
|
||||
"flv": "video/x-flv",
|
||||
"wav": "audio/wav",
|
||||
"mp3": "audio/mpeg",
|
||||
"aac": "audio/aac",
|
||||
"ogg": "audio/ogg",
|
||||
"flac": "audio/flac",
|
||||
"asf": "video/x-ms-asf",
|
||||
"mpeg": "video/mpeg",
|
||||
"webm": "video/webm",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._metadata: Dict[str, str] = {}
|
||||
self._resolved_path: Optional[str] = None
|
||||
self._ffprobe_data: Optional[Dict[str, Any]] = None
|
||||
|
||||
def setDataSource(self, path: str):
|
||||
resolved = resolve_android_path(path)
|
||||
if os.path.exists(resolved):
|
||||
size = os.path.getsize(resolved)
|
||||
self._metadata = {
|
||||
MediaMetadataRetriever.METADATA_KEY_DURATION: "30000",
|
||||
MediaMetadataRetriever.METADATA_KEY_TITLE: os.path.basename(resolved),
|
||||
MediaMetadataRetriever.METADATA_KEY_MIMETYPE: "video/mp4",
|
||||
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH: "1920",
|
||||
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT: "1080",
|
||||
MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO: "yes",
|
||||
MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO: "yes",
|
||||
MediaMetadataRetriever.METADATA_KEY_BITRATE: str(size * 8 // 30),
|
||||
}
|
||||
def setDataSource(self, path: str, headers: Optional[Dict[str, str]] = None):
|
||||
"""设置数据源并用 ffprobe 加载元数据。"""
|
||||
self._resolved_path = resolve_android_path(path)
|
||||
self._metadata = {}
|
||||
self._ffprobe_data = None
|
||||
if not self._resolved_path or not os.path.exists(self._resolved_path):
|
||||
return
|
||||
# 用 ffprobe 获取真实元数据(try/except 保护)
|
||||
try:
|
||||
self._ffprobe_data = _ffprobe_metadata(self._resolved_path)
|
||||
except Exception:
|
||||
self._ffprobe_data = None
|
||||
if self._ffprobe_data:
|
||||
self._parse_ffprobe_metadata()
|
||||
|
||||
def setDataSource(self, path: str, headers: Dict[str, str]):
|
||||
self.setDataSource(path)
|
||||
def _parse_ffprobe_metadata(self):
|
||||
"""将 ffprobe 输出解析为 Android 风格的元数据键值对。"""
|
||||
if not self._ffprobe_data:
|
||||
return
|
||||
data = self._ffprobe_data
|
||||
fmt = data.get("format", {})
|
||||
tags = fmt.get("tags", {})
|
||||
streams = data.get("streams", [])
|
||||
|
||||
# 找到视频流和音频流
|
||||
video_stream = None
|
||||
audio_stream = None
|
||||
for s in streams:
|
||||
ct = s.get("codec_type", "")
|
||||
if ct == "video" and video_stream is None:
|
||||
video_stream = s
|
||||
elif ct == "audio" and audio_stream is None:
|
||||
audio_stream = s
|
||||
|
||||
md = self._metadata
|
||||
|
||||
# 时长(毫秒)
|
||||
duration = fmt.get("duration")
|
||||
if duration is None and video_stream:
|
||||
duration = video_stream.get("duration")
|
||||
if duration is None and audio_stream:
|
||||
duration = audio_stream.get("duration")
|
||||
if duration is not None:
|
||||
try:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_DURATION] = str(
|
||||
int(float(duration) * 1000))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 标题
|
||||
title = tags.get("title") or tags.get("TITLE")
|
||||
if title:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_TITLE] = title
|
||||
|
||||
# 艺术家
|
||||
artist = tags.get("artist") or tags.get("ARTIST")
|
||||
if artist:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_ARTIST] = artist
|
||||
|
||||
# 专辑
|
||||
album = tags.get("album") or tags.get("ALBUM")
|
||||
if album:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_ALBUM] = album
|
||||
|
||||
# 作曲家
|
||||
composer = tags.get("composer") or tags.get("COMPOSER")
|
||||
if composer:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_COMPOSER] = composer
|
||||
|
||||
# 作者
|
||||
writer = tags.get("writer") or tags.get("WRITER")
|
||||
if writer:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_WRITER] = writer
|
||||
|
||||
# 流派
|
||||
genre = tags.get("genre") or tags.get("GENRE")
|
||||
if genre:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_GENRE] = genre
|
||||
|
||||
# 日期 / 年份
|
||||
date = tags.get("date") or tags.get("DATE") or tags.get("year")
|
||||
if date:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_DATE] = str(date)
|
||||
# 尝试提取年份
|
||||
year_str = str(date)[:4]
|
||||
if year_str.isdigit():
|
||||
md[MediaMetadataRetriever.METADATA_KEY_YEAR] = year_str
|
||||
|
||||
# 音轨号
|
||||
track = tags.get("track") or tags.get("TRACK")
|
||||
if track:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER] = str(track)
|
||||
|
||||
# 位置
|
||||
location = tags.get("location") or tags.get("LOCATION")
|
||||
if location:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_LOCATION] = str(location)
|
||||
|
||||
# MIME 类型
|
||||
format_name = fmt.get("format_name", "")
|
||||
if format_name:
|
||||
mime = MediaMetadataRetriever._MIME_MAP.get(
|
||||
format_name, f"video/{format_name.split(',')[0]}")
|
||||
md[MediaMetadataRetriever.METADATA_KEY_MIMETYPE] = mime
|
||||
|
||||
# 轨道数
|
||||
if streams:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS] = str(len(streams))
|
||||
|
||||
# 比特率
|
||||
bit_rate = fmt.get("bit_rate")
|
||||
if bit_rate:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_BITRATE] = str(bit_rate)
|
||||
|
||||
# 视频信息
|
||||
if video_stream:
|
||||
w = video_stream.get("width", 0)
|
||||
h = video_stream.get("height", 0)
|
||||
if w:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH] = str(w)
|
||||
if h:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT] = str(h)
|
||||
md[MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO] = "yes"
|
||||
else:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO] = "no"
|
||||
|
||||
# 音频信息
|
||||
if audio_stream:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO] = "yes"
|
||||
else:
|
||||
md[MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO] = "no"
|
||||
|
||||
def extractMetadata(self, keyCode: str) -> Optional[str]:
|
||||
return self._metadata.get(keyCode)
|
||||
|
||||
def getEmbeddedPicture(self) -> Optional[bytes]:
|
||||
"""用 ffmpeg 提取嵌入的封面图片,返回原始字节数据。
|
||||
失败返回 None。"""
|
||||
if not self._resolved_path or not os.path.exists(self._resolved_path):
|
||||
return None
|
||||
try:
|
||||
return _ffmpeg_extract_cover(self._resolved_path)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def getFrameAtTime(self, timeUs: int = -1, option: int = 0) -> Optional["Bitmap"]:
|
||||
if HAS_PIL:
|
||||
from PIL import Image
|
||||
from .graphics import Bitmap
|
||||
img = Image.new("RGB", (1920, 1080), (64, 64, 64))
|
||||
bm = Bitmap(1920, 1080, Bitmap.Config.ARGB_8888)
|
||||
bm._image = img
|
||||
return bm
|
||||
"""用 ffmpeg 提取指定时间点的视频帧,返回 Bitmap(内含 PIL Image)。
|
||||
timeUs < 0 表示提取第一帧。失败返回 None。"""
|
||||
if not self._resolved_path or not os.path.exists(self._resolved_path):
|
||||
return None
|
||||
try:
|
||||
img = _ffmpeg_extract_frame(self._resolved_path, timeUs)
|
||||
if img is not None:
|
||||
from .graphics import Bitmap
|
||||
w, h = img.size
|
||||
bm = Bitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
# 确保 RGBA 模式
|
||||
if img.mode != "RGBA":
|
||||
img = img.convert("RGBA")
|
||||
bm._image = img
|
||||
return bm
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def getScaledFrameAtTime(self, timeUs: int, option: int, width: int, height: int) -> Optional["Bitmap"]:
|
||||
if HAS_PIL:
|
||||
from PIL import Image
|
||||
from .graphics import Bitmap
|
||||
img = Image.new("RGB", (width, height), (64, 64, 64))
|
||||
bm = Bitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
bm._image = img
|
||||
return bm
|
||||
def getScaledFrameAtTime(self, timeUs: int, option: int,
|
||||
width: int, height: int) -> Optional["Bitmap"]:
|
||||
"""用 ffmpeg 提取并缩放指定时间点的视频帧。"""
|
||||
if not self._resolved_path or not os.path.exists(self._resolved_path):
|
||||
return None
|
||||
try:
|
||||
img = _ffmpeg_extract_frame(self._resolved_path, timeUs, width, height)
|
||||
if img is not None:
|
||||
from .graphics import Bitmap
|
||||
bm = Bitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
if img.mode != "RGBA":
|
||||
img = img.convert("RGBA")
|
||||
bm._image = img
|
||||
return bm
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def release(self):
|
||||
self._metadata.clear()
|
||||
self._resolved_path = None
|
||||
self._ffprobe_data = None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -1788,6 +2246,7 @@ class ExifInterface(Stub):
|
||||
super().__init__()
|
||||
self._filename = resolve_android_path(filename)
|
||||
self._attributes: Dict[str, str] = {}
|
||||
self._pil_image = None
|
||||
self._load_exif()
|
||||
|
||||
def _load_exif(self):
|
||||
@@ -1799,6 +2258,7 @@ class ExifInterface(Stub):
|
||||
from PIL import ExifTags
|
||||
try:
|
||||
img = Image.open(self._filename)
|
||||
self._pil_image = img
|
||||
exif_data = img._getexif()
|
||||
if exif_data:
|
||||
for tag_id, value in exif_data.items():
|
||||
@@ -1870,13 +2330,29 @@ class ExifInterface(Stub):
|
||||
pass
|
||||
|
||||
def flipHorizontally(self):
|
||||
pass
|
||||
if hasattr(self, '_pil_image') and self._pil_image:
|
||||
from PIL import Image
|
||||
self._pil_image = self._pil_image.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
self._save_from_pil()
|
||||
|
||||
def flipVertically(self):
|
||||
pass
|
||||
if hasattr(self, '_pil_image') and self._pil_image:
|
||||
from PIL import Image
|
||||
self._pil_image = self._pil_image.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
self._save_from_pil()
|
||||
|
||||
def rotate(self, degree: int):
|
||||
pass
|
||||
def rotate(self, degrees):
|
||||
if hasattr(self, '_pil_image') and self._pil_image:
|
||||
from PIL import Image
|
||||
self._pil_image = self._pil_image.rotate(degrees, expand=True)
|
||||
self._save_from_pil()
|
||||
# 更新方向属性
|
||||
self.setAttribute("Orientation", "1") # Normal after rotation
|
||||
|
||||
def _save_from_pil(self):
|
||||
"""从 PIL 图像保存回文件"""
|
||||
if hasattr(self, '_pil_image') and self._pil_image and hasattr(self, '_filename'):
|
||||
self._pil_image.save(self._filename)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
+195
-33
@@ -17,6 +17,13 @@ from .._core import (
|
||||
_sdcard_root, _app_data_dir, _package_name,
|
||||
)
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Uri (re-export from content)
|
||||
@@ -379,6 +386,38 @@ class NetworkInfo(Stub):
|
||||
}
|
||||
return names.get(type_, "UNKNOWN")
|
||||
|
||||
@classmethod
|
||||
def from_linux_data(cls, data: Optional[dict] = None,
|
||||
network_type: int = 9) -> "NetworkInfo":
|
||||
"""从 Linux 真实网络数据构造 NetworkInfo
|
||||
|
||||
data 字典可包含: ip, interface, connected, type_name 等
|
||||
"""
|
||||
if data is None:
|
||||
data = {}
|
||||
connected = bool(data.get("connected", False))
|
||||
state = "CONNECTED" if connected else "DISCONNECTED"
|
||||
detailed_state = "CONNECTED" if connected else "DISCONNECTED"
|
||||
type_name = data.get("type_name") or cls._default_type_name_static(network_type)
|
||||
extra_info = data.get("ip") or data.get("interface")
|
||||
return cls(
|
||||
type_=network_type,
|
||||
state=state,
|
||||
detailed_state=detailed_state,
|
||||
type_name=type_name,
|
||||
extra_info=extra_info,
|
||||
is_available=True,
|
||||
roaming=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _default_type_name_static(type_: int) -> str:
|
||||
names = {
|
||||
0: "MOBILE", 1: "WIFI", 6: "WIMAX", 7: "BLUETOOTH",
|
||||
9: "ETHERNET", 17: "VPN",
|
||||
}
|
||||
return names.get(type_, "UNKNOWN")
|
||||
|
||||
def getType(self) -> int:
|
||||
return self._type
|
||||
|
||||
@@ -1040,53 +1079,98 @@ class TrafficStats(Stub):
|
||||
UNSUPPORTED = -1
|
||||
TAG_SYSTEM_UPDATE = 0
|
||||
|
||||
_cached_stats = None
|
||||
_cache_time = 0
|
||||
|
||||
@staticmethod
|
||||
def getMobileTxBytes() -> int:
|
||||
def _read_net_dev():
|
||||
"""读取 /proc/net/dev 获取网络流量"""
|
||||
try:
|
||||
with open("/proc/net/dev", "r") as f:
|
||||
lines = f.readlines()[2:] # 跳过前两行表头
|
||||
total_rx = 0
|
||||
total_tx = 0
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
ifname = parts[0].rstrip(":")
|
||||
if ifname == "lo":
|
||||
continue
|
||||
rx_bytes = int(parts[1])
|
||||
tx_bytes = int(parts[9])
|
||||
total_rx += rx_bytes
|
||||
total_tx += tx_bytes
|
||||
return {"rx_bytes": total_rx, "tx_bytes": total_tx,
|
||||
"rx_packets": 0, "tx_packets": 0}
|
||||
except Exception:
|
||||
return {"rx_bytes": 0, "tx_bytes": 0, "rx_packets": 0, "tx_packets": 0}
|
||||
|
||||
@staticmethod
|
||||
def getMobileRxBytes():
|
||||
return 0 # 桌面无移动数据
|
||||
|
||||
@staticmethod
|
||||
def getMobileTxBytes():
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def getMobileRxBytes() -> int:
|
||||
def getMobileRxPackets():
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def getTotalTxBytes() -> int:
|
||||
def getMobileTxPackets():
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def getTotalRxBytes() -> int:
|
||||
return 0
|
||||
def getTotalRxBytes():
|
||||
return TrafficStats._read_net_dev()["rx_bytes"]
|
||||
|
||||
@staticmethod
|
||||
def getMobileTxPackets() -> int:
|
||||
return 0
|
||||
def getTotalTxBytes():
|
||||
return TrafficStats._read_net_dev()["tx_bytes"]
|
||||
|
||||
@staticmethod
|
||||
def getMobileRxPackets() -> int:
|
||||
return 0
|
||||
def getTotalRxPackets():
|
||||
return TrafficStats._read_net_dev()["rx_packets"]
|
||||
|
||||
@staticmethod
|
||||
def getTotalTxPackets() -> int:
|
||||
return 0
|
||||
def getTotalTxPackets():
|
||||
return TrafficStats._read_net_dev()["tx_packets"]
|
||||
|
||||
@staticmethod
|
||||
def getTotalRxPackets() -> int:
|
||||
return 0
|
||||
def getUidRxBytes(uid):
|
||||
try:
|
||||
with open(f"/proc/uid_stat/{uid}/tcp_rcv", "r") as f:
|
||||
return int(f.read().strip())
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def getUidTxBytes(uid: int) -> int:
|
||||
return 0
|
||||
def getUidTxBytes(uid):
|
||||
try:
|
||||
with open(f"/proc/uid_stat/{uid}/tcp_snd", "r") as f:
|
||||
return int(f.read().strip())
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def getUidRxBytes(uid: int) -> int:
|
||||
return 0
|
||||
def getUidRxPackets(uid):
|
||||
return TrafficStats.getUidRxBytes(uid)
|
||||
|
||||
@staticmethod
|
||||
def getUidTxPackets(uid: int) -> int:
|
||||
return 0
|
||||
def getUidTxPackets(uid):
|
||||
return TrafficStats.getUidTxBytes(uid)
|
||||
|
||||
@staticmethod
|
||||
def getUidRxPackets(uid: int) -> int:
|
||||
return 0
|
||||
def getThreadStatsTag():
|
||||
return getattr(TrafficStats, '_thread_tag', 0)
|
||||
|
||||
@staticmethod
|
||||
def setThreadStatsTag(tag):
|
||||
TrafficStats._thread_tag = tag
|
||||
|
||||
@staticmethod
|
||||
def clearThreadStatsTag():
|
||||
TrafficStats._thread_tag = 0
|
||||
|
||||
@staticmethod
|
||||
def getUidTcpTxBytes(uid: int) -> int:
|
||||
@@ -1104,18 +1188,6 @@ class TrafficStats(Stub):
|
||||
def getUidUdpRxBytes(uid: int) -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def setThreadStatsTag(tag: int):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def clearThreadStatsTag():
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def getThreadStatsTag() -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def setThreadStatsUid(uid: int):
|
||||
pass
|
||||
@@ -1230,22 +1302,112 @@ class ConnectivityManager(Stub):
|
||||
self._network_caps: Dict[int, NetworkCapabilities] = {}
|
||||
self._link_props: Dict[int, LinkProperties] = {}
|
||||
|
||||
def _get_linux_active_info(self) -> Optional[NetworkInfo]:
|
||||
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
|
||||
try:
|
||||
interfaces = LinuxNetwork.get_interfaces()
|
||||
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
|
||||
connected = any(
|
||||
(iface.get("ifname") or iface.get("name", "")) != "lo"
|
||||
and iface.get("operstate", "").lower() == "up"
|
||||
for iface in interfaces
|
||||
)
|
||||
# 若有活跃接口但 get_connectivity 为 False (外网不可达), 仍视为已连接
|
||||
if not connected:
|
||||
connected = LinuxNetwork.get_connectivity()
|
||||
ip = LinuxNetwork.get_ip_address()
|
||||
iface_name = ""
|
||||
for iface in interfaces:
|
||||
name = iface.get("ifname") or iface.get("name", "")
|
||||
state = iface.get("operstate", "")
|
||||
if name and name != "lo" and state.lower() == "up":
|
||||
iface_name = name
|
||||
break
|
||||
# 判断网络类型 (无线接口 -> WIFI, 否则 ETHERNET)
|
||||
net_type = self.TYPE_ETHERNET
|
||||
for iface in interfaces:
|
||||
name = iface.get("ifname") or iface.get("name", "")
|
||||
if name and name.startswith(("wl", "wlan", "wlp")):
|
||||
net_type = self.TYPE_WIFI
|
||||
iface_name = iface_name or name
|
||||
break
|
||||
data = {
|
||||
"connected": connected,
|
||||
"ip": ip,
|
||||
"interface": iface_name,
|
||||
"type_name": "WIFI" if net_type == self.TYPE_WIFI else "ETHERNET",
|
||||
}
|
||||
return NetworkInfo.from_linux_data(data, network_type=net_type)
|
||||
except Exception:
|
||||
return self._active_network_info
|
||||
|
||||
def getActiveNetworkInfo(self) -> Optional[NetworkInfo]:
|
||||
if _USE_LINUX:
|
||||
return self._get_linux_active_info()
|
||||
return self._active_network_info
|
||||
|
||||
def getNetworkInfo(self, network_type: int) -> Optional[NetworkInfo]:
|
||||
if _USE_LINUX:
|
||||
info = self._get_linux_active_info()
|
||||
if info is not None and info.getType() == network_type:
|
||||
return info
|
||||
# 请求的类型与活跃网络不同时, 返回断开状态
|
||||
return NetworkInfo(
|
||||
type_=network_type, state="DISCONNECTED",
|
||||
detailed_state="DISCONNECTED", is_available=False,
|
||||
)
|
||||
network = self._networks.get(network_type)
|
||||
if network:
|
||||
return NetworkInfo(type_=network_type, state="CONNECTED", is_available=True)
|
||||
return self._active_network_info
|
||||
|
||||
def getConnectivityStatus(self) -> bool:
|
||||
"""返回当前网络连通性 (True=已连接)"""
|
||||
if _USE_LINUX:
|
||||
return LinuxNetwork.get_connectivity()
|
||||
return True
|
||||
|
||||
def getNetworkInfoForUid(self, network: Network, uid: int) -> Optional[NetworkInfo]:
|
||||
return self._active_network_info
|
||||
|
||||
def getAllNetworks(self) -> List[Network]:
|
||||
if _USE_LINUX:
|
||||
networks: List[Network] = []
|
||||
try:
|
||||
interfaces = LinuxNetwork.get_interfaces()
|
||||
idx = 0
|
||||
for iface in interfaces:
|
||||
name = iface.get("ifname") or iface.get("name", "")
|
||||
if not name or name == "lo":
|
||||
continue
|
||||
idx += 1
|
||||
net = Network(idx)
|
||||
networks.append(net)
|
||||
# 若无真实接口, 回退到内部缓存
|
||||
if networks:
|
||||
return networks
|
||||
except Exception:
|
||||
pass
|
||||
return list(self._networks.values())
|
||||
|
||||
def getActiveNetwork(self) -> Optional[Network]:
|
||||
if _USE_LINUX:
|
||||
try:
|
||||
interfaces = LinuxNetwork.get_interfaces()
|
||||
for iface in interfaces:
|
||||
name = iface.get("ifname") or iface.get("name", "")
|
||||
state = iface.get("operstate", "")
|
||||
if name and name != "lo" and state.lower() == "up":
|
||||
net = Network(1)
|
||||
self._networks[1] = net
|
||||
return net
|
||||
# 回退: 若有外网连通性, 仍返回一个活跃 Network
|
||||
if LinuxNetwork.get_connectivity():
|
||||
net = Network(1)
|
||||
self._networks[1] = net
|
||||
return net
|
||||
except Exception:
|
||||
pass
|
||||
if self._networks:
|
||||
nid = next(iter(self._networks))
|
||||
return self._networks[nid]
|
||||
|
||||
@@ -14,6 +14,25 @@ import time
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from .._core import Stub, AndroidOnlyError
|
||||
from .net import ConnectivityManager as _BaseConnectivityManager
|
||||
from .net import DhcpInfo as _BaseDhcpInfo
|
||||
from .net import LinkAddress as _BaseLinkAddress
|
||||
from .net import LinkProperties as _BaseLinkProperties
|
||||
from .net import Network as _BaseNetwork
|
||||
from .net import NetworkCapabilities as _BaseNetworkCapabilities
|
||||
from .net import NetworkInfo as _BaseNetworkInfo
|
||||
from .net import NetworkRequest as _BaseNetworkRequest
|
||||
from .net import NetworkSpecifier as _BaseNetworkSpecifier
|
||||
from .net import ProxyInfo as _BaseProxyInfo
|
||||
from .net import TrafficStats as _BaseTrafficStats
|
||||
from .net import MatchAllNetworkSpecifier as _BaseMatchAllNetworkSpecifier
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
__all__ = [
|
||||
"Network", "NetworkCapabilities", "NetworkRequest", "NetworkSpecifier",
|
||||
@@ -30,7 +49,7 @@ __all__ = [
|
||||
# Network
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class Network(Stub):
|
||||
class Network(_BaseNetwork):
|
||||
"""Network — 网络"""
|
||||
|
||||
def __init__(self, netId: int = 0):
|
||||
@@ -74,7 +93,7 @@ class Network(Stub):
|
||||
# NetworkCapabilities
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class NetworkCapabilities(Stub):
|
||||
class NetworkCapabilities(_BaseNetworkCapabilities):
|
||||
"""NetworkCapabilities — 网络能力"""
|
||||
|
||||
TRANSPORT_CELLULAR = 0
|
||||
@@ -236,7 +255,7 @@ class NetworkCapabilities(Stub):
|
||||
# NetworkSpecifier / MatchAllNetworkSpecifier
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class NetworkSpecifier(Stub):
|
||||
class NetworkSpecifier(_BaseNetworkSpecifier):
|
||||
"""NetworkSpecifier — 网络指定器"""
|
||||
|
||||
def satisfiedBy(self, other: Optional["NetworkSpecifier"]) -> bool:
|
||||
@@ -252,7 +271,7 @@ class NetworkSpecifier(Stub):
|
||||
return id(self) & 0x7FFFFFFF
|
||||
|
||||
|
||||
class MatchAllNetworkSpecifier(NetworkSpecifier):
|
||||
class MatchAllNetworkSpecifier(_BaseMatchAllNetworkSpecifier, NetworkSpecifier):
|
||||
"""MatchAllNetworkSpecifier — 匹配所有网络"""
|
||||
|
||||
def satisfiedBy(self, other: Optional["NetworkSpecifier"]) -> bool:
|
||||
@@ -266,7 +285,7 @@ class MatchAllNetworkSpecifier(NetworkSpecifier):
|
||||
# NetworkRequest
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class NetworkRequest(Stub):
|
||||
class NetworkRequest(_BaseNetworkRequest):
|
||||
"""NetworkRequest — 网络请求"""
|
||||
|
||||
def __init__(self, capabilities: Optional[NetworkCapabilities] = None,
|
||||
@@ -430,7 +449,7 @@ class NetworkAgent(Stub):
|
||||
# NetworkInfo (扩展)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class NetworkInfo(Stub):
|
||||
class NetworkInfo(_BaseNetworkInfo):
|
||||
"""NetworkInfo — 网络信息 (完整版)"""
|
||||
|
||||
TYPE_NONE = -1
|
||||
@@ -558,7 +577,7 @@ class NetworkInfo(Stub):
|
||||
# LinkAddress
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class LinkAddress(Stub):
|
||||
class LinkAddress(_BaseLinkAddress):
|
||||
"""LinkAddress — 链路地址"""
|
||||
|
||||
def __init__(self, address: str = "0.0.0.0", prefixLength: int = 24,
|
||||
@@ -600,7 +619,7 @@ class LinkAddress(Stub):
|
||||
# LinkProperties
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class LinkProperties(Stub):
|
||||
class LinkProperties(_BaseLinkProperties):
|
||||
"""LinkProperties — 链路属性"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -874,7 +893,7 @@ class MobileLinkQualityInfo(LinkQualityInfo):
|
||||
# TrafficStats
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class TrafficStats(Stub):
|
||||
class TrafficStats(_BaseTrafficStats):
|
||||
"""TrafficStats — 流量统计"""
|
||||
|
||||
UID_REMOVED = -4
|
||||
@@ -969,7 +988,7 @@ class TrafficStats(Stub):
|
||||
# ProxyInfo
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ProxyInfo(Stub):
|
||||
class ProxyInfo(_BaseProxyInfo):
|
||||
"""ProxyInfo — 代理信息"""
|
||||
|
||||
def __init__(self, host: str = "", port: int = 0,
|
||||
@@ -1012,7 +1031,7 @@ class ProxyInfo(Stub):
|
||||
# DhcpInfo / DhcpInfoInternal
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class DhcpInfo(Stub):
|
||||
class DhcpInfo(_BaseDhcpInfo):
|
||||
"""DhcpInfo — DHCP 信息"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -1245,7 +1264,7 @@ class ConnectivityDiagnosticsManager(Stub):
|
||||
# ConnectivityManager (扩展)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ConnectivityManager(Stub):
|
||||
class ConnectivityManager(_BaseConnectivityManager):
|
||||
"""ConnectivityManager — 连接管理器 (扩展版)"""
|
||||
|
||||
TYPE_NONE = -1
|
||||
@@ -1291,17 +1310,64 @@ class ConnectivityManager(Stub):
|
||||
def getActiveNetwork(self) -> Optional[Network]:
|
||||
return self._active_network
|
||||
|
||||
def _get_linux_active_info(self):
|
||||
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
|
||||
try:
|
||||
from .net import NetworkInfo
|
||||
interfaces = LinuxNetwork.get_interfaces()
|
||||
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
|
||||
connected = any(
|
||||
(iface.get("ifname") or iface.get("name", "")) != "lo"
|
||||
and iface.get("operstate", "").lower() == "up"
|
||||
for iface in interfaces
|
||||
)
|
||||
if not connected:
|
||||
connected = LinuxNetwork.get_connectivity()
|
||||
ip = LinuxNetwork.get_ip_address()
|
||||
iface_name = ""
|
||||
net_type = ConnectivityManager.TYPE_ETHERNET
|
||||
for iface in interfaces:
|
||||
name = iface.get("ifname") or iface.get("name", "")
|
||||
state = iface.get("operstate", "")
|
||||
if name and name != "lo" and state.lower() == "up":
|
||||
iface_name = iface_name or name
|
||||
if name and name.startswith(("wl", "wlan", "wlp")):
|
||||
net_type = ConnectivityManager.TYPE_WIFI
|
||||
iface_name = name
|
||||
data = {
|
||||
"connected": connected,
|
||||
"ip": ip,
|
||||
"interface": iface_name,
|
||||
"type_name": "WIFI" if net_type == ConnectivityManager.TYPE_WIFI else "ETHERNET",
|
||||
}
|
||||
return NetworkInfo.from_linux_data(data, network_type=net_type)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def getActiveNetworkInfo(self) -> Optional[NetworkInfo]:
|
||||
if _USE_LINUX:
|
||||
return self._get_linux_active_info()
|
||||
if self._active_network:
|
||||
return self._network_infos.get(self._active_network.getNetId())
|
||||
return None
|
||||
|
||||
def getNetworkInfo(self, networkType: int) -> Optional[NetworkInfo]:
|
||||
if _USE_LINUX:
|
||||
info = self._get_linux_active_info()
|
||||
if info is not None and info.getType() == networkType:
|
||||
return info
|
||||
return None
|
||||
for info in self._network_infos.values():
|
||||
if info.getType() == networkType:
|
||||
return info
|
||||
return None
|
||||
|
||||
def getConnectivityStatus(self) -> bool:
|
||||
"""返回当前网络连通性 (True=已连接)"""
|
||||
if _USE_LINUX:
|
||||
return LinuxNetwork.get_connectivity()
|
||||
return self._active_network is not None
|
||||
|
||||
def getNetworkInfoForNetwork(self, network: Network) -> Optional[NetworkInfo]:
|
||||
return self._network_infos.get(network.getNetId())
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@ from .._core import (
|
||||
_sdcard_root, _app_data_dir, _package_name,
|
||||
)
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# SupplicantState
|
||||
@@ -620,6 +627,8 @@ class WifiManager(Stub):
|
||||
self._p2p_channels: List[int] = []
|
||||
|
||||
def getConnectionInfo(self) -> Optional[WifiInfo]:
|
||||
if _USE_LINUX:
|
||||
return self._get_linux_connection_info()
|
||||
if self._connection_info is None:
|
||||
info = WifiInfo()
|
||||
info._ssid = '"mock_wifi"'
|
||||
@@ -632,20 +641,127 @@ class WifiManager(Stub):
|
||||
self._connection_info = info
|
||||
return self._connection_info
|
||||
|
||||
def getWifiInfo(self) -> Optional[WifiInfo]:
|
||||
"""getWifiInfo — getConnectionInfo 的别名"""
|
||||
return self.getConnectionInfo()
|
||||
|
||||
def _get_linux_connection_info(self) -> WifiInfo:
|
||||
"""从 LinuxNetwork.get_wifi_info() 构造 WifiInfo"""
|
||||
info = WifiInfo()
|
||||
try:
|
||||
data = LinuxNetwork.get_wifi_info() or {}
|
||||
ssid = data.get("ssid", "")
|
||||
info._ssid = f'"{ssid}"' if ssid else "<unknown ssid>"
|
||||
info._bssid = data.get("bssid", "00:00:00:00:00:00")
|
||||
# signal 通常是 0-100, 转换为 RSSI (dBm, 约 -100..0)
|
||||
signal = data.get("signal", 0)
|
||||
if isinstance(signal, (int, float)) and signal > 0:
|
||||
info._rssi = int(-100 + (signal / 100.0) * 60)
|
||||
else:
|
||||
info._rssi = int(signal) if signal else WifiInfo.INVALID_RSSI
|
||||
iface = data.get("interface", "")
|
||||
if iface:
|
||||
info._frequency = 2412 # 默认 2.4GHz
|
||||
info._link_speed = data.get("link_speed", 0) or 0
|
||||
info._network_id = 0
|
||||
info._supplicant_state = SupplicantState(SupplicantState.COMPLETED)
|
||||
except Exception:
|
||||
pass
|
||||
return info
|
||||
|
||||
def getScanResults(self) -> List[ScanResult]:
|
||||
if _USE_LINUX:
|
||||
results = self._linux_scan_results()
|
||||
if results is not None:
|
||||
return results
|
||||
return list(self._scan_results)
|
||||
|
||||
def _linux_scan_results(self) -> Optional[List[ScanResult]]:
|
||||
"""通过 nmcli 获取 WiFi 扫描结果 (真实实现)"""
|
||||
import subprocess
|
||||
import re
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["nmcli", "-t", "-f", "ssid,bssid,signal,freq,security",
|
||||
"dev", "wifi", "list"],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return None
|
||||
results: List[ScanResult] = []
|
||||
seen: Set[str] = set()
|
||||
for line in proc.stdout.strip().split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
# nmcli -t 模式中值内的 : 被转义为 \:, 按非转义 : 分割
|
||||
parts = re.split(r'(?<!\\):', line)
|
||||
parts = [p.replace('\\:', ':') for p in parts]
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
ssid = parts[0]
|
||||
bssid = parts[1]
|
||||
if not bssid or bssid in seen:
|
||||
continue
|
||||
seen.add(bssid)
|
||||
sr = ScanResult()
|
||||
sr.SSID = ssid
|
||||
sr.BSSID = bssid
|
||||
try:
|
||||
sr.level = int(parts[2]) if parts[2] else 0
|
||||
except ValueError:
|
||||
sr.level = 0
|
||||
try:
|
||||
sr.frequency = int(float(parts[3])) if parts[3] else 0
|
||||
except ValueError:
|
||||
sr.frequency = 0
|
||||
sr.capabilities = parts[4] if len(parts) > 4 else ""
|
||||
results.append(sr)
|
||||
return results
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def startScan(self) -> bool:
|
||||
return True
|
||||
|
||||
def setWifiEnabled(self, enabled: bool) -> bool:
|
||||
if _USE_LINUX:
|
||||
import subprocess
|
||||
import shutil
|
||||
try:
|
||||
cmd = ["nmcli", "radio", "wifi", "on" if enabled else "off"]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||||
if proc.returncode != 0 and shutil.which("rfkill"):
|
||||
# 回退到 rfkill
|
||||
rfkill_cmd = (["rfkill", "unblock", "wifi"] if enabled
|
||||
else ["rfkill", "block", "wifi"])
|
||||
subprocess.run(rfkill_cmd, capture_output=True, timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
self._wifi_enabled = enabled
|
||||
self._wifi_state = self.WIFI_STATE_ENABLED if enabled else self.WIFI_STATE_DISABLED
|
||||
return True
|
||||
|
||||
def isWifiEnabled(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
return self._linux_has_wireless_interface()
|
||||
return self._wifi_enabled
|
||||
|
||||
def _linux_has_wireless_interface(self) -> bool:
|
||||
"""检查是否存在无线网络接口"""
|
||||
try:
|
||||
for iface in LinuxNetwork.get_interfaces():
|
||||
name = iface.get("ifname") or iface.get("name", "")
|
||||
if name and name.startswith(("wl", "wlan", "wlp")):
|
||||
return True
|
||||
# 回退: 检查 /sys/class/net 下的无线接口
|
||||
import os as _os
|
||||
for name in _os.listdir("/sys/class/net/") if _os.path.isdir("/sys/class/net") else []:
|
||||
if name.startswith(("wl", "wlan", "wlp")):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def getWifiState(self) -> int:
|
||||
return self._wifi_state
|
||||
|
||||
|
||||
+551
-40
@@ -20,6 +20,13 @@ from .._core import (
|
||||
HAS_PSUTIL,
|
||||
)
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxPower, LinuxProcess, HAS_PSUTIL as _HAS_PSUTIL
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Build
|
||||
@@ -108,16 +115,95 @@ class Build(Stub):
|
||||
|
||||
@staticmethod
|
||||
def getRadioVersion() -> str:
|
||||
return ""
|
||||
"""返回基带/无线电版本。桌面端无无线电硬件,返回空字符串。"""
|
||||
return Build.RADIO
|
||||
|
||||
@staticmethod
|
||||
def getSerial() -> str:
|
||||
return "unknown"
|
||||
"""返回设备序列号。尝试从 /sys 或 /proc 读取真实硬件序列号。"""
|
||||
# 尝试从 /sys/class/android_usb 读取
|
||||
try:
|
||||
for name in _os.listdir("/sys/class/android_usb"):
|
||||
serial_path = f"/sys/class/android_usb/{name}/iSerial"
|
||||
if _os.path.exists(serial_path):
|
||||
with open(serial_path, "r") as f:
|
||||
serial = f.read().strip()
|
||||
if serial:
|
||||
return serial
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
# 尝试从 /proc/cpuinfo 读取 Serial 字段
|
||||
try:
|
||||
with open("/proc/cpuinfo", "r") as f:
|
||||
for line in f:
|
||||
if line.lower().startswith("serial"):
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2:
|
||||
serial = parts[1].strip()
|
||||
if serial and serial != "0" * len(serial):
|
||||
return serial
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
# 尝试从 /sys/class/dmi/id/product_serial 读取
|
||||
try:
|
||||
path = "/sys/class/dmi/id/product_serial"
|
||||
if _os.path.exists(path):
|
||||
with open(path, "r") as f:
|
||||
serial = f.read().strip()
|
||||
if serial:
|
||||
return serial
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return Build.SERIAL
|
||||
|
||||
@staticmethod
|
||||
def getFingerprint() -> str:
|
||||
return Build.FINGERPRINT
|
||||
|
||||
@classmethod
|
||||
def _populate_from_linux(cls):
|
||||
"""从 LinuxProcess.get_system_info() 填充真实设备信息"""
|
||||
try:
|
||||
info = LinuxProcess.get_system_info() or {}
|
||||
import platform as _platform
|
||||
import socket as _socket
|
||||
hostname = info.get("hostname") or _socket.gethostname()
|
||||
system = info.get("platform") or _platform.system()
|
||||
arch = info.get("arch") or _platform.machine()
|
||||
kernel = info.get("kernel") or _platform.release()
|
||||
cls.MODEL = hostname
|
||||
cls.MANUFACTURER = "Linux"
|
||||
cls.BRAND = system
|
||||
cls.DEVICE = arch
|
||||
cls.BOARD = arch
|
||||
cls.HARDWARE = arch
|
||||
cls.HOST = hostname
|
||||
cls.PRODUCT = hostname
|
||||
cls.DISPLAY = kernel
|
||||
cls.ID = kernel
|
||||
# FINGERPRINT: brand/device/device:version/id/build:user/release-keys
|
||||
cls.FINGERPRINT = (
|
||||
f"{system}/{hostname}/{arch}:Linux/{kernel}/{hostname}:user/release-keys"
|
||||
)
|
||||
# CPU ABI 用架构映射
|
||||
_abi_map = {
|
||||
"x86_64": "x86_64", "amd64": "x86_64",
|
||||
"aarch64": "arm64-v8a", "arm64": "arm64-v8a",
|
||||
"x86": "x86", "i386": "x86", "i686": "x86",
|
||||
"armv7l": "armeabi-v7a", "arm": "armeabi-v7a",
|
||||
}
|
||||
cls.CPU_ABI = _abi_map.get(arch, arch)
|
||||
cls.SUPPORTED_ABIS = [cls.CPU_ABI]
|
||||
cls.SUPPORTED_64_BIT_ABIS = [cls.CPU_ABI] if "64" in cls.CPU_ABI else []
|
||||
cls.SUPPORTED_32_BIT_ABIS = [cls.CPU_ABI] if "64" not in cls.CPU_ABI else []
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# 在 Linux 上用真实系统信息填充 Build
|
||||
if _USE_LINUX:
|
||||
Build._populate_from_linux()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# SystemClock
|
||||
@@ -150,7 +236,17 @@ class SystemClock(Stub):
|
||||
|
||||
@staticmethod
|
||||
def setCurrentTimeMillis(millis: int) -> bool:
|
||||
return False
|
||||
"""尝试设置系统时间。需要 root 权限,通常在桌面端会失败。"""
|
||||
try:
|
||||
import subprocess
|
||||
seconds = millis / 1000.0
|
||||
# 尝试使用 date 命令设置时间
|
||||
result = subprocess.run(
|
||||
["date", "-s", f"@{int(seconds)}"],
|
||||
capture_output=True, timeout=5)
|
||||
return result.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -217,6 +313,20 @@ class Environment(Stub):
|
||||
|
||||
@staticmethod
|
||||
def isExternalStorageRemovable(path: Optional[str] = None) -> bool:
|
||||
"""判断外部存储是否可移除。通过检查 /proc/mounts 判断。"""
|
||||
try:
|
||||
check_path = path or _sdcard_root
|
||||
with open("/proc/mounts", "r") as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) >= 3 and check_path in parts[1]:
|
||||
# 检查挂载选项中是否包含 SD 卡相关设备
|
||||
device = parts[0]
|
||||
if "/sd" in device or "/mmc" in device or "/usb" in device:
|
||||
return True
|
||||
return False
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@@ -258,27 +368,42 @@ class Process(Stub):
|
||||
|
||||
@staticmethod
|
||||
def myUid() -> int:
|
||||
return 10001
|
||||
"""返回当前进程的真实 UID。"""
|
||||
return _os.getuid()
|
||||
|
||||
@staticmethod
|
||||
def myPid() -> int:
|
||||
return _os.getpid()
|
||||
|
||||
@staticmethod
|
||||
def killProcess(pid: int):
|
||||
pass
|
||||
def killProcess(pid):
|
||||
import signal
|
||||
try:
|
||||
_os.kill(pid, signal.SIGKILL)
|
||||
except (OSError, ProcessLookupError):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def sendSignal(pid: int, signal: int):
|
||||
pass
|
||||
def sendSignal(pid, signal_num):
|
||||
try:
|
||||
_os.kill(pid, signal_num)
|
||||
except (OSError, ProcessLookupError):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def setThreadPriority(priority: int):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def setThreadPriority(tid: int, priority: int):
|
||||
pass
|
||||
def setThreadPriority(*args):
|
||||
"""设置线程优先级。支持 setThreadPriority(priority) 和 setThreadPriority(tid, priority)。"""
|
||||
if len(args) == 1:
|
||||
tid = 0
|
||||
priority = args[0]
|
||||
elif len(args) == 2:
|
||||
tid, priority = args
|
||||
else:
|
||||
return
|
||||
try:
|
||||
_os.setpriority(_os.PRIO_PROCESS, tid, priority)
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def getElapsedCpuTime() -> int:
|
||||
@@ -945,7 +1070,20 @@ class Looper(Stub):
|
||||
return self._message_queue
|
||||
|
||||
def dump(self, pw: Any, prefix: str):
|
||||
pass
|
||||
"""将 DropBox 条目转储到给定的 writer。"""
|
||||
try:
|
||||
if hasattr(pw, "write"):
|
||||
pw.write(f"{prefix}DropBoxManager entries:\n")
|
||||
for entry in self._entries:
|
||||
pw.write(f"{prefix} [{entry.getTag()}] @ {entry.getTimeMillis()}: "
|
||||
f"{entry.getText(1024)}\n")
|
||||
elif hasattr(pw, "println"):
|
||||
pw.println(f"{prefix}DropBoxManager entries:")
|
||||
for entry in self._entries:
|
||||
pw.println(f"{prefix} [{entry.getTag()}] @ {entry.getTimeMillis()}: "
|
||||
f"{entry.getText(1024)}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def toString(self) -> str:
|
||||
return f"Looper({self._thread.name})"
|
||||
@@ -1299,9 +1437,34 @@ class PowerManager(Stub):
|
||||
return PowerManagerWakeLock(level_and_flags, tag)
|
||||
|
||||
def isInteractive(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
return self.isScreenOn()
|
||||
return self._is_interactive
|
||||
|
||||
def isScreenOn(self) -> bool:
|
||||
"""屏幕是否点亮 — 在 Linux 上检查 DISPLAY 环境变量"""
|
||||
if _USE_LINUX:
|
||||
import os as _os_mod
|
||||
display = _os_mod.environ.get("DISPLAY", "")
|
||||
if display:
|
||||
return True
|
||||
# WAYLAND_DISPLAY 作为回退
|
||||
wayland = _os_mod.environ.get("WAYLAND_DISPLAY", "")
|
||||
return bool(wayland)
|
||||
return self._is_interactive
|
||||
|
||||
def isPowerSaveMode(self) -> bool:
|
||||
"""检查是否处于省电模式。读取 /sys 中的电源相关状态。"""
|
||||
try:
|
||||
# 检查 CPU 调速器是否为 powersave
|
||||
gov_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"
|
||||
if _os.path.exists(gov_path):
|
||||
with open(gov_path, "r") as f:
|
||||
governor = f.read().strip()
|
||||
if governor == "powersave":
|
||||
return True
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return self._is_power_save
|
||||
|
||||
def isDeviceIdleMode(self) -> bool:
|
||||
@@ -1314,16 +1477,29 @@ class PowerManager(Stub):
|
||||
return False
|
||||
|
||||
def reboot(self, reason: Optional[str] = None):
|
||||
pass
|
||||
"""重启设备。桌面端尝试执行 reboot 命令(通常需要 root)。"""
|
||||
try:
|
||||
import subprocess
|
||||
cmd = ["reboot"]
|
||||
if reason:
|
||||
cmd.append(reason)
|
||||
subprocess.run(cmd, capture_output=True, timeout=5)
|
||||
except Exception:
|
||||
sys.stderr.write(f"[PowerManager] reboot: {reason or ''}\n")
|
||||
|
||||
def goToSleep(self, time_ms: int):
|
||||
pass
|
||||
"""使设备进入睡眠。桌面端无实际效果。"""
|
||||
self._is_interactive = False
|
||||
sys.stderr.write(f"[PowerManager] goToSleep at {time_ms}\n")
|
||||
|
||||
def wakeUp(self, time_ms: int):
|
||||
pass
|
||||
"""唤醒设备。桌面端无实际效果。"""
|
||||
self._is_interactive = True
|
||||
sys.stderr.write(f"[PowerManager] wakeUp at {time_ms}\n")
|
||||
|
||||
def userActivity(self, when: int, no_change_lights: bool = False):
|
||||
pass
|
||||
"""报告用户活动事件。桌面端无实际效果。"""
|
||||
self._is_interactive = True
|
||||
|
||||
|
||||
class PowerManagerWakeLock(Stub):
|
||||
@@ -1373,20 +1549,101 @@ class Vibrator(Stub):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._ff_device: Optional[str] = None
|
||||
self._vibrating = False
|
||||
|
||||
def vibrate(self, milliseconds: int):
|
||||
sys.stderr.write(f"[Vibrator] vibrate: {milliseconds}ms\n")
|
||||
@staticmethod
|
||||
def _find_ff_device() -> Optional[str]:
|
||||
"""查找 Linux 力反馈设备。"""
|
||||
try:
|
||||
for name in _os.listdir("/dev/input"):
|
||||
if name.startswith("event"):
|
||||
path = f"/dev/input/{name}"
|
||||
if _os.access(path, _os.W_OK):
|
||||
return path
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
# 检查 /sys/class/leds/vibrator
|
||||
try:
|
||||
if _os.path.exists("/sys/class/leds/vibrator"):
|
||||
return "/sys/class/leds/vibrator"
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def vibrate(self, pattern: List[int], repeat: int):
|
||||
sys.stderr.write(f"[Vibrator] vibrate pattern: {pattern}, repeat={repeat}\n")
|
||||
def vibrate(self, *args):
|
||||
"""振动。支持 vibrate(milliseconds) 和 vibrate(pattern, repeat)。"""
|
||||
if len(args) == 1 and isinstance(args[0], (int, float)):
|
||||
milliseconds = int(args[0])
|
||||
self._vibrating = True
|
||||
if self._ff_device is None:
|
||||
self._ff_device = self._find_ff_device()
|
||||
if self._ff_device and self._ff_device.startswith("/sys/class/leds"):
|
||||
try:
|
||||
with open(_os.path.join(self._ff_device, "brightness"), "w") as f:
|
||||
f.write("1")
|
||||
import threading
|
||||
def _stop():
|
||||
_time.sleep(milliseconds / 1000.0)
|
||||
try:
|
||||
with open(_os.path.join(self._ff_device, "brightness"), "w") as f:
|
||||
f.write("0")
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
self._vibrating = False
|
||||
threading.Thread(target=_stop, daemon=True).start()
|
||||
return
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
sys.stderr.write(f"[Vibrator] vibrate: {milliseconds}ms\n")
|
||||
elif len(args) == 2 and isinstance(args[0], list):
|
||||
pattern = args[0]
|
||||
repeat = args[1]
|
||||
if not pattern:
|
||||
return
|
||||
self._vibrating = True
|
||||
import threading
|
||||
def _run_pattern():
|
||||
idx = 0
|
||||
while True:
|
||||
duration = pattern[idx % len(pattern)]
|
||||
if idx % 2 == 0:
|
||||
_time.sleep(duration / 1000.0)
|
||||
else:
|
||||
self.vibrate(duration)
|
||||
_time.sleep(duration / 1000.0)
|
||||
idx += 1
|
||||
if repeat < 0 or idx >= len(pattern):
|
||||
if repeat < 0:
|
||||
continue
|
||||
break
|
||||
self._vibrating = False
|
||||
threading.Thread(target=_run_pattern, daemon=True).start()
|
||||
sys.stderr.write(f"[Vibrator] vibrate pattern: {pattern}, repeat={repeat}\n")
|
||||
|
||||
def cancel(self):
|
||||
"""取消振动。"""
|
||||
self._vibrating = False
|
||||
if self._ff_device and self._ff_device.startswith("/sys/class/leds"):
|
||||
try:
|
||||
with open(_os.path.join(self._ff_device, "brightness"), "w") as f:
|
||||
f.write("0")
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
sys.stderr.write("[Vibrator] cancel\n")
|
||||
|
||||
def hasVibrator(self) -> bool:
|
||||
return True
|
||||
"""检查是否有振动器硬件。"""
|
||||
return self._find_ff_device() is not None
|
||||
|
||||
def hasAmplitudeControl(self) -> bool:
|
||||
"""检查是否支持振幅控制。"""
|
||||
try:
|
||||
if _os.path.exists("/sys/class/leds/vibrator/max_brightness"):
|
||||
with open("/sys/class/leds/vibrator/max_brightness", "r") as f:
|
||||
return int(f.read().strip()) > 1
|
||||
except (OSError, IOError, ValueError):
|
||||
pass
|
||||
return False
|
||||
|
||||
def areEffectsSupported(self, effect_ids: List[int]) -> List[int]:
|
||||
@@ -1399,20 +1656,41 @@ class Vibrator(Stub):
|
||||
class VibratorEffect(Stub):
|
||||
"""VibratorEffect — 振动效果"""
|
||||
|
||||
EFFECT_CLICK = 0
|
||||
EFFECT_DOUBLE_CLICK = 1
|
||||
EFFECT_TICK = 2
|
||||
EFFECT_THUD = 3
|
||||
EFFECT_POP = 4
|
||||
EFFECT_HEAVY_CLICK = 5
|
||||
|
||||
def __init__(self, effect_id: int = 0):
|
||||
super().__init__()
|
||||
self._effect_id = effect_id
|
||||
self._timings: Optional[List[int]] = None
|
||||
self._amplitudes: Optional[List[int]] = None
|
||||
self._repeat = -1
|
||||
|
||||
@staticmethod
|
||||
def createOneShot(milliseconds: int, amplitude: int) -> "VibratorEffect":
|
||||
return VibratorEffect(0)
|
||||
"""创建单次振动效果。"""
|
||||
effect = VibratorEffect(0)
|
||||
effect._timings = [0, milliseconds]
|
||||
effect._amplitudes = [0, amplitude]
|
||||
effect._repeat = -1
|
||||
return effect
|
||||
|
||||
@staticmethod
|
||||
def createWaveform(pattern: List[int], repeat: int) -> "VibratorEffect":
|
||||
return VibratorEffect(0)
|
||||
"""创建波形振动效果。"""
|
||||
effect = VibratorEffect(0)
|
||||
effect._timings = list(pattern)
|
||||
effect._amplitudes = [255 if i % 2 == 1 else 0 for i in range(len(pattern))]
|
||||
effect._repeat = repeat
|
||||
return effect
|
||||
|
||||
@staticmethod
|
||||
def createPredefined(effect_id: int) -> "VibratorEffect":
|
||||
"""创建预定义振动效果。"""
|
||||
return VibratorEffect(effect_id)
|
||||
|
||||
|
||||
@@ -1478,8 +1756,58 @@ class BatteryManager(Stub):
|
||||
self._current_now = -500000
|
||||
self._current_avg = -450000
|
||||
self._energy_counter = 15000000
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
|
||||
_LINUX_STATUS_MAP = {
|
||||
"charging": 2, # BATTERY_STATUS_CHARGING
|
||||
"discharging": 3, # BATTERY_STATUS_DISCHARGING
|
||||
"not charging": 4, # BATTERY_STATUS_NOT_CHARGING
|
||||
"full": 5, # BATTERY_STATUS_FULL
|
||||
"unknown": 1, # BATTERY_STATUS_UNKNOWN
|
||||
}
|
||||
|
||||
def _get_linux_battery_info(self) -> dict:
|
||||
"""从 LinuxPower 获取真实电池信息"""
|
||||
try:
|
||||
return LinuxPower.get_battery_info() or {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _refresh_from_linux(self):
|
||||
"""用 Linux 真实电池信息刷新内部状态"""
|
||||
info = self._get_linux_battery_info()
|
||||
if not info:
|
||||
return
|
||||
self._present = bool(info.get("present", False))
|
||||
level = info.get("level", -1)
|
||||
if self._present and isinstance(level, (int, float)) and level >= 0:
|
||||
self._level = int(level)
|
||||
self._capacity = int(level)
|
||||
else:
|
||||
# 无电池或电量未知
|
||||
self._level = -1
|
||||
self._capacity = -1
|
||||
status_str = str(info.get("status", "unknown")).lower()
|
||||
self._status = self._LINUX_STATUS_MAP.get(status_str, self.BATTERY_STATUS_UNKNOWN)
|
||||
voltage = info.get("voltage", 0)
|
||||
if isinstance(voltage, (int, float)):
|
||||
# LinuxPower 返回伏特, Android 期望毫伏
|
||||
self._voltage = int(voltage * 1000)
|
||||
temperature = info.get("temperature", 0)
|
||||
if isinstance(temperature, (int, float)):
|
||||
# LinuxPower 返回摄氏度, Android 期望十分之一度
|
||||
self._temperature = int(temperature * 10)
|
||||
self._technology = info.get("technology", "") or "Li-ion"
|
||||
# 充电状态决定 plugged
|
||||
if self._status == self.BATTERY_STATUS_CHARGING:
|
||||
self._plugged = self.BATTERY_PLUGGED_AC
|
||||
else:
|
||||
self._plugged = 0
|
||||
|
||||
def getIntProperty(self, prop_id: int) -> int:
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
if prop_id == self.BATTERY_PROPERTY_CAPACITY:
|
||||
return self._capacity
|
||||
if prop_id == self.BATTERY_PROPERTY_STATUS:
|
||||
@@ -1498,9 +1826,57 @@ class BatteryManager(Stub):
|
||||
return self.getIntProperty(prop_id)
|
||||
|
||||
def isCharging(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._status == self.BATTERY_STATUS_CHARGING
|
||||
|
||||
def isPresent(self) -> bool:
|
||||
"""电池是否存在"""
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._present
|
||||
|
||||
def getLevel(self) -> int:
|
||||
"""电池电量 (0-100, -1 表示未知)"""
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._level
|
||||
|
||||
def getVoltage(self) -> int:
|
||||
"""电池电压 (毫伏)"""
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._voltage
|
||||
|
||||
def getTemperature(self) -> int:
|
||||
"""电池温度 (十分之一摄氏度)"""
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._temperature
|
||||
|
||||
def getTechnology(self) -> str:
|
||||
"""电池技术"""
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._technology
|
||||
|
||||
def getPlugType(self) -> int:
|
||||
"""充电类型"""
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._plugged
|
||||
|
||||
def getStatus(self) -> int:
|
||||
"""电池状态"""
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._status
|
||||
|
||||
def computeChargeTimeRemaining(self) -> int:
|
||||
if _USE_LINUX:
|
||||
# 无电池或已充满时返回 -1
|
||||
if not self._present or self._status == self.BATTERY_STATUS_FULL:
|
||||
return -1
|
||||
return 1800000 # 30 minutes
|
||||
|
||||
def toString(self) -> str:
|
||||
@@ -1582,18 +1958,38 @@ class TokenWatcher(Stub):
|
||||
|
||||
def __init__(self, handler: Handler, tag: str):
|
||||
super().__init__()
|
||||
self._handler = handler
|
||||
self._tag = tag
|
||||
self._tokens: Dict[int, str] = {}
|
||||
|
||||
def acquire(self, token: Any, tag: str):
|
||||
pass
|
||||
"""获取令牌。"""
|
||||
self._tokens[id(token)] = tag
|
||||
if len(self._tokens) == 1:
|
||||
self.acquired()
|
||||
|
||||
def release(self, token: Any):
|
||||
pass
|
||||
"""释放令牌。"""
|
||||
self._tokens.pop(id(token), None)
|
||||
if not self._tokens:
|
||||
self.released()
|
||||
|
||||
def isAcquired(self) -> bool:
|
||||
return False
|
||||
"""检查是否有令牌被持有。"""
|
||||
return len(self._tokens) > 0
|
||||
|
||||
def acquired(self):
|
||||
"""当第一个令牌被获取时调用。子类可重写。"""
|
||||
pass
|
||||
|
||||
def released(self):
|
||||
"""当所有令牌被释放时调用。子类可重写。"""
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
pass
|
||||
"""转储令牌信息。"""
|
||||
for tid, tag in self._tokens.items():
|
||||
sys.stderr.write(f"[TokenWatcher] {self._tag}: token={tid}, tag={tag}\n")
|
||||
|
||||
|
||||
class WorkSource(Stub):
|
||||
@@ -1744,42 +2140,157 @@ class FileObserver(Stub):
|
||||
super().__init__()
|
||||
self._path = path
|
||||
self._watching = False
|
||||
self._thread = None
|
||||
self._snapshot: Dict[str, float] = {}
|
||||
|
||||
def startWatching(self):
|
||||
"""开始监听文件变化。使用轮询方式检测文件系统事件。"""
|
||||
if self._watching:
|
||||
return
|
||||
self._watching = True
|
||||
self._snapshot = self._take_snapshot()
|
||||
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _take_snapshot(self) -> Dict[str, float]:
|
||||
"""获取当前目录的文件快照(文件名 -> 修改时间)。"""
|
||||
snapshot: Dict[str, float] = {}
|
||||
try:
|
||||
if _os.path.isdir(self._path):
|
||||
for name in _os.listdir(self._path):
|
||||
full = _os.path.join(self._path, name)
|
||||
try:
|
||||
snapshot[name] = _os.path.getmtime(full)
|
||||
except OSError:
|
||||
pass
|
||||
elif _os.path.exists(self._path):
|
||||
try:
|
||||
snapshot[self._path] = _os.path.getmtime(self._path)
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
return snapshot
|
||||
|
||||
def _poll_loop(self):
|
||||
"""轮询检测文件变化并触发事件。"""
|
||||
while self._watching:
|
||||
_time.sleep(0.5)
|
||||
try:
|
||||
current = self._take_snapshot()
|
||||
# 检测新建和修改
|
||||
for name, mtime in current.items():
|
||||
if name not in self._snapshot:
|
||||
self.onEvent(FileObserver.CREATE, name)
|
||||
elif mtime != self._snapshot[name]:
|
||||
self.onEvent(FileObserver.MODIFY, name)
|
||||
# 检测删除
|
||||
for name in list(self._snapshot.keys()):
|
||||
if name not in current:
|
||||
self.onEvent(FileObserver.DELETE, name)
|
||||
self._snapshot = current
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def stopWatching(self):
|
||||
"""停止监听。"""
|
||||
self._watching = False
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=1.0)
|
||||
self._thread = None
|
||||
|
||||
def onEvent(self, event: int, path: Optional[str]):
|
||||
"""文件事件回调,子类可重写。"""
|
||||
pass
|
||||
|
||||
def finalize(self):
|
||||
pass
|
||||
"""清理资源。"""
|
||||
self.stopWatching()
|
||||
|
||||
|
||||
class FileUtils(Stub):
|
||||
"""FileUtils — 文件工具"""
|
||||
|
||||
@staticmethod
|
||||
def readFileToString(file: Any) -> str:
|
||||
return ""
|
||||
def readFileToString(path):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def stringToFile(file: Any, string: str):
|
||||
pass
|
||||
def stringToFile(path, content):
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def copyFile(src: Any, dest: Any) -> bool:
|
||||
return True
|
||||
def copyFile(src, dst):
|
||||
try:
|
||||
import shutil
|
||||
shutil.copy2(src, dst)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def deleteDirectory(dir_: Any) -> bool:
|
||||
return True
|
||||
def deleteDirectory(path):
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(path)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def deleteContents(path):
|
||||
try:
|
||||
import shutil
|
||||
for entry in _os.listdir(path):
|
||||
full = _os.path.join(path, entry)
|
||||
if _os.path.isdir(full):
|
||||
shutil.rmtree(full)
|
||||
else:
|
||||
_os.remove(full)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def deleteOlderFiles(dir_: Any, min_count: int, min_age_ms: int):
|
||||
pass
|
||||
"""删除目录中较旧的文件,但保留至少 min_count 个文件。
|
||||
min_age_ms 为文件的最小年龄(毫秒),低于此年龄的文件不会被删除。"""
|
||||
try:
|
||||
if not _os.path.isdir(dir_):
|
||||
return
|
||||
entries = []
|
||||
for name in _os.listdir(dir_):
|
||||
full = _os.path.join(dir_, name)
|
||||
if _os.path.isfile(full):
|
||||
try:
|
||||
mtime = _os.path.getmtime(full)
|
||||
entries.append((full, mtime))
|
||||
except OSError:
|
||||
pass
|
||||
# 按修改时间排序(最新在前)
|
||||
entries.sort(key=lambda x: x[1], reverse=True)
|
||||
now = _time.time()
|
||||
min_age_s = min_age_ms / 1000.0
|
||||
# 保留最新的 min_count 个文件,删除超过 min_age 的旧文件
|
||||
for i, (path, mtime) in enumerate(entries):
|
||||
if i < min_count:
|
||||
continue
|
||||
if (now - mtime) >= min_age_s:
|
||||
try:
|
||||
_os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class PatternMatcher(Stub):
|
||||
|
||||
@@ -17,6 +17,15 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Callable
|
||||
from .._core import Stub, AndroidOnlyError, _app_data_dir
|
||||
|
||||
from ..java.lang import RuntimeException
|
||||
from .os import BatteryManager as _BaseBatteryManager
|
||||
from .os import PowerManager as _BasePowerManager
|
||||
|
||||
# Linux 真实实现导入
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxPower, LinuxProcess, HAS_PSUTIL as _HAS_PSUTIL
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
|
||||
|
||||
@@ -421,8 +430,8 @@ class BatteryStatsManager(Stub):
|
||||
# BatteryManager (扩展)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class BatteryManager(Stub):
|
||||
"""BatteryManager (扩展) — 电池管理器"""
|
||||
class BatteryManager(_BaseBatteryManager):
|
||||
"""BatteryManager (扩展) — 电池管理器 (继承 os.BatteryManager 的所有方法)"""
|
||||
|
||||
EXTRA_STATUS = "status"
|
||||
EXTRA_HEALTH = "health"
|
||||
@@ -505,6 +514,54 @@ class BatteryManager(Stub):
|
||||
self._discharge_time_remaining = -1
|
||||
self._is_capacity_estimated = False
|
||||
self._capacity_percentage = 100.0
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
|
||||
_LINUX_STATUS_MAP = {
|
||||
"charging": BATTERY_STATUS_CHARGING,
|
||||
"discharging": BATTERY_STATUS_DISCHARGING,
|
||||
"not charging": BATTERY_STATUS_NOT_CHARGING,
|
||||
"full": BATTERY_STATUS_FULL,
|
||||
"unknown": BATTERY_STATUS_UNKNOWN,
|
||||
}
|
||||
|
||||
def _get_linux_battery_info(self) -> dict:
|
||||
"""从 LinuxPower 获取真实电池信息"""
|
||||
try:
|
||||
return LinuxPower.get_battery_info() or {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _refresh_from_linux(self):
|
||||
"""用 Linux 真实电池信息刷新内部状态"""
|
||||
info = self._get_linux_battery_info()
|
||||
if not info:
|
||||
return
|
||||
self._present = bool(info.get("present", False))
|
||||
level = info.get("level", -1)
|
||||
if self._present and isinstance(level, (int, float)) and level >= 0:
|
||||
self._battery_level = int(level)
|
||||
self._capacity_percentage = float(level)
|
||||
# BATTERY_PROPERTY_CAPACITY 在 Android 中返回百分比 (0-100)
|
||||
self._capacity = int(level)
|
||||
else:
|
||||
# 无电池或电量未知
|
||||
self._battery_level = -1
|
||||
self._capacity_percentage = 0.0
|
||||
self._capacity = -1
|
||||
status_str = str(info.get("status", "unknown")).lower()
|
||||
self._status = self._LINUX_STATUS_MAP.get(status_str, BATTERY_STATUS_UNKNOWN)
|
||||
voltage = info.get("voltage", 0)
|
||||
if isinstance(voltage, (int, float)):
|
||||
self._voltage = int(voltage * 1000) # V -> mV
|
||||
temperature = info.get("temperature", 0)
|
||||
if isinstance(temperature, (int, float)):
|
||||
self._temperature = temperature # 摄氏度
|
||||
self._technology = info.get("technology", "") or "Li-ion"
|
||||
if self._status == BATTERY_STATUS_CHARGING:
|
||||
self._plugged = BATTERY_PLUGGED_AC
|
||||
else:
|
||||
self._plugged = 0
|
||||
|
||||
@staticmethod
|
||||
def getInstance(context: Any) -> "BatteryManager":
|
||||
@@ -513,6 +570,8 @@ class BatteryManager(Stub):
|
||||
return BatteryManager._instance
|
||||
|
||||
def getIntProperty(self, id: int) -> int:
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
properties = {
|
||||
BATTERY_PROPERTY_CAPACITY: self._capacity,
|
||||
BATTERY_PROPERTY_CHARGE_COUNTER: self._charge_counter,
|
||||
@@ -521,6 +580,9 @@ class BatteryManager(Stub):
|
||||
BATTERY_PROPERTY_ENERGY_COUNTER: self._energy_counter,
|
||||
BATTERY_PROPERTY_CHARGE_TIME_REMAINING: self._charge_time_remaining,
|
||||
BATTERY_PROPERTY_DISCHARGE_TIME_REMAINING: self._discharge_time_remaining,
|
||||
BATTERY_PROPERTY_STATUS: self._status,
|
||||
BATTERY_PROPERTY_TEMPERATURE: int(self._temperature),
|
||||
BATTERY_PROPERTY_VOLTAGE: self._voltage,
|
||||
}
|
||||
return properties.get(id, 0)
|
||||
|
||||
@@ -528,9 +590,13 @@ class BatteryManager(Stub):
|
||||
return self.getIntProperty(id)
|
||||
|
||||
def isCharging(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._status == BATTERY_STATUS_CHARGING or self._status == BATTERY_STATUS_FULL
|
||||
|
||||
def getBatteryLevel(self) -> int:
|
||||
if _USE_LINUX:
|
||||
self._refresh_from_linux()
|
||||
return self._battery_level
|
||||
|
||||
def getInt(intent: Any, key: str, defaultValue: int) -> int:
|
||||
@@ -617,8 +683,8 @@ class BatterySaverController(Stub):
|
||||
# PowerManager (扩展)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class PowerManager(Stub):
|
||||
"""PowerManager — 电源管理器 (扩展)"""
|
||||
class PowerManager(_BasePowerManager):
|
||||
"""PowerManager — 电源管理器 (扩展,继承 os.PowerManager 的所有方法)"""
|
||||
|
||||
PARTIAL_WAKE_LOCK = PARTIAL_WAKE_LOCK
|
||||
FULL_WAKE_LOCK = FULL_WAKE_LOCK
|
||||
@@ -687,9 +753,11 @@ class PowerManager(Stub):
|
||||
self._shutdown_requested = False
|
||||
self._thermal_status = THERMAL_STATUS_NONE
|
||||
self._power_save_state = PowerSaveState()
|
||||
self._current_wake_up_reason = WAKE_REASON_UNKNOWN
|
||||
self._current_wake_up_reason = PowerManager.WAKE_REASON_UNKNOWN
|
||||
|
||||
def isInteractive(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
return self.isScreenOn()
|
||||
return self._is_interactive
|
||||
|
||||
def isPowerSaveMode(self) -> bool:
|
||||
@@ -705,6 +773,13 @@ class PowerManager(Stub):
|
||||
return True
|
||||
|
||||
def isScreenOn(self) -> bool:
|
||||
if _USE_LINUX:
|
||||
import os as _os_mod
|
||||
display = _os_mod.environ.get("DISPLAY", "")
|
||||
if display:
|
||||
return True
|
||||
wayland = _os_mod.environ.get("WAYLAND_DISPLAY", "")
|
||||
return bool(wayland)
|
||||
return self._is_screen_on
|
||||
|
||||
def isWakeLockLevelSupported(self, level: int) -> bool:
|
||||
|
||||
@@ -108,6 +108,25 @@ class _SettingsBase(Stub):
|
||||
data = _SettingsBase._load(name)
|
||||
return data.get(key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def putBoolean(name: str, key: str, value: bool) -> bool:
|
||||
data = _SettingsBase._load(name)
|
||||
data[key] = bool(value)
|
||||
_SettingsBase._save(name)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def getBoolean(name: str, key: str, default_value: bool = False) -> bool:
|
||||
data = _SettingsBase._load(name)
|
||||
val = data.get(key, default_value)
|
||||
if isinstance(val, bool):
|
||||
return val
|
||||
if isinstance(val, str):
|
||||
return val.lower() in ("true", "1", "yes")
|
||||
if isinstance(val, (int, float)):
|
||||
return bool(val)
|
||||
return default_value
|
||||
|
||||
|
||||
class SettingsSystem(_SettingsBase):
|
||||
_NAME = "settings_system"
|
||||
@@ -144,6 +163,14 @@ class SettingsSystem(_SettingsBase):
|
||||
def getLong(key: str, default_value: int = 0) -> int:
|
||||
return _SettingsBase.getLong(SettingsSystem._NAME, key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def putBoolean(key: str, value: bool) -> bool:
|
||||
return _SettingsBase.putBoolean(SettingsSystem._NAME, key, value)
|
||||
|
||||
@staticmethod
|
||||
def getBoolean(key: str, default_value: bool = False) -> bool:
|
||||
return _SettingsBase.getBoolean(SettingsSystem._NAME, key, default_value)
|
||||
|
||||
|
||||
class SettingsSecure(_SettingsBase):
|
||||
_NAME = "settings_secure"
|
||||
@@ -180,6 +207,14 @@ class SettingsSecure(_SettingsBase):
|
||||
def getLong(key: str, default_value: int = 0) -> int:
|
||||
return _SettingsBase.getLong(SettingsSecure._NAME, key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def putBoolean(key: str, value: bool) -> bool:
|
||||
return _SettingsBase.putBoolean(SettingsSecure._NAME, key, value)
|
||||
|
||||
@staticmethod
|
||||
def getBoolean(key: str, default_value: bool = False) -> bool:
|
||||
return _SettingsBase.getBoolean(SettingsSecure._NAME, key, default_value)
|
||||
|
||||
|
||||
class SettingsGlobal(_SettingsBase):
|
||||
_NAME = "settings_global"
|
||||
@@ -216,6 +251,14 @@ class SettingsGlobal(_SettingsBase):
|
||||
def getLong(key: str, default_value: int = 0) -> int:
|
||||
return _SettingsBase.getLong(SettingsGlobal._NAME, key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def putBoolean(key: str, value: bool) -> bool:
|
||||
return _SettingsBase.putBoolean(SettingsGlobal._NAME, key, value)
|
||||
|
||||
@staticmethod
|
||||
def getBoolean(key: str, default_value: bool = False) -> bool:
|
||||
return _SettingsBase.getBoolean(SettingsGlobal._NAME, key, default_value)
|
||||
|
||||
|
||||
class Settings(Stub):
|
||||
"""Settings — 系统设置"""
|
||||
@@ -236,6 +279,10 @@ class Settings(Stub):
|
||||
class MediaStore(Stub):
|
||||
"""MediaStore — 媒体存储"""
|
||||
|
||||
VOLUME_INTERNAL = "internal"
|
||||
VOLUME_EXTERNAL = "external"
|
||||
VOLUME_EXTERNAL_PRIMARY = "external_primary"
|
||||
|
||||
class MediaColumns(Stub):
|
||||
_ID = 0
|
||||
DATA = 1
|
||||
@@ -259,16 +306,35 @@ class MediaStore(Stub):
|
||||
EXTERNAL_CONTENT_URI = "content://media/external/images/media"
|
||||
INTERNAL_CONTENT_URI = "content://media/internal/images/media"
|
||||
|
||||
@staticmethod
|
||||
def getContentUri(volume_name: str) -> str:
|
||||
return f"content://media/{volume_name}/images/media"
|
||||
|
||||
class Thumbnails(Stub):
|
||||
EXTERNAL_CONTENT_URI = "content://media/external/images/thumbnails"
|
||||
|
||||
class Video(Stub):
|
||||
class Media(Stub):
|
||||
EXTERNAL_CONTENT_URI = "content://media/external/video/media"
|
||||
INTERNAL_CONTENT_URI = "content://media/internal/video/media"
|
||||
|
||||
@staticmethod
|
||||
def getContentUri(volume_name: str) -> str:
|
||||
return f"content://media/{volume_name}/video/media"
|
||||
|
||||
class Audio(Stub):
|
||||
class Media(Stub):
|
||||
EXTERNAL_CONTENT_URI = "content://media/external/audio/media"
|
||||
INTERNAL_CONTENT_URI = "content://media/internal/audio/media"
|
||||
|
||||
@staticmethod
|
||||
def getContentUri(volume_name: str) -> str:
|
||||
return f"content://media/{volume_name}/audio/media"
|
||||
|
||||
@staticmethod
|
||||
def getMediaUri(volume_name: str) -> str:
|
||||
"""返回指定卷的媒体 URI。"""
|
||||
return f"content://media/{volume_name}"
|
||||
|
||||
@staticmethod
|
||||
def getVersion(context: Any, uri: str) -> str:
|
||||
@@ -337,22 +403,94 @@ class DocumentsContract(Stub):
|
||||
class ContactsContract(Stub):
|
||||
"""ContactsContract — 联系人合约"""
|
||||
|
||||
AUTHORITY = "com.android.contacts"
|
||||
|
||||
class Contacts(Stub):
|
||||
CONTENT_URI = "content://com.android.contacts/contacts"
|
||||
CONTENT_FILTER_URI = "content://com.android.contacts/contacts/filter"
|
||||
CONTENT_TYPE = "vnd.android.cursor.dir/contact"
|
||||
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact"
|
||||
DISPLAY_NAME = "display_name"
|
||||
PHOTO_URI = "photo_uri"
|
||||
PHOTO_ID = "photo_id"
|
||||
_ID = "_id"
|
||||
HAS_PHONE_NUMBER = "has_phone_number"
|
||||
LOOKUP_KEY = "lookup"
|
||||
STARRED = "starred"
|
||||
|
||||
class CommonDataKinds(Stub):
|
||||
class Email(Stub):
|
||||
CONTENT_URI = "content://com.android.contacts/data/emails"
|
||||
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/email_v2"
|
||||
ADDRESS = "data1"
|
||||
DISPLAY_NAME = "display_name"
|
||||
TYPE = "data2"
|
||||
TYPE_HOME = 1
|
||||
TYPE_WORK = 2
|
||||
TYPE_OTHER = 3
|
||||
TYPE_MOBILE = 4
|
||||
|
||||
class Phone(Stub):
|
||||
CONTENT_URI = "content://com.android.contacts/data/phones"
|
||||
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone_v2"
|
||||
NUMBER = "data1"
|
||||
DISPLAY_NAME = "display_name"
|
||||
TYPE = "data2"
|
||||
TYPE_HOME = 1
|
||||
TYPE_MOBILE = 2
|
||||
TYPE_WORK = 3
|
||||
TYPE_FAX_WORK = 4
|
||||
TYPE_FAX_HOME = 5
|
||||
TYPE_PAGER = 6
|
||||
TYPE_OTHER = 7
|
||||
TYPE_CALLBACK = 8
|
||||
TYPE_CAR = 9
|
||||
TYPE_COMPANY_MAIN = 10
|
||||
TYPE_ISDN = 11
|
||||
TYPE_MAIN = 12
|
||||
TYPE_OTHER_FAX = 13
|
||||
TYPE_RADIO = 14
|
||||
TYPE_TELEX = 15
|
||||
TYPE_TTY_TDD = 16
|
||||
TYPE_WORK_MOBILE = 17
|
||||
TYPE_WORK_PAGER = 18
|
||||
TYPE_ASSISTANT = 19
|
||||
|
||||
class StructuredName(Stub):
|
||||
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/name"
|
||||
DISPLAY_NAME = "data1"
|
||||
GIVEN_NAME = "data2"
|
||||
FAMILY_NAME = "data3"
|
||||
PREFIX = "data4"
|
||||
MIDDLE_NAME = "data5"
|
||||
SUFFIX = "data6"
|
||||
|
||||
class Organization(Stub):
|
||||
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/organization"
|
||||
COMPANY = "data1"
|
||||
TYPE = "data2"
|
||||
TITLE = "data4"
|
||||
TYPE_WORK = 1
|
||||
TYPE_OTHER = 2
|
||||
|
||||
class Data(Stub):
|
||||
CONTENT_URI = "content://com.android.contacts/data"
|
||||
CONTENT_TYPE = "vnd.android.cursor.dir/data"
|
||||
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/data"
|
||||
MIMETYPE = "mimetype"
|
||||
_ID = "_id"
|
||||
DISPLAY_NAME = "display_name"
|
||||
RAW_CONTACT_ID = "raw_contact_id"
|
||||
CONTACT_ID = "contact_id"
|
||||
|
||||
class RawContacts(Stub):
|
||||
CONTENT_URI = "content://com.android.contacts/raw_contacts"
|
||||
CONTENT_TYPE = "vnd.android.cursor.dir/raw_contact"
|
||||
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/raw_contact"
|
||||
_ID = "_id"
|
||||
CONTACT_ID = "contact_id"
|
||||
ACCOUNT_NAME = "account_name"
|
||||
ACCOUNT_TYPE = "account_type"
|
||||
|
||||
|
||||
class ContactsContractContacts(ContactsContract.Contacts):
|
||||
|
||||
@@ -80,7 +80,7 @@ class OsConstants(Stub):
|
||||
SIGTTIN = 21
|
||||
SIGTTOU = 22
|
||||
|
||||
# errno 常量
|
||||
# errno 常量 (使用 getattr 处理 Windows 上不存在的常量)
|
||||
EPERM = errno.EPERM
|
||||
ENOENT = errno.ENOENT
|
||||
ESRCH = errno.ESRCH
|
||||
@@ -95,7 +95,7 @@ class OsConstants(Stub):
|
||||
ENOMEM = errno.ENOMEM
|
||||
EACCES = errno.EACCES
|
||||
EFAULT = errno.EFAULT
|
||||
ENOTBLK = errno.ENOTBLK
|
||||
ENOTBLK = getattr(errno, 'ENOTBLK', 15) # Windows 不支持
|
||||
EBUSY = errno.EBUSY
|
||||
EEXIST = errno.EEXIST
|
||||
EXDEV = errno.EXDEV
|
||||
@@ -106,7 +106,7 @@ class OsConstants(Stub):
|
||||
ENFILE = errno.ENFILE
|
||||
EMFILE = errno.EMFILE
|
||||
ENOTTY = errno.ENOTTY
|
||||
ETXTBSY = errno.ETXTBSY
|
||||
ETXTBSY = getattr(errno, 'ETXTBSY', 139)
|
||||
EFBIG = errno.EFBIG
|
||||
ENOSPC = errno.ENOSPC
|
||||
ESPIPE = errno.ESPIPE
|
||||
@@ -123,16 +123,16 @@ class OsConstants(Stub):
|
||||
ELOOP = errno.ELOOP
|
||||
ENOMSG = errno.ENOMSG
|
||||
EIDRM = errno.EIDRM
|
||||
ENOSTR = errno.ENOSTR
|
||||
ENODATA = errno.ENODATA
|
||||
ETIME = errno.ETIME
|
||||
ENOSR = errno.ENOSR
|
||||
EREMOTE = errno.EREMOTE
|
||||
ENOLINK = errno.ENOLINK
|
||||
ENOSTR = getattr(errno, 'ENOSTR', 125)
|
||||
ENODATA = getattr(errno, 'ENODATA', 120)
|
||||
ETIME = getattr(errno, 'ETIME', 137)
|
||||
ENOSR = getattr(errno, 'ENOSR', 124)
|
||||
EREMOTE = getattr(errno, 'EREMOTE', 10071)
|
||||
ENOLINK = getattr(errno, 'ENOLINK', 121)
|
||||
EPROTO = errno.EPROTO
|
||||
EMULTIHOP = errno.EMULTIHOP
|
||||
EBADMSG = errno.EBADMSG
|
||||
EOVERFLOW = errno.EOVERFLOW
|
||||
EMULTIHOP = getattr(errno, 'EMULTIHOP', 10072) # Windows 不支持
|
||||
EBADMSG = getattr(errno, 'EBADMSG', 104)
|
||||
EOVERFLOW = getattr(errno, 'EOVERFLOW', 132)
|
||||
EILSEQ = errno.EILSEQ
|
||||
ENOTSOCK = errno.ENOTSOCK
|
||||
EDESTADDRREQ = errno.EDESTADDRREQ
|
||||
@@ -140,10 +140,10 @@ class OsConstants(Stub):
|
||||
EPROTOTYPE = errno.EPROTOTYPE
|
||||
ENOPROTOOPT = errno.ENOPROTOOPT
|
||||
EPROTONOSUPPORT = errno.EPROTONOSUPPORT
|
||||
ESOCKTNOSUPPORT = errno.ESOCKTNOSUPPORT
|
||||
EOPNOTSUPP = errno.EOPNOTSUPP
|
||||
EPFNOSUPPORT = errno.EPFNOSUPPORT
|
||||
EAFNOSUPPORT = errno.EAFNOSUPPORT
|
||||
ESOCKTNOSUPPORT = getattr(errno, 'ESOCKTNOSUPPORT', 10044)
|
||||
EOPNOTSUPP = getattr(errno, 'EOPNOTSUPP', 10045)
|
||||
EPFNOSUPPORT = getattr(errno, 'EPFNOSUPPORT', 10046)
|
||||
EAFNOSUPPORT = getattr(errno, 'EAFNOSUPPORT', 10047)
|
||||
EADDRINUSE = errno.EADDRINUSE
|
||||
EADDRNOTAVAIL = errno.EADDRNOTAVAIL
|
||||
ENETDOWN = errno.ENETDOWN
|
||||
@@ -154,32 +154,32 @@ class OsConstants(Stub):
|
||||
ENOBUFS = errno.ENOBUFS
|
||||
EISCONN = errno.EISCONN
|
||||
ENOTCONN = errno.ENOTCONN
|
||||
ESHUTDOWN = errno.ESHUTDOWN
|
||||
ETOOMANYREFS = errno.ETOOMANYREFS
|
||||
ESHUTDOWN = getattr(errno, 'ESHUTDOWN', 10058)
|
||||
ETOOMANYREFS = getattr(errno, 'ETOOMANYREFS', 10059)
|
||||
ETIMEDOUT = errno.ETIMEDOUT
|
||||
ECONNREFUSED = errno.ECONNREFUSED
|
||||
EHOSTDOWN = errno.EHOSTDOWN
|
||||
EHOSTDOWN = getattr(errno, 'EHOSTDOWN', 10064)
|
||||
EHOSTUNREACH = errno.EHOSTUNREACH
|
||||
EALREADY = errno.EALREADY
|
||||
EINPROGRESS = errno.EINPROGRESS
|
||||
ESTALE = errno.ESTALE
|
||||
EUCLEAN = errno.EUCLEAN
|
||||
ENOTNAM = errno.ENOTNAM
|
||||
ENAVAIL = errno.ENAVAIL
|
||||
EISNAM = errno.EISNAM
|
||||
EREMOTEIO = errno.EREMOTEIO
|
||||
EDQUOT = errno.EDQUOT
|
||||
ENOMEDIUM = errno.ENOMEDIUM
|
||||
EMEDIUMTYPE = errno.EMEDIUMTYPE
|
||||
ECANCELED = errno.ECANCELED
|
||||
ENOKEY = errno.ENOKEY
|
||||
EKEYEXPIRED = errno.EKEYEXPIRED
|
||||
EKEYREVOKED = errno.EKEYREVOKED
|
||||
EKEYREJECTED = errno.EKEYREJECTED
|
||||
EOWNERDEAD = errno.EOWNERDEAD
|
||||
ENOTRECOVERABLE = errno.ENOTRECOVERABLE
|
||||
ERFKILL = errno.ERFKILL
|
||||
EHWPOISON = getattr(errno, 'EHWPOISON', 1001)
|
||||
ESTALE = getattr(errno, 'ESTALE', 10070)
|
||||
EUCLEAN = getattr(errno, 'EUCLEAN', 117) # Windows 不支持
|
||||
ENOTNAM = getattr(errno, 'ENOTNAM', 118) # Windows 不支持
|
||||
ENAVAIL = getattr(errno, 'ENAVAIL', 119) # Windows 不支持
|
||||
EISNAM = getattr(errno, 'EISNAM', 120) # Windows 不支持
|
||||
EREMOTEIO = getattr(errno, 'EREMOTEIO', 121) # Windows 不支持
|
||||
EDQUOT = getattr(errno, 'EDQUOT', 10069)
|
||||
ENOMEDIUM = getattr(errno, 'ENOMEDIUM', 123) # Windows 不支持
|
||||
EMEDIUMTYPE = getattr(errno, 'EMEDIUMTYPE', 124) # Windows 不支持
|
||||
ECANCELED = getattr(errno, 'ECANCELED', 105)
|
||||
ENOKEY = getattr(errno, 'ENOKEY', 126) # Windows 不支持
|
||||
EKEYEXPIRED = getattr(errno, 'EKEYEXPIRED', 127) # Windows 不支持
|
||||
EKEYREVOKED = getattr(errno, 'EKEYREVOKED', 128) # Windows 不支持
|
||||
EKEYREJECTED = getattr(errno, 'EKEYREJECTED', 129) # Windows 不支持
|
||||
EOWNERDEAD = getattr(errno, 'EOWNERDEAD', 133)
|
||||
ENOTRECOVERABLE = getattr(errno, 'ENOTRECOVERABLE', 127)
|
||||
ERFKILL = getattr(errno, 'ERFKILL', 132) # Windows 不支持
|
||||
EHWPOISON = getattr(errno, 'EHWPOISON', 1001) # Windows 不支持
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@@ -14,6 +14,12 @@ from .._core import (
|
||||
_sdcard_root, _app_data_dir, _package_name,
|
||||
)
|
||||
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxNetwork, LinuxProcess, LinuxLocation
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# CellIdentity 基类集合
|
||||
@@ -553,6 +559,9 @@ class TelephonyManager(Stub):
|
||||
self._context = context
|
||||
|
||||
def getNetworkOperatorName(self) -> str:
|
||||
if _USE_LINUX:
|
||||
loc = LinuxLocation.get_location()
|
||||
return loc.get("isp", "") or ""
|
||||
return "MockOperator"
|
||||
|
||||
def getNetworkOperator(self) -> str:
|
||||
@@ -562,9 +571,15 @@ class TelephonyManager(Stub):
|
||||
return "us"
|
||||
|
||||
def getNetworkType(self) -> int:
|
||||
if _USE_LINUX:
|
||||
if LinuxNetwork.get_connectivity():
|
||||
return self.NETWORK_TYPE_IWLAN
|
||||
return self.NETWORK_TYPE_UNKNOWN
|
||||
return self.NETWORK_TYPE_LTE
|
||||
|
||||
def getPhoneType(self) -> int:
|
||||
if _USE_LINUX:
|
||||
return self.PHONE_TYPE_NONE
|
||||
return self.PHONE_TYPE_GSM
|
||||
|
||||
def getDataNetworkType(self) -> int:
|
||||
@@ -574,6 +589,8 @@ class TelephonyManager(Stub):
|
||||
return self.NETWORK_TYPE_LTE
|
||||
|
||||
def getSimOperator(self) -> str:
|
||||
if _USE_LINUX:
|
||||
return ""
|
||||
return "310260"
|
||||
|
||||
def getSimOperatorName(self) -> str:
|
||||
@@ -583,18 +600,29 @@ class TelephonyManager(Stub):
|
||||
return "us"
|
||||
|
||||
def getSimSerialNumber(self) -> Optional[str]:
|
||||
if _USE_LINUX:
|
||||
return ""
|
||||
return "89014103211118510720"
|
||||
|
||||
def getSubscriberId(self) -> Optional[str]:
|
||||
if _USE_LINUX:
|
||||
return ""
|
||||
return "310260123456789"
|
||||
|
||||
def getDeviceId(self) -> Optional[str]:
|
||||
if _USE_LINUX:
|
||||
return LinuxNetwork.get_mac_address() or None
|
||||
return "000000000000000"
|
||||
|
||||
def getDeviceSoftwareVersion(self) -> Optional[str]:
|
||||
if _USE_LINUX:
|
||||
info = LinuxProcess.get_system_info()
|
||||
return info.get("kernel", "")
|
||||
return "00.00.00.00"
|
||||
|
||||
def getLine1Number(self) -> Optional[str]:
|
||||
if _USE_LINUX:
|
||||
return ""
|
||||
return "15551234567"
|
||||
|
||||
def getGroupIdLevel1(self) -> Optional[str]:
|
||||
@@ -619,6 +647,9 @@ class TelephonyManager(Stub):
|
||||
return self.SIM_STATE_READY
|
||||
|
||||
def getCellLocation(self) -> Any:
|
||||
# 桌面无基站,Linux 下返回 None
|
||||
if _USE_LINUX:
|
||||
return None
|
||||
return None
|
||||
|
||||
def getAllCellInfo(self) -> Optional[List[CellInfo]]:
|
||||
@@ -630,8 +661,10 @@ class TelephonyManager(Stub):
|
||||
def getMmsUAProfUrl(self) -> str:
|
||||
return "http://www.example.com/uaprof.xml"
|
||||
|
||||
def listen(self, listener: PhoneStateListener, events: int):
|
||||
pass
|
||||
def listen(self, listener, events):
|
||||
if not hasattr(self, '_listeners'):
|
||||
self._listeners = []
|
||||
self._listeners.append((listener, events))
|
||||
|
||||
def getDataState(self) -> int:
|
||||
return self.DATA_CONNECTED
|
||||
|
||||
@@ -7,6 +7,7 @@ import html as _html
|
||||
import re as _re
|
||||
import sys
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
|
||||
from urllib.parse import quote_plus as _quote_plus, unquote_plus as _unquote_plus
|
||||
|
||||
from .._core import (
|
||||
Stub, AndroidOnlyError, android_context,
|
||||
@@ -176,7 +177,9 @@ class TextUtils(Stub):
|
||||
|
||||
@staticmethod
|
||||
def split(text: str, delimiter: str) -> List[str]:
|
||||
return text.split(delimiter)
|
||||
if text is None or len(text) == 0:
|
||||
return []
|
||||
return _re.split(delimiter, text)
|
||||
|
||||
@staticmethod
|
||||
def htmlEncode(s: str) -> str:
|
||||
@@ -186,6 +189,48 @@ class TextUtils(Stub):
|
||||
def htmlDecode(s: str) -> str:
|
||||
return _html.unescape(s)
|
||||
|
||||
@staticmethod
|
||||
def concat(*texts: Any) -> str:
|
||||
parts = []
|
||||
for t in texts:
|
||||
if t is not None:
|
||||
parts.append(str(t))
|
||||
return "".join(parts)
|
||||
|
||||
@staticmethod
|
||||
def indexOf(s: Any, needle: Any, start: int = 0) -> int:
|
||||
if s is None:
|
||||
return -1
|
||||
s = str(s)
|
||||
needle = str(needle)
|
||||
if start < 0:
|
||||
start = 0
|
||||
return s.find(needle, start)
|
||||
|
||||
@staticmethod
|
||||
def lastIndexOf(s: Any, needle: Any, last: Optional[int] = None) -> int:
|
||||
if s is None:
|
||||
return -1
|
||||
s = str(s)
|
||||
needle = str(needle)
|
||||
if last is None:
|
||||
return s.rfind(needle)
|
||||
if last < 0:
|
||||
return -1
|
||||
return s.rfind(needle, 0, last + 1)
|
||||
|
||||
@staticmethod
|
||||
def urlEncode(s: str) -> str:
|
||||
if s is None:
|
||||
return ""
|
||||
return _quote_plus(str(s))
|
||||
|
||||
@staticmethod
|
||||
def urlDecode(s: str) -> str:
|
||||
if s is None:
|
||||
return ""
|
||||
return _unquote_plus(str(s))
|
||||
|
||||
@staticmethod
|
||||
def getReverse(s: str, start: int, end: int) -> str:
|
||||
return s[start:end][::-1]
|
||||
@@ -227,8 +272,13 @@ class TextUtils(Stub):
|
||||
return text[:max_chars] + ellipsis
|
||||
|
||||
@staticmethod
|
||||
def isDigitsOnly(s: str) -> bool:
|
||||
return s.isdigit() if s else False
|
||||
def isDigitsOnly(s: Any) -> bool:
|
||||
if s is None:
|
||||
return False
|
||||
s = str(s)
|
||||
if len(s) == 0:
|
||||
return False
|
||||
return s.isdigit()
|
||||
|
||||
@staticmethod
|
||||
def isGraphical(s: str) -> bool:
|
||||
@@ -293,12 +343,12 @@ class Spanned(Stub):
|
||||
def __init__(self, source: str = ""):
|
||||
super().__init__()
|
||||
self._source = source
|
||||
self._spans: Dict[Tuple[type, int], Tuple[int, int, Any]] = {} # (type, id) -> (start, end, span_obj)
|
||||
self._spans: Dict[Tuple[type, int], Tuple[int, int, int, Any]] = {} # (type, id) -> (start, end, flags, span_obj)
|
||||
|
||||
def setSpan(self, span_obj: Any, start: int, end: int, flags: int):
|
||||
span_id = id(span_obj)
|
||||
span_type = type(span_obj)
|
||||
self._spans[(span_type, span_id)] = (start, end, span_obj)
|
||||
self._spans[(span_type, span_id)] = (start, end, flags, span_obj)
|
||||
|
||||
def removeSpan(self, span_obj: Any):
|
||||
span_id = id(span_obj)
|
||||
@@ -306,12 +356,13 @@ class Spanned(Stub):
|
||||
self._spans.pop((span_type, span_id), None)
|
||||
|
||||
def getSpans(self, start: int, end: int, span_type: Optional[type] = None) -> List[Any]:
|
||||
result = []
|
||||
for (st, sid), (s, e, obj) in self._spans.items():
|
||||
collected = []
|
||||
for (st, sid), (s, e, flags, obj) in self._spans.items():
|
||||
if span_type is None or st == span_type:
|
||||
if s < end and e > start:
|
||||
result.append(obj)
|
||||
return result
|
||||
collected.append((s, obj))
|
||||
collected.sort(key=lambda t: t[0])
|
||||
return [obj for _, obj in collected]
|
||||
|
||||
def getSpanStart(self, span_obj: Any) -> int:
|
||||
span_id = id(span_obj)
|
||||
@@ -329,10 +380,18 @@ class Spanned(Stub):
|
||||
span_id = id(span_obj)
|
||||
span_type = type(span_obj)
|
||||
data = self._spans.get((span_type, span_id))
|
||||
return 0 if data is None else 0
|
||||
return data[2] if data else 0
|
||||
|
||||
def nextSpanTransition(self, start: int, end: int, span_type: Optional[type] = None) -> int:
|
||||
return end
|
||||
result = end
|
||||
for (st, sid), (s, e, flags, obj) in self._spans.items():
|
||||
if span_type is not None and st != span_type:
|
||||
continue
|
||||
if start < s < result:
|
||||
result = s
|
||||
if start < e < result:
|
||||
result = e
|
||||
return result
|
||||
|
||||
def toString(self) -> str:
|
||||
return self._source
|
||||
@@ -435,15 +494,17 @@ class SpannableStringBuilder(Editable):
|
||||
else:
|
||||
super().__init__(str(source))
|
||||
|
||||
def append(self, text: Any) -> "SpannableStringBuilder":
|
||||
super().append(text)
|
||||
return self
|
||||
|
||||
def append(self, text: Any, start: int, end: int) -> "SpannableStringBuilder":
|
||||
if isinstance(text, str):
|
||||
self._source += text[start:end]
|
||||
def append(self, text: Any, start: Optional[int] = None,
|
||||
end: Optional[int] = None) -> "SpannableStringBuilder":
|
||||
if text is None:
|
||||
return self
|
||||
if start is None and end is None:
|
||||
self._source += str(text)
|
||||
else:
|
||||
self._source += str(text)[start:end]
|
||||
s = 0 if start is None else start
|
||||
text_str = str(text)
|
||||
e = len(text_str) if end is None else end
|
||||
self._source += text_str[s:e]
|
||||
return self
|
||||
|
||||
def insert(self, where: int, text: Any) -> "SpannableStringBuilder":
|
||||
|
||||
@@ -5,6 +5,7 @@ Android Util 组件 — Log, SparseArray, LruCache, DisplayMetrics 等
|
||||
|
||||
import math
|
||||
import re as _re
|
||||
import struct
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union
|
||||
@@ -51,19 +52,44 @@ class Log(Stub):
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def w(tag: str, msg: str) -> int:
|
||||
def w(tag: str, msg: str = None, tr: Exception = None) -> int:
|
||||
if Log._min_log_level <= Log.WARN:
|
||||
# 支持 w(tag, Throwable) 调用形式
|
||||
if msg is None and tr is not None:
|
||||
msg = Log.getStackTraceString(tr)
|
||||
elif tr is not None:
|
||||
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
|
||||
sys.stderr.write(f"W/{tag}: {msg}\n")
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def e(tag: str, msg: str) -> int:
|
||||
def e(tag: str, msg: str = None, tr: Exception = None) -> int:
|
||||
if Log._min_log_level <= Log.ERROR:
|
||||
# 支持 e(tag, Throwable) 调用形式
|
||||
if msg is None and tr is not None:
|
||||
msg = Log.getStackTraceString(tr)
|
||||
elif tr is not None:
|
||||
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
|
||||
sys.stderr.write(f"E/{tag}: {msg}\n")
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def wtf(tag: str, msg: str) -> int:
|
||||
def wtf(tag: str, msg=None, tr: Exception = None) -> int:
|
||||
"""What a Terrible Failure。支持以下调用形式:
|
||||
- wtf(tag, msg)
|
||||
- wtf(tag, Throwable)
|
||||
- wtf(tag, msg, Throwable)
|
||||
"""
|
||||
# 如果 msg 是异常对象,则当作 tr 处理
|
||||
if isinstance(msg, BaseException):
|
||||
tr = msg
|
||||
msg = None
|
||||
if msg is None and tr is not None:
|
||||
msg = Log.getStackTraceString(tr)
|
||||
elif tr is not None:
|
||||
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
|
||||
if msg is None:
|
||||
msg = ""
|
||||
sys.stderr.write(f"WTF/{tag}: {msg}\n")
|
||||
return 0
|
||||
|
||||
@@ -88,17 +114,16 @@ class Log(Stub):
|
||||
@staticmethod
|
||||
def getStackTraceString(tr: Exception) -> str:
|
||||
import traceback
|
||||
if tr is None:
|
||||
return ""
|
||||
if isinstance(tr, str):
|
||||
return tr
|
||||
return "".join(traceback.format_exception(type(tr), tr, tr.__traceback__))
|
||||
|
||||
@staticmethod
|
||||
def setMinLogLevel(level: int):
|
||||
Log._min_log_level = level
|
||||
|
||||
@staticmethod
|
||||
def wtf(tag: str, tr: Exception) -> int:
|
||||
sys.stderr.write(f"WTF/{tag}: {Log.getStackTraceString(tr)}\n")
|
||||
return 0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# SparseArray
|
||||
@@ -518,6 +543,23 @@ class TypedValue(Stub):
|
||||
COMPLEX_UNIT_FRACTION = 6
|
||||
COMPLEX_UNIT_FRACTION_PARENT = 7
|
||||
|
||||
COMPLEX_UNIT_SHIFT = 0
|
||||
COMPLEX_UNIT_MASK = 0xF
|
||||
|
||||
COMPLEX_RADIX_SHIFT = 4
|
||||
COMPLEX_RADIX_MASK = 0x3
|
||||
|
||||
COMPLEX_MANTISSA_SHIFT = 8
|
||||
COMPLEX_MANTISSA_MASK = 0x00ffffff
|
||||
|
||||
# 复数值的基数乘数(与 AOSP TypedValue.RADIX_MULTS 一致)
|
||||
_RADIX_MULTS = (
|
||||
0.00390625, # 1/256, radix 0
|
||||
3.0517578e-5, # 1/32768, radix 1
|
||||
1.1920929e-7, # 1/8388608, radix 2
|
||||
4.6566129e-10, # 1/2147483648, radix 3
|
||||
)
|
||||
|
||||
TYPE_NULL = 0x00
|
||||
TYPE_REFERENCE = 0x01
|
||||
TYPE_ATTRIBUTE = 0x02
|
||||
@@ -536,6 +578,9 @@ class TypedValue(Stub):
|
||||
TYPE_LAST_COLOR_INT = 0x1F
|
||||
TYPE_LAST_INT = 0x1F
|
||||
|
||||
DATA_NULL_UNDEFINED = 0
|
||||
DATA_NULL_EMPTY = 1
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.type = self.TYPE_NULL
|
||||
@@ -564,18 +609,27 @@ class TypedValue(Stub):
|
||||
|
||||
@staticmethod
|
||||
def complexToDimension(data: int, metrics: DisplayMetrics) -> float:
|
||||
unit = data & 0xF
|
||||
value = (data >> 4) & 0xFFFFFF
|
||||
# Sign-extend
|
||||
if value & 0x800000:
|
||||
value = value - 0x1000000
|
||||
return TypedValue.applyDimension(unit, float(value), metrics)
|
||||
"""将复数维度值转换为像素值。
|
||||
复数值格式:bits 0-3=unit, bits 4-5=radix, bits 8-31=mantissa(有符号24位)"""
|
||||
unit = (data >> TypedValue.COMPLEX_UNIT_SHIFT) & TypedValue.COMPLEX_UNIT_MASK
|
||||
radix = (data >> TypedValue.COMPLEX_RADIX_SHIFT) & TypedValue.COMPLEX_RADIX_MASK
|
||||
mantissa = (data >> TypedValue.COMPLEX_MANTISSA_SHIFT) & TypedValue.COMPLEX_MANTISSA_MASK
|
||||
# 24 位有符号数符号扩展
|
||||
if mantissa & 0x800000:
|
||||
mantissa = mantissa - 0x1000000
|
||||
value = mantissa * TypedValue._RADIX_MULTS[radix]
|
||||
return TypedValue.applyDimension(unit, value, metrics)
|
||||
|
||||
@staticmethod
|
||||
def complexToFloat(data: int) -> float:
|
||||
value = (data & 0xFFFFFF00) >> 8
|
||||
exp = (data & 0xFF) - 150
|
||||
return float(value) * (10.0 ** exp)
|
||||
"""将复数值转换为浮点数(与 AOSP complexToFloat 一致)。"""
|
||||
radix = (data >> TypedValue.COMPLEX_RADIX_SHIFT) & TypedValue.COMPLEX_RADIX_MASK
|
||||
# mantissa 保持在原始位位置(左移 8 位),与 AOSP 一致
|
||||
mantissa = data & (TypedValue.COMPLEX_MANTISSA_MASK << TypedValue.COMPLEX_MANTISSA_SHIFT)
|
||||
# 处理符号(Java int 是有符号的,bit 31 为符号位)
|
||||
if mantissa & 0x80000000:
|
||||
mantissa = mantissa - 0x100000000
|
||||
return float(mantissa) * TypedValue._RADIX_MULTS[radix]
|
||||
|
||||
@staticmethod
|
||||
def complexToDimensionPixelOffset(data: int, metrics: DisplayMetrics) -> int:
|
||||
@@ -589,7 +643,9 @@ class TypedValue(Stub):
|
||||
return int(val - 0.5)
|
||||
|
||||
def getFloat(self) -> float:
|
||||
return float(self.data)
|
||||
"""将 data 字段按 IEEE 754 单精度浮点数解读(与 Float.intBitsToFloat 一致)。"""
|
||||
bits = self.data & 0xFFFFFFFF
|
||||
return struct.unpack('f', struct.pack('I', bits))[0]
|
||||
|
||||
def toString(self) -> str:
|
||||
return f"TypedValue(type={self.type}, data={self.data})"
|
||||
|
||||
+250
-11
@@ -10,6 +10,12 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from .._core import Stub, AndroidOnlyError, android_context, HAS_PYAUTOGUI
|
||||
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxDisplay, LinuxUIAutomation, HAS_XDOTOOL
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Gravity
|
||||
@@ -363,6 +369,12 @@ class View(Stub):
|
||||
def performClick(self) -> bool:
|
||||
return self.callOnClick()
|
||||
|
||||
def screenshot(self, path=None):
|
||||
"""截屏"""
|
||||
if _USE_LINUX:
|
||||
return LinuxDisplay.screenshot(path)
|
||||
return None
|
||||
|
||||
def performLongClick(self) -> bool:
|
||||
if self._on_long_click_listener:
|
||||
self._on_long_click_listener(self)
|
||||
@@ -926,6 +938,106 @@ class View(Stub):
|
||||
def onApplyWindowInsets(self, insets):
|
||||
return insets
|
||||
|
||||
def setOnCreateContextMenuListener(self, listener):
|
||||
self._on_create_context_menu_listener = listener
|
||||
return self
|
||||
|
||||
def getBaseline(self):
|
||||
return -1
|
||||
|
||||
def setFocusableInTouchMode(self, focusable):
|
||||
self._focusable_in_touch_mode = focusable
|
||||
|
||||
def isFocusableInTouchMode(self):
|
||||
return getattr(self, '_focusable_in_touch_mode', False)
|
||||
|
||||
def setRotationX(self, rotationX):
|
||||
self._rotation_x = rotationX
|
||||
|
||||
def getRotationX(self):
|
||||
return getattr(self, '_rotation_x', 0.0)
|
||||
|
||||
def setRotationY(self, rotationY):
|
||||
self._rotation_y = rotationY
|
||||
|
||||
def getRotationY(self):
|
||||
return getattr(self, '_rotation_y', 0.0)
|
||||
|
||||
def setCameraDistance(self, distance):
|
||||
self._camera_distance = distance
|
||||
|
||||
def getCameraDistance(self):
|
||||
return getattr(self, '_camera_distance', 0.0)
|
||||
|
||||
def setForeground(self, foreground):
|
||||
self._foreground = foreground
|
||||
|
||||
def getForeground(self):
|
||||
return getattr(self, '_foreground', None)
|
||||
|
||||
def setBackgroundTintList(self, tint):
|
||||
self._background_tint = tint
|
||||
|
||||
def setBackgroundTintMode(self, tintMode):
|
||||
self._background_tint_mode = tintMode
|
||||
|
||||
def setScrollbarFadingEnabled(self, fadeScrollbars):
|
||||
self._fade_scrollbars = fadeScrollbars
|
||||
|
||||
def isScrollbarFadingEnabled(self):
|
||||
return getattr(self, '_fade_scrollbars', True)
|
||||
|
||||
def setVerticalScrollBarEnabled(self, enabled):
|
||||
self._vertical_scrollbar = enabled
|
||||
|
||||
def isVerticalScrollBarEnabled(self):
|
||||
return getattr(self, '_vertical_scrollbar', True)
|
||||
|
||||
def setHorizontalScrollBarEnabled(self, enabled):
|
||||
self._horizontal_scrollbar = enabled
|
||||
|
||||
def isHorizontalScrollBarEnabled(self):
|
||||
return getattr(self, '_horizontal_scrollbar', True)
|
||||
|
||||
def setLabelFor(self, id):
|
||||
self._label_for = id
|
||||
|
||||
def getLabelFor(self):
|
||||
return getattr(self, '_label_for', 0)
|
||||
|
||||
def cancelLongPress(self):
|
||||
pass
|
||||
|
||||
def setPaddingRelative(self, start, top, end, bottom):
|
||||
self._padding_start = start
|
||||
self._padding_top = top
|
||||
self._padding_end = end
|
||||
self._padding_bottom = bottom
|
||||
|
||||
def setSoundEffectsEnabled(self, soundEffectsEnabled):
|
||||
self._sound_effects = soundEffectsEnabled
|
||||
|
||||
def isSoundEffectsEnabled(self):
|
||||
return getattr(self, '_sound_effects', True)
|
||||
|
||||
def setHapticFeedbackEnabled(self, hapticFeedbackEnabled):
|
||||
self._haptic_feedback = hapticFeedbackEnabled
|
||||
|
||||
def isHapticFeedbackEnabled(self):
|
||||
return getattr(self, '_haptic_feedback', True)
|
||||
|
||||
def refreshDrawableState(self):
|
||||
pass
|
||||
|
||||
def getDrawableState(self):
|
||||
return []
|
||||
|
||||
def requestRectangleOnScreen(self, rectangle):
|
||||
return False
|
||||
|
||||
def setAllCaps(self, allCaps):
|
||||
self._all_caps = allCaps
|
||||
|
||||
|
||||
class MeasureSpec:
|
||||
"""MeasureSpec — 测量规格包装"""
|
||||
@@ -1130,8 +1242,13 @@ class ViewGroup(View, ViewParent, ViewManager):
|
||||
|
||||
# ─── child management ──────────────────────────────────────
|
||||
|
||||
def addView(self, child: View, index: int = -1,
|
||||
def addView(self, child: View, index: Optional[int] = -1,
|
||||
params: Optional["ViewGroup.LayoutParams"] = None):
|
||||
"""addView — 支持 addView(child), addView(child, params), addView(child, index), addView(child, index, params)"""
|
||||
# 智能 参数分派:第二个参数可能是 index(int) 或 params(LayoutParams)
|
||||
if isinstance(index, ViewGroup.LayoutParams):
|
||||
params = index
|
||||
index = -1
|
||||
if params is None:
|
||||
params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
@@ -1143,9 +1260,6 @@ class ViewGroup(View, ViewParent, ViewManager):
|
||||
child.setLayoutParams(params)
|
||||
child.onAttachedToWindow()
|
||||
|
||||
def addView(self, child: View, params: Optional["ViewGroup.LayoutParams"] = None):
|
||||
self.addView(child, -1, params)
|
||||
|
||||
def removeView(self, view: View):
|
||||
if view in self._children:
|
||||
self._children.remove(view)
|
||||
@@ -1248,6 +1362,32 @@ class ViewGroup(View, ViewParent, ViewManager):
|
||||
def isMotionEventSplittingEnabled(self) -> bool:
|
||||
return True
|
||||
|
||||
# ─── layout transition ────────────────────────────────────
|
||||
|
||||
def setLayoutTransition(self, transition: Any):
|
||||
self._layout_transition = transition
|
||||
|
||||
def getLayoutTransition(self) -> Any:
|
||||
return getattr(self, '_layout_transition', None)
|
||||
|
||||
# ─── focus / touch blocking ───────────────────────────────
|
||||
|
||||
def setTouchscreenBlocksFocus(self, blocks: bool):
|
||||
self._touchscreen_blocks_focus = blocks
|
||||
|
||||
def isTouchscreenBlocksFocus(self) -> bool:
|
||||
return getattr(self, '_touchscreen_blocks_focus', False)
|
||||
|
||||
# ─── child visible rect ───────────────────────────────────
|
||||
|
||||
def getChildVisibleRect(self, child: View, r: Any, offset: Any) -> bool:
|
||||
if r is not None and hasattr(r, 'set'):
|
||||
r.set(child.getLeft(), child.getTop(),
|
||||
child.getRight(), child.getBottom())
|
||||
if offset is not None and hasattr(offset, 'set'):
|
||||
offset.set(0, 0)
|
||||
return True
|
||||
|
||||
def setTransitionGroup(self, is_transition_group: bool):
|
||||
pass
|
||||
|
||||
@@ -1697,6 +1837,75 @@ class MotionEvent(Stub):
|
||||
def getHistoricalSize(self, pos: int, pointer: int = 0) -> float:
|
||||
return 0.5
|
||||
|
||||
# ─── historical coordinates ───────────────────────────────
|
||||
|
||||
def getHistorySize(self) -> int:
|
||||
return len(getattr(self, '_history', []))
|
||||
|
||||
def getHistoricalX(self, pointer_index: int = 0, pos: int = 0) -> float:
|
||||
history = getattr(self, '_history', [])
|
||||
if 0 <= pos < len(history):
|
||||
return history[pos].get('x', self.getX(pointer_index))
|
||||
return self.getX(pointer_index)
|
||||
|
||||
def getHistoricalY(self, pointer_index: int = 0, pos: int = 0) -> float:
|
||||
history = getattr(self, '_history', [])
|
||||
if 0 <= pos < len(history):
|
||||
return history[pos].get('y', self.getY(pointer_index))
|
||||
return self.getY(pointer_index)
|
||||
|
||||
def getHistoricalPressure(self, pointer_index: int = 0, pos: int = 0) -> float:
|
||||
history = getattr(self, '_history', [])
|
||||
if 0 <= pos < len(history):
|
||||
return history[pos].get('pressure', 1.0)
|
||||
return 1.0
|
||||
|
||||
def getHistoricalEventTime(self, pos: int = 0) -> int:
|
||||
history = getattr(self, '_history', [])
|
||||
if 0 <= pos < len(history):
|
||||
return history[pos].get('event_time', self._event_time)
|
||||
return self._event_time
|
||||
|
||||
# ─── edge flags ───────────────────────────────────────────
|
||||
|
||||
def getEdgeFlags(self) -> int:
|
||||
return getattr(self, '_edge_flags', 0)
|
||||
|
||||
def setEdgeFlags(self, edge_flags: int):
|
||||
self._edge_flags = edge_flags
|
||||
|
||||
# ─── action mutation ──────────────────────────────────────
|
||||
|
||||
def setAction(self, action: int):
|
||||
self._action = action
|
||||
|
||||
# ─── batch ────────────────────────────────────────────────
|
||||
|
||||
def addBatch(self, event_time: int, x: float, y: float,
|
||||
pressure: float = 1.0, size: float = 0.5,
|
||||
meta_state: int = 0):
|
||||
if not hasattr(self, '_history'):
|
||||
self._history = []
|
||||
self._history.append({
|
||||
'event_time': self._event_time,
|
||||
'x': self._x,
|
||||
'y': self._y,
|
||||
'pressure': self.getPressure(0),
|
||||
'size': self.getSize(0),
|
||||
'meta_state': self._meta_state,
|
||||
})
|
||||
self._x = x
|
||||
self._y = y
|
||||
self._raw_x = x
|
||||
self._raw_y = y
|
||||
self._event_time = event_time
|
||||
self._meta_state = meta_state
|
||||
if self._pointers:
|
||||
self._pointers[0]['x'] = x
|
||||
self._pointers[0]['y'] = y
|
||||
self._pointers[0]['pressure'] = pressure
|
||||
self._pointers[0]['size'] = size
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# KeyEvent
|
||||
@@ -1981,6 +2190,28 @@ class KeyEvent(Stub):
|
||||
meta_state=event._meta_state,
|
||||
)
|
||||
|
||||
def getCharacters(self):
|
||||
return getattr(self, '_characters', None)
|
||||
|
||||
def isSystem(self):
|
||||
return False
|
||||
|
||||
def isPrintingKey(self):
|
||||
return False
|
||||
|
||||
def isModifierKey(self, keyCode):
|
||||
return keyCode in (KEYCODE_SHIFT_LEFT, KEYCODE_SHIFT_RIGHT, KEYCODE_ALT_LEFT, KEYCODE_ALT_RIGHT, KEYCODE_CTRL_LEFT, KEYCODE_CTRL_RIGHT, KEYCODE_META_LEFT, KEYCODE_META_RIGHT)
|
||||
|
||||
def startTracking(self):
|
||||
self._tracking = True
|
||||
|
||||
def isTracking(self):
|
||||
return getattr(self, '_tracking', False)
|
||||
|
||||
@staticmethod
|
||||
def keyCodeToString(keyCode):
|
||||
return f"KEYCODE_{keyCode}"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# GestureDetector
|
||||
@@ -2688,14 +2919,18 @@ class SurfaceHolder(Stub):
|
||||
def setFormat(self, format: int):
|
||||
self._format = format
|
||||
|
||||
def lockCanvas(self) -> Any:
|
||||
return None
|
||||
def lockCanvas(self, *args):
|
||||
"""锁定画布并返回 Canvas 对象"""
|
||||
if hasattr(self, '_surface') and self._surface:
|
||||
from .graphics import Canvas, Bitmap
|
||||
if hasattr(self._surface, '_bitmap') and self._surface._bitmap:
|
||||
return Canvas(self._surface._bitmap)
|
||||
from .graphics import Canvas, Bitmap
|
||||
return Canvas(Bitmap(100, 100))
|
||||
|
||||
def lockCanvas(self, dirty: Any) -> Any:
|
||||
return None
|
||||
|
||||
def unlockCanvasAndPost(self, canvas: Any):
|
||||
pass
|
||||
def unlockCanvasAndPost(self, canvas):
|
||||
"""解锁画布并提交"""
|
||||
pass # 桌面环境无需实际提交
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -3260,9 +3495,13 @@ class Display(Stub):
|
||||
return self._display_id
|
||||
|
||||
def getWidth(self) -> int:
|
||||
if _USE_LINUX:
|
||||
return LinuxDisplay.get_screen_size()[0]
|
||||
return self._width
|
||||
|
||||
def getHeight(self) -> int:
|
||||
if _USE_LINUX:
|
||||
return LinuxDisplay.get_screen_size()[1]
|
||||
return self._height
|
||||
|
||||
def getRotation(self) -> int:
|
||||
|
||||
@@ -13,14 +13,33 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from .._core import Stub, AndroidOnlyError
|
||||
from ..java.lang import RuntimeException
|
||||
from .view import KeyEvent as _BaseKeyEvent
|
||||
from .view import ViewGroup as _BaseViewGroup
|
||||
from .view import View as _BaseView
|
||||
from .view import ViewOutlineProvider as _BaseViewOutlineProvider
|
||||
from .view import ViewPropertyAnimator as _BaseViewPropertyAnimator
|
||||
from .view import ViewTreeObserver as _BaseViewTreeObserver
|
||||
from .view import ViewConfiguration as _BaseViewConfiguration
|
||||
from .view import ViewParent as _BaseViewParent
|
||||
from .view import WindowInsets as _BaseWindowInsets
|
||||
from .view import Display as _BaseDisplay
|
||||
from .view import DragEvent as _BaseDragEvent
|
||||
from .view import GestureDetector as _BaseGestureDetector
|
||||
from .view import ScaleGestureDetector as _BaseScaleGestureDetector
|
||||
from .view import SoundEffectConstants as _BaseSoundEffectConstants
|
||||
from .view import Surface as _BaseSurface
|
||||
from .view import SurfaceView as _BaseSurfaceView
|
||||
from .view import TextureView as _BaseTextureView
|
||||
from .view import VelocityTracker as _BaseVelocityTracker
|
||||
from .view import WindowInsetsController as _BaseWindowInsetsController
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# ViewOutlineProvider
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ViewOutlineProvider(Stub):
|
||||
"""ViewOutlineProvider — 视图轮廓提供者"""
|
||||
class ViewOutlineProvider(_BaseViewOutlineProvider):
|
||||
"""ViewOutlineProvider — 视图轮廓提供者 (继承 view.ViewOutlineProvider 的所有方法)"""
|
||||
|
||||
BACKGROUND = None
|
||||
BOUNDS = None
|
||||
@@ -99,8 +118,8 @@ class Outline(Stub):
|
||||
# ViewPropertyAnimator
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ViewPropertyAnimator(Stub):
|
||||
"""ViewPropertyAnimator — 视图属性动画器"""
|
||||
class ViewPropertyAnimator(_BaseViewPropertyAnimator):
|
||||
"""ViewPropertyAnimator — 视图属性动画器 (继承 view.ViewPropertyAnimator 的所有方法)"""
|
||||
|
||||
def __init__(self, view: Any = None):
|
||||
super().__init__()
|
||||
@@ -381,8 +400,8 @@ class ViewStructure(Stub):
|
||||
# ViewTreeObserver
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ViewTreeObserver(Stub):
|
||||
"""ViewTreeObserver — 视图树观察者"""
|
||||
class ViewTreeObserver(_BaseViewTreeObserver):
|
||||
"""ViewTreeObserver — 视图树观察者 (继承 view.ViewTreeObserver 的所有方法)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -529,8 +548,8 @@ class ViewDebug(Stub):
|
||||
# ViewConfiguration
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ViewConfiguration(Stub):
|
||||
"""ViewConfiguration — 视图配置"""
|
||||
class ViewConfiguration(_BaseViewConfiguration):
|
||||
"""ViewConfiguration — 视图配置 (继承 view.ViewConfiguration 的所有方法)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -615,8 +634,8 @@ class ViewConfiguration(Stub):
|
||||
# ViewParent (扩展)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ViewParent(Stub):
|
||||
"""ViewParent — 视图父容器接口扩展"""
|
||||
class ViewParent(_BaseViewParent):
|
||||
"""ViewParent — 视图父容器接口扩展 (继承 view.ViewParent 的所有方法)"""
|
||||
|
||||
def requestLayout(self) -> None:
|
||||
pass
|
||||
@@ -699,8 +718,8 @@ class ViewParent(Stub):
|
||||
# ViewGroup (扩展)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ViewGroup(Stub):
|
||||
"""ViewGroup — 视图组扩展"""
|
||||
class ViewGroup(_BaseViewGroup):
|
||||
"""ViewGroup — 视图组扩展 (继承 view.ViewGroup 的所有方法)"""
|
||||
|
||||
CLIP_TO_PADDING_MASK = 0
|
||||
FOCUS_AFTER = 1
|
||||
@@ -781,15 +800,19 @@ class ViewGroup(Stub):
|
||||
f"margins=({self.leftMargin},{self.topMargin},"
|
||||
f"{self.rightMargin},{self.bottomMargin}))")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._children: List[Any] = []
|
||||
self._clip_children = True
|
||||
self._clip_to_padding = True
|
||||
def __init__(self, context=None):
|
||||
super().__init__(context)
|
||||
if not hasattr(self, '_children'):
|
||||
self._children: List[Any] = []
|
||||
if not hasattr(self, '_clip_children'):
|
||||
self._clip_children = True
|
||||
if not hasattr(self, '_clip_to_padding'):
|
||||
self._clip_to_padding = True
|
||||
self._layout_mode = self.LAYOUT_MODE_CLIP_BOUNDS
|
||||
self._persistent_caching = self.PERSISTENT_NO_CACHE
|
||||
self._touch_mode = False
|
||||
self._motion_event_splitting = True
|
||||
if not hasattr(self, '_motion_event_splitting'):
|
||||
self._motion_event_splitting = True
|
||||
self._transient_child: Optional[Any] = None
|
||||
self._add_stateset_focus = False
|
||||
self._child_drawable_state: Optional[Any] = None
|
||||
@@ -947,8 +970,8 @@ class ViewOverlay(Stub):
|
||||
# WindowInsets / DisplayCutout / Display
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class WindowInsets(Stub):
|
||||
"""WindowInsets — 窗口插入区域"""
|
||||
class WindowInsets(_BaseWindowInsets):
|
||||
"""WindowInsets — 窗口插入区域 (继承 view.WindowInsets 的所有方法)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -1080,8 +1103,8 @@ class DisplayCutout(Stub):
|
||||
return f"DisplayCutout(safe={self._safe_insets})"
|
||||
|
||||
|
||||
class Display(Stub):
|
||||
"""Display — 屏幕显示信息"""
|
||||
class Display(_BaseDisplay):
|
||||
"""Display — 屏幕显示信息 (继承 view.Display 的所有方法)"""
|
||||
|
||||
DEFAULT_DISPLAY = 0
|
||||
FLAG_PRESENTATION = 1
|
||||
@@ -1423,8 +1446,8 @@ WINDOW_INSETS_ANIMATION_TYPE_TRANSLATION = 2
|
||||
WINDOW_INSETS_ANIMATION_TYPE_ALPHA_AND_TRANSLATION = 3
|
||||
|
||||
|
||||
class DragEvent(Stub):
|
||||
"""DragEvent — 拖放事件"""
|
||||
class DragEvent(_BaseDragEvent):
|
||||
"""DragEvent — 拖放事件 (继承 view.DragEvent 的所有方法)"""
|
||||
|
||||
ACTION_DRAG_STARTED = DRAG_EVENT_ACTION_DRAG_STARTED
|
||||
ACTION_DRAG_ENTERED = DRAG_EVENT_ACTION_DRAG_ENTERED
|
||||
@@ -1467,8 +1490,8 @@ class DragEvent(Stub):
|
||||
return f"DragEvent(action={actions.get(self._action, 'UNKNOWN')})"
|
||||
|
||||
|
||||
class GestureDetector(Stub):
|
||||
"""GestureDetector — 手势检测器"""
|
||||
class GestureDetector(_BaseGestureDetector):
|
||||
"""GestureDetector — 手势检测器 (继承 view.GestureDetector 的所有方法)"""
|
||||
|
||||
class SimpleOnGestureListener(Stub):
|
||||
"""GestureDetector.SimpleOnGestureListener — 简单手势监听器"""
|
||||
@@ -1559,8 +1582,8 @@ class GestureDetector(Stub):
|
||||
return f"GestureDetector(listener={'yes' if self._listener else 'no'})"
|
||||
|
||||
|
||||
class KeyEvent(Stub):
|
||||
"""KeyEvent — 按键事件"""
|
||||
class KeyEvent(_BaseKeyEvent):
|
||||
"""KeyEvent — 按键事件 (扩展版,继承 view.KeyEvent 的所有常量)"""
|
||||
|
||||
ACTION_DOWN = 0
|
||||
ACTION_UP = 1
|
||||
@@ -2083,8 +2106,8 @@ class ContextMenu(Menu):
|
||||
# ScaleGestureDetector / SoundEffectConstants / Surface / etc.
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ScaleGestureDetector(Stub):
|
||||
"""ScaleGestureDetector — 缩放手势检测器"""
|
||||
class ScaleGestureDetector(_BaseScaleGestureDetector):
|
||||
"""ScaleGestureDetector — 缩放手势检测器 (继承 view.ScaleGestureDetector 的所有方法)"""
|
||||
|
||||
class SimpleOnScaleGestureListener(Stub):
|
||||
def onScale(self, detector: "ScaleGestureDetector") -> bool:
|
||||
@@ -2164,8 +2187,8 @@ class ScaleGestureDetector(Stub):
|
||||
f"inProgress={self._in_progress})")
|
||||
|
||||
|
||||
class SoundEffectConstants(Stub):
|
||||
"""SoundEffectConstants — 音效常量"""
|
||||
class SoundEffectConstants(_BaseSoundEffectConstants):
|
||||
"""SoundEffectConstants — 音效常量 (继承 view.SoundEffectConstants 的所有方法)"""
|
||||
|
||||
CLICK = 0
|
||||
NAVIGATION_UP = 1
|
||||
@@ -2182,8 +2205,8 @@ class SoundEffectConstants(Stub):
|
||||
return SoundEffectConstants.NAVIGATION_UP
|
||||
|
||||
|
||||
class Surface(Stub):
|
||||
"""Surface — 表面"""
|
||||
class Surface(_BaseSurface):
|
||||
"""Surface — 表面 (继承 view.Surface 的所有方法)"""
|
||||
|
||||
ROTATION_0 = 0
|
||||
ROTATION_90 = 1
|
||||
@@ -2256,8 +2279,8 @@ class SurfaceControl(Stub):
|
||||
return f"SurfaceControl(layer={self._layer}, visible={self._visible})"
|
||||
|
||||
|
||||
class SurfaceView(Stub):
|
||||
"""SurfaceView — 表面视图扩展"""
|
||||
class SurfaceView(_BaseSurfaceView):
|
||||
"""SurfaceView — 表面视图扩展 (继承 view.SurfaceView 的所有方法)"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
super().__init__()
|
||||
@@ -2284,8 +2307,8 @@ class SurfaceView(Stub):
|
||||
return "SurfaceView()"
|
||||
|
||||
|
||||
class TextureView(Stub):
|
||||
"""TextureView — 纹理视图"""
|
||||
class TextureView(_BaseTextureView):
|
||||
"""TextureView — 纹理视图扩展 (继承 view.TextureView 的所有方法)"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
super().__init__()
|
||||
@@ -2328,8 +2351,8 @@ class TouchDelegate(Stub):
|
||||
return f"TouchDelegate(delegate={self._delegate_view})"
|
||||
|
||||
|
||||
class VelocityTracker(Stub):
|
||||
"""VelocityTracker — 速度追踪器"""
|
||||
class VelocityTracker(_BaseVelocityTracker):
|
||||
"""VelocityTracker — 速度追踪器 (继承 view.VelocityTracker 的所有方法)"""
|
||||
|
||||
def __init__(self, strategy: str = ""):
|
||||
super().__init__()
|
||||
@@ -2445,8 +2468,8 @@ class WindowInsetsAnimation(Stub):
|
||||
f"fraction={self._interpolated_fraction})")
|
||||
|
||||
|
||||
class WindowInsetsController(Stub):
|
||||
"""WindowInsetsController — 窗口插入控制器"""
|
||||
class WindowInsetsController(_BaseWindowInsetsController):
|
||||
"""WindowInsetsController — 窗口插入控制器 (继承 view.WindowInsetsController 的所有方法)"""
|
||||
|
||||
APPEARANCE_OPAQUE_STATUS_BARS = 1
|
||||
APPEARANCE_OPAQUE_NAVIGATION_BARS = 2
|
||||
|
||||
+144
-30
@@ -14,6 +14,12 @@ from .view import (
|
||||
ViewGroup as VG,
|
||||
)
|
||||
|
||||
try:
|
||||
from .._linux_impl import IS_LINUX, LinuxNotify, LinuxClipboard
|
||||
_USE_LINUX = IS_LINUX
|
||||
except ImportError:
|
||||
_USE_LINUX = False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# TextView
|
||||
@@ -359,6 +365,39 @@ class TextView(View):
|
||||
def setPadding(self, left: int, top: int, right: int, bottom: int):
|
||||
super().setPadding(left, top, right, bottom)
|
||||
|
||||
def setPaddingRelative(self, start: int, top: int, end: int, bottom: int):
|
||||
self._padding_left = start
|
||||
self._padding_top = top
|
||||
self._padding_right = end
|
||||
self._padding_bottom = bottom
|
||||
|
||||
# ─── text color getter ───────────────────────────────────
|
||||
|
||||
def getTextColor(self) -> int:
|
||||
return self._text_color
|
||||
|
||||
# ─── selection action mode ───────────────────────────────
|
||||
|
||||
def setCustomSelectionActionModeCallback(self, callback: Any):
|
||||
self._custom_selection_action_mode_callback = callback
|
||||
|
||||
def getCustomSelectionActionModeCallback(self) -> Any:
|
||||
return getattr(self, '_custom_selection_action_mode_callback', None)
|
||||
|
||||
# ─── break / justification strategy ──────────────────────
|
||||
|
||||
def setBreakStrategy(self, break_strategy: int):
|
||||
self._break_strategy = break_strategy
|
||||
|
||||
def getBreakStrategy(self) -> int:
|
||||
return getattr(self, '_break_strategy', 0)
|
||||
|
||||
def setJustificationMode(self, justification_mode: int):
|
||||
self._justification_mode = justification_mode
|
||||
|
||||
def getJustificationMode(self) -> int:
|
||||
return getattr(self, '_justification_mode', 0)
|
||||
|
||||
def onMeasure(self, width_measure_spec: int, height_measure_spec: int):
|
||||
from .view import MeasureSpec
|
||||
w = MeasureSpec.getSize(width_measure_spec)
|
||||
@@ -450,6 +489,12 @@ class CheckBox(Button):
|
||||
def toggle(self):
|
||||
self._checked = not self._checked
|
||||
|
||||
def setOnCheckedChangeListener(self, listener: Optional[Callable]):
|
||||
self._on_checked_change_listener = listener
|
||||
|
||||
def getOnCheckedChangeListener(self) -> Optional[Callable]:
|
||||
return getattr(self, '_on_checked_change_listener', None)
|
||||
|
||||
def performClick(self) -> bool:
|
||||
self.toggle()
|
||||
return super().performClick()
|
||||
@@ -601,6 +646,22 @@ class Switch(Button):
|
||||
def getTextOff(self) -> str:
|
||||
return self._text_off
|
||||
|
||||
# ─── checked change listener ─────────────────────────────
|
||||
|
||||
def setOnCheckedChangeListener(self, listener: Optional[Callable]):
|
||||
self._on_checked_change_listener = listener
|
||||
|
||||
def getOnCheckedChangeListener(self) -> Optional[Callable]:
|
||||
return getattr(self, '_on_checked_change_listener', None)
|
||||
|
||||
# ─── switch text appearance ──────────────────────────────
|
||||
|
||||
def setSwitchTextAppearance(self, context: Any, resid: int):
|
||||
self._switch_text_appearance = resid
|
||||
|
||||
def getSwitchTextAppearance(self) -> int:
|
||||
return getattr(self, '_switch_text_appearance', 0)
|
||||
|
||||
def performClick(self) -> bool:
|
||||
self.toggle()
|
||||
return super().performClick()
|
||||
@@ -688,6 +749,17 @@ class ImageView(View):
|
||||
def getCropToPadding(self) -> bool:
|
||||
return self._crop_to_padding
|
||||
|
||||
# ─── color filter ─────────────────────────────────────────
|
||||
|
||||
def setColorFilter(self, color_filter: Any):
|
||||
self._color_filter = color_filter
|
||||
|
||||
def getColorFilter(self) -> Any:
|
||||
return getattr(self, '_color_filter', None)
|
||||
|
||||
def clearColorFilter(self):
|
||||
self._color_filter = None
|
||||
|
||||
def setBaseline(self, baseline: int):
|
||||
self._baseline = baseline
|
||||
|
||||
@@ -1110,6 +1182,10 @@ class ListView(AdapterView):
|
||||
def getCheckItemIds(self) -> List[int]:
|
||||
return list(self._checked_id_positions.keys())
|
||||
|
||||
def clearChoices(self):
|
||||
self._checked_item_positions.clear()
|
||||
self._checked_id_positions.clear()
|
||||
|
||||
|
||||
class GridView(AdapterView):
|
||||
"""GridView — 网格视图"""
|
||||
@@ -1254,28 +1330,48 @@ class RecyclerView(ViewGroup):
|
||||
return False
|
||||
|
||||
def notifyDataSetChanged(self):
|
||||
pass
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onChanged()
|
||||
|
||||
def notifyItemChanged(self, position: int):
|
||||
pass
|
||||
def notifyItemChanged(self, position, payload=None):
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onItemRangeChanged(position, 1, payload)
|
||||
|
||||
def notifyItemInserted(self, position: int):
|
||||
pass
|
||||
def notifyItemInserted(self, position):
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onItemRangeInserted(position, 1)
|
||||
|
||||
def notifyItemRemoved(self, position: int):
|
||||
pass
|
||||
def notifyItemRemoved(self, position):
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onItemRangeRemoved(position, 1)
|
||||
|
||||
def notifyItemMoved(self, from_position: int, to_position: int):
|
||||
pass
|
||||
def notifyItemMoved(self, fromPosition, toPosition):
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onItemRangeMoved(fromPosition, toPosition, 1)
|
||||
|
||||
def notifyItemRangeChanged(self, position_start: int, item_count: int):
|
||||
pass
|
||||
def notifyItemRangeChanged(self, positionStart, itemCount, payload=None):
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onItemRangeChanged(positionStart, itemCount, payload)
|
||||
|
||||
def notifyItemRangeInserted(self, position_start: int, item_count: int):
|
||||
pass
|
||||
def notifyItemRangeInserted(self, positionStart, itemCount):
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onItemRangeInserted(positionStart, itemCount)
|
||||
|
||||
def notifyItemRangeRemoved(self, position_start: int, item_count: int):
|
||||
pass
|
||||
def notifyItemRangeRemoved(self, positionStart, itemCount):
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onItemRangeRemoved(positionStart, itemCount)
|
||||
|
||||
def registerAdapterDataObserver(self, observer):
|
||||
if not hasattr(self, '_observers'):
|
||||
self._observers = []
|
||||
self._observers.append(observer)
|
||||
|
||||
def unregisterAdapterDataObserver(self, observer):
|
||||
if hasattr(self, '_observers'):
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def getStateRestorationPolicy(self) -> int:
|
||||
return 0
|
||||
@@ -2062,11 +2158,13 @@ class ScrollView(FrameLayout):
|
||||
def arrowScroll(self, direction: int) -> bool:
|
||||
return True
|
||||
|
||||
def smoothScrollTo(self, x: int, y: int):
|
||||
self._scroll_position = y
|
||||
def smoothScrollTo(self, x, y):
|
||||
self._scroll_x = x
|
||||
self._scroll_y = y
|
||||
|
||||
def smoothScrollBy(self, dx: int, dy: int):
|
||||
self._scroll_position += dy
|
||||
def smoothScrollBy(self, dx, dy):
|
||||
self._scroll_x = getattr(self, '_scroll_x', 0) + dx
|
||||
self._scroll_y = getattr(self, '_scroll_y', 0) + dy
|
||||
|
||||
def pageScroll(self, direction: int) -> bool:
|
||||
return True
|
||||
@@ -2100,11 +2198,13 @@ class HorizontalScrollView(FrameLayout):
|
||||
def arrowScroll(self, direction: int) -> bool:
|
||||
return True
|
||||
|
||||
def smoothScrollTo(self, x: int, y: int):
|
||||
pass
|
||||
def smoothScrollTo(self, x, y):
|
||||
self._scroll_x = x
|
||||
self._scroll_y = y
|
||||
|
||||
def smoothScrollBy(self, dx: int, dy: int):
|
||||
pass
|
||||
def smoothScrollBy(self, dx, dy):
|
||||
self._scroll_x = getattr(self, '_scroll_x', 0) + dx
|
||||
self._scroll_y = getattr(self, '_scroll_y', 0) + dy
|
||||
|
||||
def pageScroll(self, direction: int) -> bool:
|
||||
return True
|
||||
@@ -2789,16 +2889,24 @@ class BaseAdapter(Stub):
|
||||
return self.getView(position, convert_view, parent)
|
||||
|
||||
def notifyDataSetChanged(self):
|
||||
pass
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onChanged()
|
||||
|
||||
def notifyDataSetInvalidated(self):
|
||||
pass
|
||||
for observer in getattr(self, '_observers', []):
|
||||
observer.onInvalidated()
|
||||
|
||||
def registerDataSetObserver(self, observer: Any):
|
||||
pass
|
||||
def registerDataSetObserver(self, observer):
|
||||
if not hasattr(self, '_observers'):
|
||||
self._observers = []
|
||||
self._observers.append(observer)
|
||||
|
||||
def unregisterDataSetObserver(self, observer: Any):
|
||||
pass
|
||||
def unregisterDataSetObserver(self, observer):
|
||||
if hasattr(self, '_observers'):
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class ArrayAdapter(BaseAdapter):
|
||||
@@ -3589,9 +3697,15 @@ class Toast(Stub):
|
||||
return toast
|
||||
|
||||
def show(self):
|
||||
pass
|
||||
if _USE_LINUX:
|
||||
LinuxNotify.send("Toast", self._text)
|
||||
else:
|
||||
print(f"[Toast] {self._text}")
|
||||
|
||||
def cancel(self):
|
||||
if _USE_LINUX:
|
||||
# 桌面通知自动消失,无需操作
|
||||
return
|
||||
pass
|
||||
|
||||
def setGravity(self, gravity: int, x_offset: int, y_offset: int):
|
||||
|
||||
@@ -11,6 +11,35 @@ import threading
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from .._core import Stub, AndroidOnlyError
|
||||
from .widget import AdapterView as _BaseAdapterView
|
||||
from .widget import AutoCompleteTextView as _BaseAutoCompleteTextView
|
||||
from .widget import ProgressBar as _BaseProgressBar
|
||||
from .widget import Chronometer as _BaseChronometer
|
||||
from .widget import TableLayout as _BaseTableLayout
|
||||
from .widget import TableRow as _BaseTableRow
|
||||
from .widget import LinearLayout as _BaseLinearLayout
|
||||
from .widget import FrameLayout as _BaseFrameLayout
|
||||
from .widget import RelativeLayout as _BaseRelativeLayout
|
||||
from .widget import NumberPicker as _BaseNumberPicker
|
||||
from .widget import SearchView as _BaseSearchView
|
||||
from .widget import TabHost as _BaseTabHost
|
||||
from .widget import TabWidget as _BaseTabWidget
|
||||
from .widget import ViewAnimator as _BaseViewAnimator
|
||||
from .widget import ImageButton as _BaseImageButton
|
||||
from .widget import ZoomControls as _BaseZoomControls
|
||||
from .widget import DigitalClock as _BaseDigitalClock
|
||||
from .widget import AnalogClock as _BaseAnalogClock
|
||||
from .widget import ToggleButton as _BaseToggleButton
|
||||
from .widget import RadioButton as _BaseRadioButton
|
||||
from .widget import SeekBar as _BaseSeekBar
|
||||
from .widget import RatingBar as _BaseRatingBar
|
||||
from .widget import ViewSwitcher as _BaseViewSwitcher
|
||||
from .widget import TextSwitcher as _BaseTextSwitcher
|
||||
from .widget import ImageSwitcher as _BaseImageSwitcher
|
||||
from .widget import BaseAdapter as _BaseBaseAdapter
|
||||
from .widget import ArrayAdapter as _BaseArrayAdapter
|
||||
from .widget import SimpleAdapter as _BaseSimpleAdapter
|
||||
from .widget import CursorAdapter as _BaseCursorAdapter
|
||||
from ..java.lang import RuntimeException
|
||||
|
||||
|
||||
@@ -135,7 +164,7 @@ class Adapter(Stub):
|
||||
return f"Adapter(count={self.getCount()})"
|
||||
|
||||
|
||||
class AdapterView(Stub):
|
||||
class AdapterView(_BaseAdapterView):
|
||||
"""AdapterView — 适配器视图"""
|
||||
|
||||
ITEM_VIEW_TYPE_IGNORE = -1
|
||||
@@ -326,7 +355,7 @@ class ThemedSpinnerAdapter(Stub):
|
||||
# AutoCompleteTextView
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class AutoCompleteTextView(Stub):
|
||||
class AutoCompleteTextView(_BaseAutoCompleteTextView):
|
||||
"""AutoCompleteTextView — 自动完成文本视图"""
|
||||
|
||||
THRESHOLD_DEFAULT = 2
|
||||
@@ -429,7 +458,7 @@ class AutoCompleteTextView(Stub):
|
||||
# ProgressBar / SeekBar / AbsSeekBar / RatingBar
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ProgressBar(Stub):
|
||||
class ProgressBar(_BaseProgressBar):
|
||||
"""ProgressBar — 进度条扩展"""
|
||||
|
||||
STYLE_HORIZONTAL = 0x00000001
|
||||
@@ -568,11 +597,11 @@ class AbsSeekBar(ProgressBar):
|
||||
self._on_seek_bar_change = listener
|
||||
|
||||
|
||||
class SeekBar(AbsSeekBar):
|
||||
class SeekBar(_BaseSeekBar, AbsSeekBar):
|
||||
"""SeekBar — 拖动条"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
super().__init__(context)
|
||||
AbsSeekBar.__init__(self, context)
|
||||
self._on_seek_bar_change: Optional[Callable] = None
|
||||
|
||||
def setOnSeekBarChangeListener(self, listener: Optional[Callable]) -> None:
|
||||
@@ -585,12 +614,12 @@ class SeekBar(AbsSeekBar):
|
||||
return f"SeekBar(progress={self.getProgress()})"
|
||||
|
||||
|
||||
class RatingBar(ProgressBar):
|
||||
class RatingBar(_BaseRatingBar, ProgressBar):
|
||||
"""RatingBar — 评分条"""
|
||||
|
||||
def __init__(self, context: Any = None, num_stars: int = 5,
|
||||
step_size: float = 0.5):
|
||||
super().__init__(context, ProgressBar.STYLE_HORIZONTAL)
|
||||
ProgressBar.__init__(self, context, ProgressBar.STYLE_HORIZONTAL)
|
||||
self._num_stars = num_stars
|
||||
self._step_size = step_size
|
||||
self._rating: float = 0.0
|
||||
@@ -638,7 +667,7 @@ class RatingBar(ProgressBar):
|
||||
# Chronometer
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class Chronometer(Stub):
|
||||
class Chronometer(_BaseChronometer):
|
||||
"""Chronometer — 计时器"""
|
||||
|
||||
FORMAT_ELAPSED_TIME = 1
|
||||
@@ -734,11 +763,12 @@ class CompoundButton(Stub):
|
||||
return f"CompoundButton(checked={self._checked})"
|
||||
|
||||
|
||||
class ToggleButton(CompoundButton):
|
||||
class ToggleButton(_BaseToggleButton, CompoundButton):
|
||||
"""ToggleButton — 开关按钮"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
super().__init__(context)
|
||||
CompoundButton.__init__(self, context)
|
||||
self._text_on: str = "ON"
|
||||
self._text_off: str = "OFF"
|
||||
|
||||
@@ -758,11 +788,12 @@ class ToggleButton(CompoundButton):
|
||||
return f"ToggleButton(checked={self.isChecked()})"
|
||||
|
||||
|
||||
class RadioButton(CompoundButton):
|
||||
class RadioButton(_BaseRadioButton, CompoundButton):
|
||||
"""RadioButton — 单选按钮"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
super().__init__(context)
|
||||
CompoundButton.__init__(self, context)
|
||||
|
||||
def toggle(self) -> None:
|
||||
if not self.isChecked():
|
||||
@@ -827,7 +858,7 @@ class RadioGroup(Stub):
|
||||
# TableLayout / TableRow
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class TableLayout(Stub):
|
||||
class TableLayout(_BaseTableLayout):
|
||||
"""TableLayout — 表格布局"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
@@ -863,7 +894,7 @@ class TableLayout(Stub):
|
||||
return f"TableLayout(rows={len(self._rows)})"
|
||||
|
||||
|
||||
class TableRow(Stub):
|
||||
class TableRow(_BaseTableRow):
|
||||
"""TableRow — 表格行"""
|
||||
|
||||
class LayoutParams(Stub):
|
||||
@@ -1029,7 +1060,7 @@ class GridLayout(Stub):
|
||||
# LinearLayout / FrameLayout / RelativeLayout
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class LinearLayout(Stub):
|
||||
class LinearLayout(_BaseLinearLayout):
|
||||
"""LinearLayout — 线性布局"""
|
||||
|
||||
HORIZONTAL = 0
|
||||
@@ -1120,7 +1151,7 @@ class LinearLayout(Stub):
|
||||
return f"LinearLayout(orientation={'H' if self._orientation == 0 else 'V'})"
|
||||
|
||||
|
||||
class FrameLayout(Stub):
|
||||
class FrameLayout(_BaseFrameLayout):
|
||||
"""FrameLayout — 帧布局"""
|
||||
|
||||
class LayoutParams(Stub):
|
||||
@@ -1177,7 +1208,7 @@ class FrameLayout(Stub):
|
||||
return "FrameLayout()"
|
||||
|
||||
|
||||
class RelativeLayout(Stub):
|
||||
class RelativeLayout(_BaseRelativeLayout):
|
||||
"""RelativeLayout — 相对布局"""
|
||||
|
||||
TRUE = -1
|
||||
@@ -1284,7 +1315,7 @@ class RelativeLayout(Stub):
|
||||
# NumberPicker / SearchView / TabHost / TabWidget
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class NumberPicker(Stub):
|
||||
class NumberPicker(_BaseNumberPicker):
|
||||
"""NumberPicker — 数字选择器"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
@@ -1344,7 +1375,7 @@ class NumberPicker(Stub):
|
||||
return f"NumberPicker(value={self._value}, range=[{self._min_value}-{self._max_value}])"
|
||||
|
||||
|
||||
class SearchView(Stub):
|
||||
class SearchView(_BaseSearchView):
|
||||
"""SearchView — 搜索视图扩展"""
|
||||
|
||||
ICONIFIED = 0
|
||||
@@ -1425,7 +1456,7 @@ class SearchView(Stub):
|
||||
return f"SearchView(query={self._query})"
|
||||
|
||||
|
||||
class TabHost(Stub):
|
||||
class TabHost(_BaseTabHost):
|
||||
"""TabHost — 标签宿主"""
|
||||
|
||||
class TabSpec(Stub):
|
||||
@@ -1493,7 +1524,7 @@ class TabHost(Stub):
|
||||
return f"TabHost(tabs={len(self._tabs)}, current={self._current_tab})"
|
||||
|
||||
|
||||
class TabWidget(Stub):
|
||||
class TabWidget(_BaseTabWidget):
|
||||
"""TabWidget — 标签小部件"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
@@ -1529,7 +1560,7 @@ class TabWidget(Stub):
|
||||
# TextSwitcher / ViewSwitcher / ViewAnimator / ImageSwitcher
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class ViewAnimator(Stub):
|
||||
class ViewAnimator(_BaseViewAnimator):
|
||||
"""ViewAnimator — 视图动画器"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
@@ -1595,7 +1626,7 @@ class ViewAnimator(Stub):
|
||||
return f"ViewAnimator(children={len(self._children)}, displayed={self._displayed_child})"
|
||||
|
||||
|
||||
class ViewSwitcher(ViewAnimator):
|
||||
class ViewSwitcher(_BaseViewSwitcher, ViewAnimator):
|
||||
"""ViewSwitcher — 视图切换器"""
|
||||
|
||||
class ViewFactory(Stub):
|
||||
@@ -1605,6 +1636,7 @@ class ViewSwitcher(ViewAnimator):
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
super().__init__(context)
|
||||
ViewAnimator.__init__(self, context)
|
||||
self._factory: Optional[ViewSwitcher.ViewFactory] = None
|
||||
|
||||
def setFactory(self, factory: ViewFactory) -> None:
|
||||
@@ -1623,11 +1655,12 @@ class ViewSwitcher(ViewAnimator):
|
||||
return f"ViewSwitcher()"
|
||||
|
||||
|
||||
class TextSwitcher(ViewSwitcher):
|
||||
class TextSwitcher(_BaseTextSwitcher, ViewSwitcher):
|
||||
"""TextSwitcher — 文本切换器"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
super().__init__(context)
|
||||
ViewSwitcher.__init__(self, context)
|
||||
self._factory = None
|
||||
|
||||
def setCurrentText(self, text: str) -> None:
|
||||
@@ -1640,11 +1673,12 @@ class TextSwitcher(ViewSwitcher):
|
||||
return "TextSwitcher()"
|
||||
|
||||
|
||||
class ImageSwitcher(ViewSwitcher):
|
||||
class ImageSwitcher(_BaseImageSwitcher, ViewSwitcher):
|
||||
"""ImageSwitcher — 图片切换器"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
super().__init__(context)
|
||||
ViewSwitcher.__init__(self, context)
|
||||
|
||||
def setImageResource(self, res_id: int) -> None:
|
||||
pass
|
||||
@@ -1659,7 +1693,7 @@ class ImageSwitcher(ViewSwitcher):
|
||||
return "ImageSwitcher()"
|
||||
|
||||
|
||||
class ImageButton(Stub):
|
||||
class ImageButton(_BaseImageButton):
|
||||
"""ImageButton — 图片按钮"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
@@ -1711,7 +1745,7 @@ class ZoomButton(ImageButton):
|
||||
return f"ZoomButton(zoomIn={self._zoom_in})"
|
||||
|
||||
|
||||
class ZoomControls(Stub):
|
||||
class ZoomControls(_BaseZoomControls):
|
||||
"""ZoomControls — 缩放控件"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
@@ -1757,7 +1791,7 @@ class ZoomControls(Stub):
|
||||
# DigitalClock / TextClock / AnalogClock
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class DigitalClock(Stub):
|
||||
class DigitalClock(_BaseDigitalClock):
|
||||
"""DigitalClock — 数字时钟"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
@@ -1825,7 +1859,7 @@ class TextClock(Stub):
|
||||
return f"TextClock(time={self.getText()})"
|
||||
|
||||
|
||||
class AnalogClock(Stub):
|
||||
class AnalogClock(_BaseAnalogClock):
|
||||
"""AnalogClock — 模拟时钟"""
|
||||
|
||||
def __init__(self, context: Any = None):
|
||||
@@ -1882,7 +1916,7 @@ class QuickContactBadge(Stub):
|
||||
# 各种 Adapter 实现
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class BaseAdapter(Adapter):
|
||||
class BaseAdapter(_BaseBaseAdapter, Adapter):
|
||||
"""BaseAdapter — 基础适配器"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -1923,12 +1957,12 @@ class BaseAdapter(Adapter):
|
||||
return self.getCount() == 0
|
||||
|
||||
|
||||
class ArrayAdapter(BaseAdapter):
|
||||
class ArrayAdapter(_BaseArrayAdapter, BaseAdapter):
|
||||
"""ArrayAdapter — 数组适配器"""
|
||||
|
||||
def __init__(self, context: Any = None, resource: int = 0,
|
||||
objects: Optional[List[Any]] = None):
|
||||
super().__init__()
|
||||
BaseAdapter.__init__(self)
|
||||
self._context = context
|
||||
self._resource = resource
|
||||
self._drop_down_resource: int = resource
|
||||
@@ -1990,7 +2024,7 @@ class ArrayAdapter(BaseAdapter):
|
||||
return f"ArrayAdapter(items={len(self._objects)})"
|
||||
|
||||
|
||||
class SimpleAdapter(BaseAdapter):
|
||||
class SimpleAdapter(_BaseSimpleAdapter, BaseAdapter):
|
||||
"""SimpleAdapter — 简单适配器"""
|
||||
|
||||
def __init__(self, context: Any = None,
|
||||
@@ -1998,7 +2032,7 @@ class SimpleAdapter(BaseAdapter):
|
||||
resource: int = 0,
|
||||
from_keys: Optional[List[str]] = None,
|
||||
to_ids: Optional[List[int]] = None):
|
||||
super().__init__()
|
||||
BaseAdapter.__init__(self)
|
||||
self._context = context
|
||||
self._data: List[Dict[str, Any]] = data or []
|
||||
self._resource = resource
|
||||
@@ -2031,7 +2065,7 @@ class SimpleAdapter(BaseAdapter):
|
||||
return f"SimpleAdapter(items={len(self._data)})"
|
||||
|
||||
|
||||
class CursorAdapter(BaseAdapter):
|
||||
class CursorAdapter(_BaseCursorAdapter, BaseAdapter):
|
||||
"""CursorAdapter — 游标适配器"""
|
||||
|
||||
FLAG_AUTO_REQUERY = 1
|
||||
@@ -2039,7 +2073,7 @@ class CursorAdapter(BaseAdapter):
|
||||
|
||||
def __init__(self, context: Any = None, cursor: Any = None,
|
||||
flags: int = FLAG_AUTO_REQUERY):
|
||||
super().__init__()
|
||||
BaseAdapter.__init__(self)
|
||||
self._context = context
|
||||
self._cursor = cursor
|
||||
self._flags = flags
|
||||
|
||||
+209
-26
@@ -237,7 +237,10 @@ class File(Stub):
|
||||
return _os.path.abspath(self._path)
|
||||
|
||||
def getCanonicalPath(self):
|
||||
return _os.path.realpath(self._path)
|
||||
try:
|
||||
return _os.path.realpath(self._path)
|
||||
except Exception:
|
||||
return self.getAbsolutePath()
|
||||
|
||||
def getParent(self):
|
||||
p = _os.path.dirname(self._path)
|
||||
@@ -439,6 +442,43 @@ class File(Stub):
|
||||
def toPath(self):
|
||||
return _PathLibPath(self._path)
|
||||
|
||||
@staticmethod
|
||||
def createTempFile(prefix, suffix=None, directory=None):
|
||||
import tempfile
|
||||
if directory:
|
||||
fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix or ".tmp", dir=directory)
|
||||
else:
|
||||
fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix or ".tmp")
|
||||
_os.close(fd)
|
||||
return File(path)
|
||||
|
||||
def deleteOnExit(self):
|
||||
import atexit
|
||||
atexit.register(lambda: self.delete() if self.exists() else None)
|
||||
|
||||
def setLastModified(self, time):
|
||||
try:
|
||||
_os.utime(self._path, (time / 1000, time / 1000))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def setReadOnly(self):
|
||||
try:
|
||||
_os.chmod(self._path, 0o444)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def isAbsolute(self):
|
||||
return _os.path.isabs(self._path)
|
||||
|
||||
def getAbsoluteFile(self):
|
||||
return File(self.getAbsolutePath())
|
||||
|
||||
def getCanonicalFile(self):
|
||||
return File(self.getCanonicalPath())
|
||||
|
||||
def __str__(self):
|
||||
return self._path
|
||||
|
||||
@@ -705,14 +745,48 @@ class BufferedInputStream(FilterInputStream):
|
||||
return total if total > 0 else -1
|
||||
|
||||
def _fill(self):
|
||||
data = self._in.read(bytearray(self._buf_size), 0, self._buf_size)
|
||||
if data is None or data == -1:
|
||||
tmp = bytearray(self._buf_size)
|
||||
n = self._in.read(tmp, 0, self._buf_size)
|
||||
if n is None or n == -1:
|
||||
return False
|
||||
n = data if isinstance(data, int) else len(data)
|
||||
self._buffer = bytearray(self._in._buffer if hasattr(self._in, '_buffer') else data) if n > 0 else bytearray()
|
||||
if isinstance(n, (bytes, bytearray)):
|
||||
# defensive: underlying stream returned raw data instead of a count
|
||||
tmp = bytearray(n)
|
||||
n = len(tmp)
|
||||
if not isinstance(n, int) or n <= 0:
|
||||
return False
|
||||
self._buffer = bytearray(tmp[:n])
|
||||
self._pos = 0
|
||||
self._count = n
|
||||
return n > 0
|
||||
return True
|
||||
|
||||
def available(self):
|
||||
buf_avail = self._count - self._pos
|
||||
if buf_avail < 0:
|
||||
buf_avail = 0
|
||||
try:
|
||||
underlying = self._in.available()
|
||||
if underlying and underlying > 0:
|
||||
return buf_avail + underlying
|
||||
except Exception:
|
||||
pass
|
||||
return buf_avail
|
||||
|
||||
def mark(self, readlimit):
|
||||
self._mark_pos = self._pos
|
||||
self._mark_buffer = bytes(self._buffer)
|
||||
self._mark_count = self._count
|
||||
self._mark_limit = readlimit
|
||||
|
||||
def reset(self):
|
||||
if not hasattr(self, '_mark_pos'):
|
||||
raise IOException("Resetting to invalid mark")
|
||||
self._buffer = bytearray(self._mark_buffer)
|
||||
self._count = self._mark_count
|
||||
self._pos = self._mark_pos
|
||||
|
||||
def markSupported(self):
|
||||
return True
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -994,6 +1068,26 @@ class BufferedWriter(Closeable, Flushable):
|
||||
def newLine(self):
|
||||
self._buffer.append("\n")
|
||||
|
||||
def append(self, csq=None, start=None, end=None):
|
||||
if csq is None:
|
||||
return self
|
||||
if isinstance(csq, int):
|
||||
self._buffer.append(chr(csq))
|
||||
else:
|
||||
s = str(csq)
|
||||
if start is not None and end is not None:
|
||||
s = s[start:end]
|
||||
elif start is not None:
|
||||
s = s[start:]
|
||||
self._buffer.append(s)
|
||||
if sum(len(x) for x in self._buffer) >= self._buf_size:
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
def check(self):
|
||||
if getattr(self._writer, '_closed', False):
|
||||
raise IOException("Stream closed")
|
||||
|
||||
def flush(self):
|
||||
if self._buffer:
|
||||
data = "".join(self._buffer)
|
||||
@@ -1131,6 +1225,20 @@ class PrintWriter(Closeable, Flushable):
|
||||
else:
|
||||
self._write_str(str(s))
|
||||
|
||||
def append(self, csq=None, start=None, end=None):
|
||||
if csq is None:
|
||||
csq = "null"
|
||||
if isinstance(csq, int):
|
||||
self._write_str(chr(csq))
|
||||
else:
|
||||
s = str(csq)
|
||||
if start is not None and end is not None:
|
||||
s = s[start:end]
|
||||
elif start is not None:
|
||||
s = s[start:]
|
||||
self._write_str(s)
|
||||
return self
|
||||
|
||||
def format(self, fmt, *args):
|
||||
s = str(fmt) % args if args else str(fmt)
|
||||
self._write_str(s)
|
||||
@@ -1216,6 +1324,9 @@ class ByteArrayInputStream(Closeable):
|
||||
def reset(self):
|
||||
self._pos = self._mark
|
||||
|
||||
def markSupported(self):
|
||||
return True
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ByteArrayOutputStream
|
||||
@@ -1243,6 +1354,11 @@ class ByteArrayOutputStream(Closeable, Flushable):
|
||||
def toByteArray(self):
|
||||
return bytes(self._buf)
|
||||
|
||||
def writeTo(self, out):
|
||||
if self._closed:
|
||||
raise IOException("Stream closed")
|
||||
out.write(bytes(self._buf))
|
||||
|
||||
def size(self):
|
||||
return len(self._buf)
|
||||
|
||||
@@ -1690,37 +1806,104 @@ class PushbackInputStream(FilterInputStream):
|
||||
# SequenceInputStream
|
||||
# ============================================================
|
||||
|
||||
def _is_enumeration(obj) -> bool:
|
||||
"""True if obj really exposes hasMoreElements/nextElement (not via Stub)."""
|
||||
for cls in type(obj).__mro__:
|
||||
if ('hasMoreElements' in cls.__dict__
|
||||
and 'nextElement' in cls.__dict__):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class SequenceInputStream(Closeable):
|
||||
def __init__(self, streams):
|
||||
def __init__(self, *args):
|
||||
super().__init__()
|
||||
if hasattr(streams, 'hasMoreElements'):
|
||||
self._streams = []
|
||||
while streams.hasMoreElements():
|
||||
self._streams.append(streams.nextElement())
|
||||
else:
|
||||
self._streams = list(streams)
|
||||
self._streams = []
|
||||
self._current_idx = 0
|
||||
self._closed = False
|
||||
if len(args) == 0:
|
||||
return
|
||||
if len(args) == 1:
|
||||
streams = args[0]
|
||||
if isinstance(streams, (list, tuple)):
|
||||
self._streams = list(streams)
|
||||
elif _is_enumeration(streams):
|
||||
while streams.hasMoreElements():
|
||||
self._streams.append(streams.nextElement())
|
||||
else:
|
||||
# single stream
|
||||
self._streams = [streams]
|
||||
else:
|
||||
# SequenceInputStream(s1, s2, ...)
|
||||
self._streams = list(args)
|
||||
|
||||
def _current(self):
|
||||
if self._current_idx < len(self._streams):
|
||||
return self._streams[self._current_idx]
|
||||
while self._current_idx < len(self._streams):
|
||||
inp = self._streams[self._current_idx]
|
||||
if inp is None:
|
||||
self._current_idx += 1
|
||||
continue
|
||||
return inp
|
||||
return None
|
||||
|
||||
def read(self, b=None, off=None, length=None):
|
||||
while self._current():
|
||||
inp = self._current()
|
||||
if b is None:
|
||||
result = inp.read()
|
||||
else:
|
||||
result = inp.read(b, off, length)
|
||||
if result != -1:
|
||||
return result
|
||||
def _advance(self):
|
||||
if self._current_idx < len(self._streams):
|
||||
cur = self._streams[self._current_idx]
|
||||
if cur is not None:
|
||||
try:
|
||||
cur.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._current_idx += 1
|
||||
return -1
|
||||
|
||||
def read(self, b=None, off=None, length=None):
|
||||
if self._closed:
|
||||
raise IOException("Stream closed")
|
||||
if b is None:
|
||||
while True:
|
||||
inp = self._current()
|
||||
if inp is None:
|
||||
return -1
|
||||
result = inp.read()
|
||||
if result != -1:
|
||||
return result
|
||||
self._advance()
|
||||
if off is None:
|
||||
off = 0
|
||||
if length is None:
|
||||
length = len(b)
|
||||
total = 0
|
||||
while total < length:
|
||||
inp = self._current()
|
||||
if inp is None:
|
||||
break
|
||||
n = inp.read(b, off + total, length - total)
|
||||
if n is None or n == -1:
|
||||
self._advance()
|
||||
continue
|
||||
if n == 0:
|
||||
self._advance()
|
||||
continue
|
||||
total += n
|
||||
return total if total > 0 else -1
|
||||
|
||||
def available(self):
|
||||
inp = self._current()
|
||||
if inp is None:
|
||||
return 0
|
||||
try:
|
||||
return inp.available()
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def close(self):
|
||||
self._closed = True
|
||||
for s in self._streams:
|
||||
s.close()
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
+344
-2
@@ -8,6 +8,7 @@ import re as _re
|
||||
import math as _py_math
|
||||
import sys as _sys
|
||||
import os as _os
|
||||
import platform
|
||||
import time as _time
|
||||
import threading as _threading
|
||||
import inspect as _inspect
|
||||
@@ -28,6 +29,12 @@ __all__ = [
|
||||
"CloneNotSupportedException", "IllegalMonitorStateException",
|
||||
"IllegalThreadStateException", "EnumConstantNotPresentException",
|
||||
"TypeNotPresentException",
|
||||
"ClassNotFoundException", "IllegalAccessException", "InstantiationException",
|
||||
"InvocationTargetException", "NoSuchElementException",
|
||||
"AssertionError", "OutOfMemoryError", "StackOverflowError",
|
||||
"NoClassDefFoundError", "UnsatisfiedLinkError", "LinkageError",
|
||||
"VerifyError", "AbstractMethodError", "IncompatibleClassChangeError",
|
||||
"ExceptionInInitializerError",
|
||||
"String", "Integer", "Long", "Short", "Byte", "Float", "Double",
|
||||
"Boolean", "Character", "Math", "StrictMath", "System",
|
||||
"Thread", "ThreadLocal", "StringBuilder", "StringBuffer",
|
||||
@@ -227,6 +234,80 @@ class TypeNotPresentException(RuntimeException):
|
||||
self.typeName = typeName
|
||||
|
||||
|
||||
# ===== 缺失的 Exception 子类 =====
|
||||
class ClassNotFoundException(Exception):
|
||||
def __init__(self, message="", cause=None):
|
||||
super().__init__(message, cause)
|
||||
self.ex = message
|
||||
|
||||
class IllegalAccessException(Exception):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class InstantiationException(Exception):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class InvocationTargetException(Exception):
|
||||
def __init__(self, target=None, cause=None):
|
||||
if target is not None and cause is None:
|
||||
super().__init__(str(target))
|
||||
self.target = target
|
||||
else:
|
||||
super().__init__(str(target) if target else "", cause)
|
||||
self.target = target
|
||||
|
||||
class NoSuchElementException(RuntimeException):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
# ===== 缺失的 Error 类 =====
|
||||
class AssertionError(Error):
|
||||
def __init__(self, message=None):
|
||||
if message is None:
|
||||
super().__init__("Assertion failed")
|
||||
else:
|
||||
super().__init__(str(message))
|
||||
|
||||
class OutOfMemoryError(Error):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class StackOverflowError(Error):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class NoClassDefFoundError(Error):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class UnsatisfiedLinkError(Error):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class LinkageError(Error):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class VerifyError(LinkageError):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class AbstractMethodError(LinkageError):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class IncompatibleClassChangeError(LinkageError):
|
||||
def __init__(self, message=""):
|
||||
super().__init__(message)
|
||||
|
||||
class ExceptionInInitializerError(LinkageError):
|
||||
def __init__(self, message="", cause=None):
|
||||
super().__init__(message)
|
||||
self._cause = cause
|
||||
self.exception = cause
|
||||
|
||||
|
||||
# ============================================================
|
||||
# StackTraceElement
|
||||
# ============================================================
|
||||
@@ -753,6 +834,62 @@ class Integer(Number, Comparable):
|
||||
return Integer(int(env_val))
|
||||
return Integer(val) if val is not None else None
|
||||
|
||||
@staticmethod
|
||||
def compare(x, y):
|
||||
return -1 if x < y else (1 if x > y else 0)
|
||||
|
||||
@staticmethod
|
||||
def compareUnsigned(x, y):
|
||||
ux = x & 0xFFFFFFFF if x < 0 else x
|
||||
uy = y & 0xFFFFFFFF if y < 0 else y
|
||||
return -1 if ux < uy else (1 if ux > uy else 0)
|
||||
|
||||
@staticmethod
|
||||
def divideUnsigned(dividend, divisor):
|
||||
ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend
|
||||
uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor
|
||||
return ux // uy
|
||||
|
||||
@staticmethod
|
||||
def remainderUnsigned(dividend, divisor):
|
||||
ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend
|
||||
uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor
|
||||
return ux % uy
|
||||
|
||||
@staticmethod
|
||||
def parseUnsignedInt(s, radix=10):
|
||||
return int(s, radix)
|
||||
|
||||
@staticmethod
|
||||
def toUnsignedString(i, radix=10):
|
||||
if i < 0:
|
||||
i = i & 0xFFFFFFFF
|
||||
if radix == 10:
|
||||
return str(i)
|
||||
elif radix == 16:
|
||||
return format(i, 'x')
|
||||
elif radix == 2:
|
||||
return format(i, 'b')
|
||||
elif radix == 8:
|
||||
return format(i, 'o')
|
||||
return str(i)
|
||||
|
||||
@staticmethod
|
||||
def toUnsignedLong(i):
|
||||
return i & 0xFFFFFFFF
|
||||
|
||||
@staticmethod
|
||||
def sum(a, b):
|
||||
return a + b
|
||||
|
||||
@staticmethod
|
||||
def max(a, b):
|
||||
return a if a > b else b
|
||||
|
||||
@staticmethod
|
||||
def min(a, b):
|
||||
return a if a < b else b
|
||||
|
||||
def toString(self):
|
||||
return String(str(self._value))
|
||||
|
||||
@@ -1418,9 +1555,92 @@ class Math:
|
||||
@staticmethod
|
||||
def copySign(m, s): return abs(m) * (1 if s >= 0 else -1)
|
||||
@staticmethod
|
||||
def floorDiv(x, y): return x // y if (x ^ y) >= 0 else -(abs(x) // abs(y))
|
||||
def floorDiv(x, y):
|
||||
return math.floor(x / y) if y != 0 else 0
|
||||
@staticmethod
|
||||
def floorMod(x, y): return x - Math.floorDiv(x, y) * y
|
||||
def floorMod(x, y):
|
||||
return x - Math.floorDiv(x, y) * y
|
||||
@staticmethod
|
||||
def subtractExact(x, y):
|
||||
result = x - y
|
||||
if isinstance(x, int) and isinstance(y, int):
|
||||
if result > 0 and x < 0 and y > 0:
|
||||
raise ArithmeticException("integer overflow")
|
||||
if result < 0 and x > 0 and y < 0:
|
||||
raise ArithmeticException("integer overflow")
|
||||
return result
|
||||
@staticmethod
|
||||
def multiplyExact(x, y):
|
||||
result = x * y
|
||||
if isinstance(x, int) and isinstance(y, int):
|
||||
if result // y != x:
|
||||
raise ArithmeticException("integer overflow")
|
||||
return result
|
||||
@staticmethod
|
||||
def incrementExact(a):
|
||||
if a == 2147483647:
|
||||
raise ArithmeticException("integer overflow")
|
||||
return a + 1
|
||||
@staticmethod
|
||||
def decrementExact(a):
|
||||
if a == -2147483648:
|
||||
raise ArithmeticException("integer overflow")
|
||||
return a - 1
|
||||
@staticmethod
|
||||
def negateExact(a):
|
||||
if a == -2147483648:
|
||||
raise ArithmeticException("integer overflow")
|
||||
return -a
|
||||
@staticmethod
|
||||
def toIntExact(value):
|
||||
if value < -2147483648 or value > 2147483647:
|
||||
raise ArithmeticException("integer overflow")
|
||||
return int(value)
|
||||
@staticmethod
|
||||
def log1p(x):
|
||||
return math.log1p(x)
|
||||
@staticmethod
|
||||
def expm1(x):
|
||||
return math.expm1(x)
|
||||
@staticmethod
|
||||
def ulp(x):
|
||||
return math.ulp(x) if hasattr(math, 'ulp') else abs(x) * 2.220446049250313e-16
|
||||
@staticmethod
|
||||
def getExponent(x):
|
||||
return math.frexp(x)[1] - 1
|
||||
@staticmethod
|
||||
def nextAfter(start, direction):
|
||||
return math.nextafter(start, direction)
|
||||
@staticmethod
|
||||
def nextUp(x):
|
||||
return math.nextafter(x, math.inf)
|
||||
@staticmethod
|
||||
def nextDown(x):
|
||||
return math.nextafter(x, -math.inf)
|
||||
@staticmethod
|
||||
def rint(x):
|
||||
return math.rint(x) if hasattr(math, 'rint') else float(math.floor(x + 0.5))
|
||||
@staticmethod
|
||||
def IEEEremainder(x, y):
|
||||
return math.remainder(x, y) if hasattr(math, 'remainder') else x - y * math.floor(x / y)
|
||||
@staticmethod
|
||||
def compareUnsigned(x, y):
|
||||
ux = x & 0xFFFFFFFF if x < 0 else x
|
||||
uy = y & 0xFFFFFFFF if y < 0 else y
|
||||
return -1 if ux < uy else (1 if ux > uy else 0)
|
||||
@staticmethod
|
||||
def divideUnsigned(dividend, divisor):
|
||||
ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend
|
||||
uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor
|
||||
return ux // uy
|
||||
@staticmethod
|
||||
def remainderUnsigned(dividend, divisor):
|
||||
ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend
|
||||
uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor
|
||||
return ux % uy
|
||||
@staticmethod
|
||||
def fma(a, b, c):
|
||||
return a * b + c
|
||||
|
||||
|
||||
class StrictMath(Math):
|
||||
@@ -1529,6 +1749,39 @@ class System:
|
||||
System.err = err_stream
|
||||
@staticmethod
|
||||
def lineSeparator(): return _os.linesep
|
||||
@staticmethod
|
||||
def getProperties():
|
||||
return {
|
||||
"java.version": "1.8.0",
|
||||
"java.vendor": "Android-pyApi",
|
||||
"java.vendor.url": "https://gitee.com/shuiyu1123/Android-pyApi",
|
||||
"java.home": _os.environ.get("JAVA_HOME", ""),
|
||||
"java.class.version": "52.0",
|
||||
"os.name": platform.system(),
|
||||
"os.arch": platform.machine(),
|
||||
"os.version": platform.release(),
|
||||
"file.separator": _os.sep,
|
||||
"path.separator": _os.pathsep,
|
||||
"line.separator": _os.linesep,
|
||||
"user.name": _os.environ.get("USER", _os.environ.get("USERNAME", "")),
|
||||
"user.home": _os.path.expanduser("~"),
|
||||
"user.dir": _os.getcwd(),
|
||||
}
|
||||
@staticmethod
|
||||
def setProperties(props):
|
||||
System._properties = props
|
||||
@staticmethod
|
||||
def mapLibraryName(libname):
|
||||
if platform.system() == "Windows":
|
||||
return libname + ".dll"
|
||||
elif platform.system() == "Darwin":
|
||||
return "lib" + libname + ".dylib"
|
||||
else:
|
||||
return "lib" + libname + ".so"
|
||||
@staticmethod
|
||||
def getLogger(name):
|
||||
import logging
|
||||
return logging.getLogger(name)
|
||||
in_ = _sys.stdin
|
||||
out = _sys.stdout
|
||||
err = _sys.stderr
|
||||
@@ -1683,6 +1936,66 @@ class Thread(Runnable):
|
||||
def getDefaultUncaughtExceptionHandler():
|
||||
return Thread._default_uncaught_exception_handler
|
||||
|
||||
def getUncaughtExceptionHandler(self):
|
||||
return getattr(self, '_uncaught_handler', Thread._default_uncaught_exception_handler)
|
||||
|
||||
def setUncaughtExceptionHandler(self, handler):
|
||||
self._uncaught_handler = handler
|
||||
|
||||
@staticmethod
|
||||
def getAllStackTraces():
|
||||
import traceback
|
||||
result = {}
|
||||
# 只返回当前线程
|
||||
result[Thread.currentThread()] = traceback.extract_stack()
|
||||
return result
|
||||
|
||||
def getStackTrace(self):
|
||||
import traceback
|
||||
return traceback.extract_stack()
|
||||
|
||||
@staticmethod
|
||||
def enumerate(array):
|
||||
# 返回活动线程数
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def activeCount():
|
||||
import threading
|
||||
return threading.active_count()
|
||||
|
||||
def getContextClassLoader(self):
|
||||
return getattr(self, '_context_classloader', None)
|
||||
|
||||
def setContextClassLoader(self, cl):
|
||||
self._context_classloader = cl
|
||||
|
||||
def getThreadGroup(self):
|
||||
return getattr(self, '_thread_group', None)
|
||||
|
||||
@staticmethod
|
||||
def holdLock(obj):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def dumpStack():
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
|
||||
@staticmethod
|
||||
def onSpinWait():
|
||||
pass
|
||||
|
||||
def isVirtual(self):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def ofVirtual():
|
||||
"""创建虚拟线程 (Java 21+)"""
|
||||
t = Thread(target=lambda: None)
|
||||
t._virtual = True
|
||||
return t
|
||||
|
||||
|
||||
class ThreadLocal(Stub):
|
||||
"""java.lang.ThreadLocal"""
|
||||
@@ -1744,6 +2057,35 @@ class StringBuilder(Stub):
|
||||
def lastIndexOf(self, s, f=None):
|
||||
return self.toString().rfind(s)
|
||||
|
||||
def ensureCapacity(self, minimumCapacity):
|
||||
pass # Python list 自动扩容
|
||||
|
||||
def trimToSize(self):
|
||||
pass
|
||||
|
||||
def setLength(self, newLength):
|
||||
current = len(self._buf)
|
||||
if newLength < current:
|
||||
del self._buf[newLength:]
|
||||
else:
|
||||
self._buf.extend([" "] * (newLength - current))
|
||||
|
||||
def subSequence(self, start, end):
|
||||
return "".join(self._buf[start:end])
|
||||
|
||||
def getChars(self, srcBegin, srcEnd, dst, dstBegin):
|
||||
for i in range(srcBegin, srcEnd):
|
||||
dst[dstBegin + i - srcBegin] = self._buf[i]
|
||||
|
||||
def codePointAt(self, index):
|
||||
return ord(self._buf[index])
|
||||
|
||||
def codePointBefore(self, index):
|
||||
return ord(self._buf[index - 1])
|
||||
|
||||
def codePointCount(self, beginIndex, endIndex):
|
||||
return endIndex - beginIndex
|
||||
|
||||
|
||||
class StringBuffer(StringBuilder):
|
||||
"""java.lang.StringBuffer — 线程安全的可变字符序列"""
|
||||
|
||||
+200
-23
@@ -24,7 +24,7 @@ __all__ = [
|
||||
"InetAddress", "InetSocketAddress",
|
||||
"Socket", "ServerSocket", "DatagramSocket", "DatagramPacket",
|
||||
"Proxy", "ProxySelector", "Authenticator", "PasswordAuthentication",
|
||||
"NetworkInterface", "URI",
|
||||
"NetworkInterface", "InterfaceAddress", "URI",
|
||||
"CookieHandler", "CookieManager", "HttpCookie",
|
||||
"URLDecoder", "URLEncoder",
|
||||
"SocketException", "ConnectException", "BindException",
|
||||
@@ -1289,26 +1289,79 @@ class PasswordAuthentication(Stub):
|
||||
return self._password
|
||||
|
||||
|
||||
# ============================================================
|
||||
# InterfaceAddress
|
||||
# ============================================================
|
||||
|
||||
class InterfaceAddress(Stub):
|
||||
"""java.net.InterfaceAddress — 接口地址信息 (地址/广播/前缀长度)"""
|
||||
|
||||
def __init__(self, address=None, broadcast=None, prefix_length=0):
|
||||
super().__init__()
|
||||
self._address = address # InetAddress
|
||||
self._broadcast = broadcast # InetAddress or None
|
||||
self._prefix_length = prefix_length
|
||||
|
||||
def getAddress(self):
|
||||
return self._address
|
||||
|
||||
def getBroadcast(self):
|
||||
return self._broadcast
|
||||
|
||||
def getNetworkPrefixLength(self):
|
||||
return self._prefix_length
|
||||
|
||||
def equals(self, other):
|
||||
if isinstance(other, InterfaceAddress):
|
||||
return (self._address == other._address
|
||||
and self._prefix_length == other._prefix_length)
|
||||
return False
|
||||
|
||||
def hashCode(self):
|
||||
return hash((repr(self._address), self._prefix_length)) & 0x7FFFFFFF
|
||||
|
||||
def toString(self):
|
||||
addr = self._address.getHostAddress() if self._address else "0.0.0.0"
|
||||
return f"{addr}/{self._prefix_length}"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# NetworkInterface
|
||||
# ============================================================
|
||||
|
||||
class NetworkInterface(Stub):
|
||||
_cache = {}
|
||||
_initialized = False
|
||||
|
||||
def __init__(self, name, display_name, addrs):
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._display_name = display_name
|
||||
self._addrs = addrs
|
||||
self._mac = None # bytes, hardware address
|
||||
self._mtu = 1500
|
||||
self._state = "unknown"
|
||||
self._flags = 0
|
||||
self._interface_addrs = [] # List[InterfaceAddress]
|
||||
|
||||
@staticmethod
|
||||
def _read_sys_file(path):
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
return f.read().strip()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def getByName(name):
|
||||
if name in NetworkInterface._cache:
|
||||
return NetworkInterface._cache[name]
|
||||
if name is None:
|
||||
return None
|
||||
NetworkInterface._ensure_init()
|
||||
return NetworkInterface._cache.get(name)
|
||||
|
||||
@staticmethod
|
||||
def getByInetAddress(addr):
|
||||
NetworkInterface._ensure_init()
|
||||
for ni in NetworkInterface._cache.values():
|
||||
for a in ni._addrs:
|
||||
if a.equals(addr):
|
||||
@@ -1317,8 +1370,18 @@ class NetworkInterface(Stub):
|
||||
|
||||
@staticmethod
|
||||
def getNetworkInterfaces():
|
||||
NetworkInterface._ensure_init()
|
||||
return list(NetworkInterface._cache.values())
|
||||
|
||||
@staticmethod
|
||||
def _ensure_init():
|
||||
if not NetworkInterface._initialized:
|
||||
NetworkInterface._initialized = True
|
||||
try:
|
||||
NetworkInterface._init_from_socket()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return self._name
|
||||
|
||||
@@ -1329,25 +1392,25 @@ class NetworkInterface(Stub):
|
||||
return iter(self._addrs)
|
||||
|
||||
def getInterfaceAddresses(self):
|
||||
return self._addrs
|
||||
return list(self._interface_addrs)
|
||||
|
||||
def isUp(self):
|
||||
return True
|
||||
return self._state.lower() == "up" or (self._flags & 0x1) != 0
|
||||
|
||||
def isLoopback(self):
|
||||
return self._name == "lo"
|
||||
return (self._flags & 0x8) != 0 or self._name == "lo"
|
||||
|
||||
def isPointToPoint(self):
|
||||
return False
|
||||
return (self._flags & 0x10) != 0
|
||||
|
||||
def supportsMulticast(self):
|
||||
return True
|
||||
return (self._flags & 0x1000) != 0
|
||||
|
||||
def getHardwareAddress(self):
|
||||
return None
|
||||
return self._mac
|
||||
|
||||
def getMTU(self):
|
||||
return 1500
|
||||
return self._mtu
|
||||
|
||||
def getSubInterfaces(self):
|
||||
return iter([])
|
||||
@@ -1363,25 +1426,139 @@ class NetworkInterface(Stub):
|
||||
|
||||
@staticmethod
|
||||
def _init_from_socket():
|
||||
import netifaces
|
||||
for iface_name in netifaces.interfaces():
|
||||
"""从 /sys/class/net/ 和 socket/fcntl 枚举真实网络接口 (不依赖 netifaces)"""
|
||||
import os as _os
|
||||
net_dir = "/sys/class/net"
|
||||
if not _os.path.isdir(net_dir):
|
||||
# 非 Linux 环境: 尝试用 socket 获取本机地址
|
||||
try:
|
||||
hostname = _socket.gethostname()
|
||||
addr = InetAddress.getByName(hostname)
|
||||
ni = NetworkInterface(hostname, hostname, [addr])
|
||||
NetworkInterface._cache[hostname] = ni
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
# 尝试用 fcntl.ioctl 获取 IPv4 地址/掩码/广播
|
||||
fcntl_available = True
|
||||
try:
|
||||
import fcntl as _fcntl
|
||||
except ImportError:
|
||||
fcntl_available = False
|
||||
|
||||
for iface_name in _os.listdir(net_dir):
|
||||
addrs = []
|
||||
ifaddresses = netifaces.ifaddresses(iface_name)
|
||||
for family in [netifaces.AF_INET, netifaces.AF_INET6]:
|
||||
if family in ifaddresses:
|
||||
for addr_info in ifaddresses[family]:
|
||||
interface_addrs = []
|
||||
mac = None
|
||||
mtu = 1500
|
||||
state = "unknown"
|
||||
flags = 0
|
||||
|
||||
# --- MAC 地址 ---
|
||||
mac_str = NetworkInterface._read_sys_file(f"{net_dir}/{iface_name}/address")
|
||||
if mac_str:
|
||||
try:
|
||||
mac = bytes(int(b, 16) for b in mac_str.split(":"))
|
||||
except Exception:
|
||||
mac = None
|
||||
|
||||
# --- MTU ---
|
||||
mtu_str = NetworkInterface._read_sys_file(f"{net_dir}/{iface_name}/mtu")
|
||||
if mtu_str:
|
||||
try:
|
||||
mtu = int(mtu_str)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# --- operstate ---
|
||||
state = NetworkInterface._read_sys_file(
|
||||
f"{net_dir}/{iface_name}/operstate") or "unknown"
|
||||
|
||||
# --- flags (十六进制字符串, 如 0x1003) ---
|
||||
flags_str = NetworkInterface._read_sys_file(
|
||||
f"{net_dir}/{iface_name}/flags")
|
||||
if flags_str:
|
||||
try:
|
||||
flags = int(flags_str, 16)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# --- IPv4 地址 (fcntl.ioctl) ---
|
||||
if fcntl_available:
|
||||
sock = None
|
||||
try:
|
||||
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
|
||||
packed = _struct.pack('256s', iface_name.encode()[:15])
|
||||
# SIOCGIFADDR = 0x8915
|
||||
result = _fcntl.ioctl(sock.fileno(), 0x8915, packed)
|
||||
ip_raw = result[20:24]
|
||||
inet_addr = InetAddress(iface_name, bytes(ip_raw))
|
||||
addrs.append(inet_addr)
|
||||
|
||||
# SIOCGIFNETMASK = 0x891b
|
||||
prefix = 24
|
||||
try:
|
||||
result = _fcntl.ioctl(sock.fileno(), 0x891b, packed)
|
||||
netmask_str = _socket.inet_ntoa(result[20:24])
|
||||
prefix = sum(bin(int(b)).count('1') for b in netmask_str.split('.'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# SIOCGIFBRDADDR = 0x8919 (广播地址)
|
||||
broadcast_addr = None
|
||||
try:
|
||||
result = _fcntl.ioctl(sock.fileno(), 0x8919, packed)
|
||||
bcast_raw = result[20:24]
|
||||
broadcast_addr = InetAddress(iface_name, bytes(bcast_raw))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
interface_addrs.append(InterfaceAddress(
|
||||
address=inet_addr,
|
||||
broadcast=broadcast_addr,
|
||||
prefix_length=prefix
|
||||
))
|
||||
except OSError:
|
||||
pass # 接口没有 IPv4 地址
|
||||
finally:
|
||||
if sock is not None:
|
||||
try:
|
||||
addr = addr_info["addr"]
|
||||
if "." in addr:
|
||||
addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET, addr)))
|
||||
else:
|
||||
addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET6, addr)))
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
NetworkInterface._cache[iface_name] = NetworkInterface(iface_name, iface_name, addrs)
|
||||
|
||||
# --- IPv6 地址 (/proc/net/if_inet6) ---
|
||||
try:
|
||||
with open("/proc/net/if_inet6", "r") as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) >= 6 and parts[5] == iface_name:
|
||||
hex_addr = parts[0]
|
||||
raw = bytes.fromhex(hex_addr)
|
||||
prefix6 = int(parts[2], 16) if parts[2] else 64
|
||||
inet6_addr = InetAddress(iface_name, raw)
|
||||
addrs.append(inet6_addr)
|
||||
interface_addrs.append(InterfaceAddress(
|
||||
address=inet6_addr,
|
||||
broadcast=None,
|
||||
prefix_length=prefix6
|
||||
))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ni = NetworkInterface(iface_name, iface_name, addrs)
|
||||
ni._mac = mac
|
||||
ni._mtu = mtu
|
||||
ni._state = state
|
||||
ni._flags = flags
|
||||
ni._interface_addrs = interface_addrs
|
||||
NetworkInterface._cache[iface_name] = ni
|
||||
|
||||
|
||||
# 初始化缓存
|
||||
try:
|
||||
NetworkInterface._init_from_socket()
|
||||
NetworkInterface._ensure_init()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -148,6 +148,240 @@ class Enumeration(Stub):
|
||||
return Iterator(self._it)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Stream / Spliterator helpers (lightweight, stdlib-backed)
|
||||
# ============================================================
|
||||
|
||||
class _Spliterator(Stub):
|
||||
"""Lightweight java.util.Spliterator backed by a Python sequence."""
|
||||
ORDERED = 0x00000010
|
||||
DISTINCT = 0x00000001
|
||||
SORTED = 0x00000004
|
||||
SIZED = 0x00000040
|
||||
NONNULL = 0x00000100
|
||||
IMMUTABLE = 0x00000400
|
||||
CONCURRENT = 0x00001000
|
||||
SUBSIZED = 0x00004000
|
||||
|
||||
def __init__(self, data, characteristics: int = 0):
|
||||
super().__init__()
|
||||
self._data = list(data)
|
||||
self._index = 0
|
||||
self._characteristics = (characteristics
|
||||
| _Spliterator.SIZED
|
||||
| _Spliterator.SUBSIZED)
|
||||
|
||||
def tryAdvance(self, action) -> bool:
|
||||
if self._index < len(self._data):
|
||||
action(self._data[self._index])
|
||||
self._index += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def forEachRemaining(self, action) -> None:
|
||||
while self._index < len(self._data):
|
||||
action(self._data[self._index])
|
||||
self._index += 1
|
||||
|
||||
def trySplit(self):
|
||||
mid = (self._index + len(self._data)) // 2
|
||||
if self._index >= mid:
|
||||
return None
|
||||
split = _Spliterator(self._data[self._index:mid],
|
||||
self._characteristics)
|
||||
self._index = mid
|
||||
return split
|
||||
|
||||
def estimateSize(self) -> int:
|
||||
return len(self._data) - self._index
|
||||
|
||||
def getExactSizeIfKnown(self) -> int:
|
||||
return self.estimateSize()
|
||||
|
||||
def characteristics(self) -> int:
|
||||
return self._characteristics
|
||||
|
||||
def hasCharacteristics(self, characteristics: int) -> bool:
|
||||
return (self._characteristics & characteristics) == characteristics
|
||||
|
||||
def iterator(self) -> Iterator:
|
||||
return Iterator(self._data[self._index:])
|
||||
|
||||
|
||||
class _Stream(Stub):
|
||||
"""Lightweight java.util.stream.Stream wrapper over a Python list."""
|
||||
def __init__(self, data):
|
||||
super().__init__()
|
||||
self._data = list(data)
|
||||
self._parallel = False
|
||||
|
||||
def iterator(self) -> Iterator:
|
||||
return Iterator(self._data)
|
||||
|
||||
def spliterator(self) -> _Spliterator:
|
||||
return _Spliterator(self._data)
|
||||
|
||||
def isParallel(self) -> bool:
|
||||
return self._parallel
|
||||
|
||||
def parallel(self) -> '_Stream':
|
||||
self._parallel = True
|
||||
return self
|
||||
|
||||
def sequential(self) -> '_Stream':
|
||||
self._parallel = False
|
||||
return self
|
||||
|
||||
def unordered(self) -> '_Stream':
|
||||
return self
|
||||
|
||||
def filter(self, predicate) -> '_Stream':
|
||||
return _Stream([x for x in self._data if predicate(x)])
|
||||
|
||||
def map(self, mapper) -> '_Stream':
|
||||
return _Stream([mapper(x) for x in self._data])
|
||||
|
||||
def mapToInt(self, mapper) -> '_Stream':
|
||||
return _Stream([mapper(x) for x in self._data])
|
||||
|
||||
def mapToLong(self, mapper) -> '_Stream':
|
||||
return _Stream([mapper(x) for x in self._data])
|
||||
|
||||
def mapToDouble(self, mapper) -> '_Stream':
|
||||
return _Stream([mapper(x) for x in self._data])
|
||||
|
||||
def flatMap(self, mapper) -> '_Stream':
|
||||
result = []
|
||||
for x in self._data:
|
||||
part = mapper(x)
|
||||
if isinstance(part, (list, tuple)):
|
||||
result.extend(part)
|
||||
else:
|
||||
result.extend(list(part))
|
||||
return _Stream(result)
|
||||
|
||||
def peek(self, action) -> '_Stream':
|
||||
for x in self._data:
|
||||
action(x)
|
||||
return self
|
||||
|
||||
def distinct(self) -> '_Stream':
|
||||
seen = []
|
||||
seen_keys = set()
|
||||
for x in self._data:
|
||||
key = x if isinstance(x, (int, str, tuple, frozenset)) else id(x)
|
||||
if key not in seen_keys:
|
||||
seen_keys.add(key)
|
||||
seen.append(x)
|
||||
return _Stream(seen)
|
||||
|
||||
def sorted(self, key=None, reverse: bool = False) -> '_Stream':
|
||||
return _Stream(sorted(self._data, key=key, reverse=reverse))
|
||||
|
||||
def limit(self, maxSize: int) -> '_Stream':
|
||||
return _Stream(self._data[:maxSize])
|
||||
|
||||
def skip(self, n: int) -> '_Stream':
|
||||
return _Stream(self._data[n:])
|
||||
|
||||
def forEach(self, action) -> None:
|
||||
for x in self._data:
|
||||
action(x)
|
||||
|
||||
def forEachOrdered(self, action) -> None:
|
||||
for x in self._data:
|
||||
action(x)
|
||||
|
||||
def toArray(self, generator=None):
|
||||
if generator is None:
|
||||
return list(self._data)
|
||||
return list(self._data)
|
||||
|
||||
def reduce(self, *args):
|
||||
if len(args) == 1:
|
||||
accumulator = args[0]
|
||||
it = iter(self._data)
|
||||
try:
|
||||
acc = next(it)
|
||||
except StopIteration:
|
||||
return Optional.empty()
|
||||
for x in it:
|
||||
acc = accumulator(acc, x)
|
||||
return Optional.of(acc)
|
||||
elif len(args) == 2:
|
||||
identity, accumulator = args
|
||||
acc = identity
|
||||
for x in self._data:
|
||||
acc = accumulator(acc, x)
|
||||
return acc
|
||||
else:
|
||||
identity, accumulator, combiner = args
|
||||
acc = identity
|
||||
for x in self._data:
|
||||
acc = accumulator(acc, x)
|
||||
return acc
|
||||
|
||||
def collect(self, *args):
|
||||
if not args:
|
||||
return list(self._data)
|
||||
if len(args) == 1:
|
||||
collector = args[0]
|
||||
supplier = getattr(collector, 'supplier', None)
|
||||
accumulator = getattr(collector, 'accumulator', None)
|
||||
finisher = getattr(collector, 'finisher', None)
|
||||
if supplier is not None and accumulator is not None:
|
||||
container = supplier() if callable(supplier) else supplier
|
||||
for x in self._data:
|
||||
if callable(accumulator):
|
||||
accumulator(container, x)
|
||||
if finisher is not None and callable(finisher):
|
||||
return finisher(container)
|
||||
return container
|
||||
if callable(collector):
|
||||
return collector(list(self._data))
|
||||
return list(self._data)
|
||||
supplier, accumulator, combiner = args[0], args[1], args[2]
|
||||
container = supplier() if callable(supplier) else supplier
|
||||
for x in self._data:
|
||||
if callable(accumulator):
|
||||
accumulator(container, x)
|
||||
return container
|
||||
|
||||
def count(self) -> int:
|
||||
return len(self._data)
|
||||
|
||||
def findFirst(self):
|
||||
return Optional.of(self._data[0]) if self._data else Optional.empty()
|
||||
|
||||
def findAny(self):
|
||||
return self.findFirst()
|
||||
|
||||
def anyMatch(self, predicate) -> bool:
|
||||
return any(predicate(x) for x in self._data)
|
||||
|
||||
def allMatch(self, predicate) -> bool:
|
||||
return all(predicate(x) for x in self._data)
|
||||
|
||||
def noneMatch(self, predicate) -> bool:
|
||||
return not any(predicate(x) for x in self._data)
|
||||
|
||||
def min(self, key=None):
|
||||
if not self._data:
|
||||
return Optional.empty()
|
||||
return Optional.of(min(self._data, key=key) if key else min(self._data))
|
||||
|
||||
def max(self, key=None):
|
||||
if not self._data:
|
||||
return Optional.empty()
|
||||
return Optional.of(max(self._data, key=key) if key else max(self._data))
|
||||
|
||||
def onClose(self, close_handler) -> '_Stream':
|
||||
return self
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ArrayList
|
||||
# ============================================================
|
||||
@@ -294,6 +528,12 @@ class ArrayList(Stub):
|
||||
def replaceAll(self, operator) -> None:
|
||||
self._data = [operator(x) for x in self._data]
|
||||
|
||||
def spliterator(self) -> _Spliterator:
|
||||
return _Spliterator(self._data, _Spliterator.ORDERED)
|
||||
|
||||
def stream(self) -> _Stream:
|
||||
return _Stream(self._data)
|
||||
|
||||
def equals(self, other) -> bool:
|
||||
if isinstance(other, ArrayList):
|
||||
return self._data == other._data
|
||||
@@ -789,6 +1029,10 @@ class LinkedHashMap(HashMap):
|
||||
key = next(iter(self._data))
|
||||
return (key, self._data[key])
|
||||
|
||||
def clone(self) -> 'LinkedHashMap':
|
||||
result = LinkedHashMap(self._data, 0.75, self._access_order)
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# TreeMap
|
||||
@@ -993,6 +1237,48 @@ class TreeMap(Stub):
|
||||
return True
|
||||
return False
|
||||
|
||||
def replaceAll(self, function) -> None:
|
||||
for k in list(self._data.keys()):
|
||||
self._data[k] = function(k, self._data[k])
|
||||
|
||||
def compute(self, key, remapping_function):
|
||||
old = self._data.get(key)
|
||||
new_val = remapping_function(key, old)
|
||||
if new_val is not None:
|
||||
self._data[key] = new_val
|
||||
return new_val
|
||||
self._data.pop(key, None)
|
||||
return None
|
||||
|
||||
def computeIfAbsent(self, key, mapping_function):
|
||||
if key not in self._data:
|
||||
new_val = mapping_function(key)
|
||||
if new_val is not None:
|
||||
self._data[key] = new_val
|
||||
return new_val
|
||||
return self._data.get(key)
|
||||
|
||||
def computeIfPresent(self, key, remapping_function):
|
||||
if key in self._data:
|
||||
new_val = remapping_function(key, self._data[key])
|
||||
if new_val is not None:
|
||||
self._data[key] = new_val
|
||||
return new_val
|
||||
self._data.pop(key, None)
|
||||
return None
|
||||
|
||||
def merge(self, key, value, remapping_function):
|
||||
old = self._data.get(key)
|
||||
if old is None:
|
||||
self._data[key] = value
|
||||
return value
|
||||
new_val = remapping_function(old, value)
|
||||
if new_val is not None:
|
||||
self._data[key] = new_val
|
||||
return new_val
|
||||
self._data.pop(key, None)
|
||||
return None
|
||||
|
||||
def size(self) -> int:
|
||||
return len(self._data)
|
||||
|
||||
@@ -1140,6 +1426,19 @@ class HashSet(Stub):
|
||||
for item in list(self._data):
|
||||
action(item)
|
||||
|
||||
def removeIf(self, predicate) -> bool:
|
||||
old_size = len(self._data)
|
||||
to_remove = [x for x in self._data if predicate(x)]
|
||||
for x in to_remove:
|
||||
self._data.discard(x)
|
||||
return len(self._data) != old_size
|
||||
|
||||
def spliterator(self) -> _Spliterator:
|
||||
return _Spliterator(self._data, _Spliterator.DISTINCT)
|
||||
|
||||
def stream(self) -> _Stream:
|
||||
return _Stream(self._data)
|
||||
|
||||
def equals(self, other) -> bool:
|
||||
if isinstance(other, HashSet):
|
||||
return self._data == other._data
|
||||
@@ -1848,6 +2147,60 @@ class Collections:
|
||||
|
||||
return _LifoQueue(deque)
|
||||
|
||||
@staticmethod
|
||||
def addAll(collection, *elements):
|
||||
for e in elements:
|
||||
collection.add(e)
|
||||
return len(elements) > 0
|
||||
|
||||
@staticmethod
|
||||
def emptyList():
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def emptySet():
|
||||
return set()
|
||||
|
||||
@staticmethod
|
||||
def emptyMap():
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def reverseOrder(comparator=None):
|
||||
if comparator is None:
|
||||
return lambda a, b: -1 if a < b else (1 if a > b else 0)
|
||||
return lambda a, b: -comparator(a, b)
|
||||
|
||||
@staticmethod
|
||||
def singletonSet(element):
|
||||
s = HashSet()
|
||||
s.add(element)
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def checkedList(lst, type_):
|
||||
return lst
|
||||
|
||||
@staticmethod
|
||||
def checkedSet(s, type_):
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def checkedMap(m, keyType, valueType):
|
||||
return m
|
||||
|
||||
@staticmethod
|
||||
def unmodifiableCollection(c):
|
||||
return c
|
||||
|
||||
@staticmethod
|
||||
def unmodifiableQueue(q):
|
||||
return q
|
||||
|
||||
@staticmethod
|
||||
def unmodifiableDeque(d):
|
||||
return d
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Arrays
|
||||
@@ -2334,6 +2687,37 @@ class Calendar(Stub):
|
||||
def getInstance() -> 'GregorianCalendar':
|
||||
return GregorianCalendar()
|
||||
|
||||
def clear(self, field=None):
|
||||
if field is None:
|
||||
self._fields = {}
|
||||
else:
|
||||
if field in self._fields:
|
||||
del self._fields[field]
|
||||
|
||||
def isSet(self, field):
|
||||
return field in self._fields
|
||||
|
||||
def getCalendarType(self):
|
||||
return "gregory"
|
||||
|
||||
@staticmethod
|
||||
def getAvailableLocales():
|
||||
from .util import Locale
|
||||
return [Locale.US, Locale.CHINA, Locale.JAPAN, Locale.KOREA, Locale.GERMANY, Locale.FRANCE]
|
||||
|
||||
def toInstant(self):
|
||||
from .time import Instant
|
||||
return Instant.ofEpochMilli(self.getTimeInMillis())
|
||||
|
||||
def getWeekYear(self):
|
||||
return self.get(Calendar.YEAR)
|
||||
|
||||
def isWeekDateSupported(self):
|
||||
return False
|
||||
|
||||
def getWeeksInWeekYear(self):
|
||||
return 52
|
||||
|
||||
|
||||
class GregorianCalendar(Calendar):
|
||||
"""java.util.GregorianCalendar"""
|
||||
@@ -3360,6 +3744,18 @@ class PriorityQueue(Stub):
|
||||
for item in list(self._heap):
|
||||
action(item)
|
||||
|
||||
def spliterator(self) -> _Spliterator:
|
||||
if self._comparator:
|
||||
items = [v[1] for v in self._heap]
|
||||
else:
|
||||
items = list(self._heap)
|
||||
return _Spliterator(items)
|
||||
|
||||
def clone(self) -> 'PriorityQueue':
|
||||
result = PriorityQueue(comparator=self._comparator)
|
||||
result._heap = list(self._heap)
|
||||
return result
|
||||
|
||||
def toString(self) -> str:
|
||||
items = ", ".join(str(x) for x in self._heap)
|
||||
return f"[{items}]"
|
||||
|
||||
@@ -115,6 +115,41 @@ class JSONObject(Stub):
|
||||
self._map[k] = arr
|
||||
return self
|
||||
|
||||
def append(self, key, value):
|
||||
"""Append value to the JSONArray under key.
|
||||
|
||||
If the key is not present a new JSONArray containing value is created.
|
||||
If the key maps to a JSONArray the value is appended to it.
|
||||
Otherwise the existing value and the new value are placed into a new
|
||||
JSONArray.
|
||||
"""
|
||||
k = str(key)
|
||||
current = self._map.get(k)
|
||||
if isinstance(current, JSONArray):
|
||||
current.put(value)
|
||||
else:
|
||||
arr = JSONArray()
|
||||
if current is not None and current is not JSONObject.NULL:
|
||||
arr.put(current)
|
||||
arr.put(value)
|
||||
self._map[k] = arr
|
||||
return self
|
||||
|
||||
def putOnce(self, key, value):
|
||||
"""Put a key/value pair only if the key is not already present."""
|
||||
if key is None:
|
||||
raise JSONException("Null key.")
|
||||
k = str(key)
|
||||
if k not in self._map:
|
||||
self._map[k] = JSONObject._wrap(value)
|
||||
return self
|
||||
|
||||
def putOpt(self, key, value):
|
||||
"""Put a key/value pair, but only if key and value are both non-null."""
|
||||
if key is not None and value is not None:
|
||||
return self.put(key, value)
|
||||
return self
|
||||
|
||||
def get(self, key):
|
||||
k = str(key)
|
||||
if k not in self._map:
|
||||
@@ -239,6 +274,14 @@ class JSONObject(Stub):
|
||||
def has(self, key):
|
||||
return str(key) in self._map
|
||||
|
||||
def isNull(self, key):
|
||||
"""Returns true if this object has no mapping for key or if it has a
|
||||
mapping whose value is JSONObject.NULL."""
|
||||
k = str(key)
|
||||
if k not in self._map:
|
||||
return True
|
||||
return self._map[k] is JSONObject.NULL
|
||||
|
||||
def remove(self, key):
|
||||
k = str(key)
|
||||
if k in self._map:
|
||||
@@ -255,6 +298,15 @@ class JSONObject(Stub):
|
||||
arr.put(k)
|
||||
return arr
|
||||
|
||||
@staticmethod
|
||||
def getNames(jsonObject):
|
||||
"""Returns an array of field names, or None if the object is None/empty."""
|
||||
if jsonObject is None:
|
||||
return None
|
||||
if not jsonObject._map:
|
||||
return None
|
||||
return [str(k) for k in jsonObject._map.keys()]
|
||||
|
||||
def length(self):
|
||||
return len(self._map)
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
from setuptools import setup, find_packages
|
||||
from pathlib import Path
|
||||
|
||||
long_description = Path("README.md").read_text(encoding="utf-8")
|
||||
|
||||
setup(
|
||||
name="android-pyapi",
|
||||
version="1.0.0",
|
||||
description="Android API for Python — 在桌面/Linux 上原生运行 Android API",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
license="MIT",
|
||||
author="shuiyu1123",
|
||||
url="https://gitee.com/shuiyu1123/Android-pyApi",
|
||||
packages=find_packages(),
|
||||
python_requires=">=3.8",
|
||||
install_requires=[
|
||||
# 核心依赖(全部可选,缺失时自动降级)
|
||||
],
|
||||
extras_require={
|
||||
"linux": [
|
||||
"psutil",
|
||||
"requests",
|
||||
"Pillow",
|
||||
"opencv-python",
|
||||
"numpy",
|
||||
],
|
||||
"windows": [
|
||||
"psutil",
|
||||
"requests",
|
||||
"Pillow",
|
||||
"opencv-python",
|
||||
"numpy",
|
||||
"pyautogui",
|
||||
"pyperclip",
|
||||
"screeninfo",
|
||||
"pywin32",
|
||||
"wmi",
|
||||
"pycaw",
|
||||
"comtypes",
|
||||
"win10toast",
|
||||
],
|
||||
"dev": [
|
||||
"pytest",
|
||||
"pytest-cov",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: System :: Hardware",
|
||||
],
|
||||
)
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Android API 综合测试脚本
|
||||
测试 Android-pyApi 的核心 API 功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from android_pyapi import (
|
||||
Intent, Uri, Bundle, ComponentName,
|
||||
ArrayList, HashMap, HashSet, JSONObject, JSONArray,
|
||||
String, StringBuilder, Integer, Math, System, Thread,
|
||||
File, FileInputStream, FileOutputStream,
|
||||
Log, Base64, Scanner,
|
||||
Calendar, Date, SimpleDateFormat,
|
||||
)
|
||||
|
||||
|
||||
def test_intent():
|
||||
"""测试 Intent"""
|
||||
print("\n[Intent 测试]")
|
||||
intent = Intent("android.intent.action.VIEW")
|
||||
intent.setData(Uri.parse("https://example.com"))
|
||||
intent.putExtra("key1", "value1")
|
||||
intent.putExtra("key2", 42)
|
||||
print(f" Action: {intent.getAction()}")
|
||||
print(f" Data: {intent.getData()}")
|
||||
print(f" Extra String: {intent.getStringExtra('key1')}")
|
||||
print(f" Extra Int: {intent.getIntExtra('key2', 0)}")
|
||||
|
||||
|
||||
def test_collections():
|
||||
"""测试集合类"""
|
||||
print("\n[集合类测试]")
|
||||
lst = ArrayList()
|
||||
lst.add("Apple")
|
||||
lst.add("Banana")
|
||||
lst.add("Cherry")
|
||||
print(f" ArrayList size: {lst.size()}")
|
||||
print(f" First: {lst.get(0)}")
|
||||
|
||||
m = HashMap()
|
||||
m.put("name", "Android-pyApi")
|
||||
m.put("version", "1.0.0")
|
||||
print(f" HashMap get: {m.get('name')}")
|
||||
|
||||
json = JSONObject()
|
||||
json.put("platform", "Windows")
|
||||
json.put("ok", True)
|
||||
print(f" JSONObject: {json.toString()}")
|
||||
|
||||
|
||||
def test_string():
|
||||
"""测试字符串类"""
|
||||
print("\n[字符串测试]")
|
||||
s = String("Hello, Android-pyApi!")
|
||||
print(f" Length: {s.length()}")
|
||||
print(f" Upper: {s.toUpperCase()}")
|
||||
print(f" Contains 'Android': {s.contains('Android')}")
|
||||
|
||||
sb = StringBuilder()
|
||||
sb.append("Hello")
|
||||
sb.append(", World!")
|
||||
print(f" StringBuilder: {sb.toString()}")
|
||||
|
||||
|
||||
def test_file():
|
||||
"""测试文件类"""
|
||||
print("\n[文件测试]")
|
||||
import tempfile
|
||||
temp_file = os.path.join(tempfile.gettempdir(), "android_pyapi_test.txt")
|
||||
|
||||
f = File(temp_file)
|
||||
print(f" Path: {f.getAbsolutePath()}")
|
||||
|
||||
with open(temp_file, 'w') as file:
|
||||
file.write("Android-pyApi test")
|
||||
|
||||
print(f" Exists: {f.exists()}")
|
||||
print(f" Name: {f.getName()}")
|
||||
print(f" Size: {f.length()} bytes")
|
||||
|
||||
os.remove(temp_file)
|
||||
print(f" After delete: {f.exists()}")
|
||||
|
||||
|
||||
def test_base64():
|
||||
"""测试 Base64"""
|
||||
print("\n[Base64 测试]")
|
||||
text = "Hello, Android-pyApi!"
|
||||
encoded = Base64.encodeToString(text.encode('utf-8'))
|
||||
decoded = Base64.decode(encoded).decode('utf-8')
|
||||
print(f" Original: {text}")
|
||||
print(f" Encoded: {encoded}")
|
||||
print(f" Decoded: {decoded}")
|
||||
print(f" Match: {text == decoded}")
|
||||
|
||||
|
||||
def test_math():
|
||||
"""测试数学类"""
|
||||
print("\n[数学测试]")
|
||||
print(f" PI: {Math.PI}")
|
||||
print(f" abs(-42): {Math.abs(-42)}")
|
||||
print(f" max(10, 20): {Math.max(10, 20)}")
|
||||
print(f" sqrt(144): {Math.sqrt(144)}")
|
||||
print(f" pow(2, 10): {Math.pow(2, 10)}")
|
||||
|
||||
|
||||
def test_calendar():
|
||||
"""测试日历类"""
|
||||
print("\n[日历测试]")
|
||||
cal = Calendar.getInstance()
|
||||
print(f" Year: {cal.get(Calendar.YEAR)}")
|
||||
print(f" Month: {cal.get(Calendar.MONTH) + 1}")
|
||||
print(f" Day: {cal.get(Calendar.DAY_OF_MONTH)}")
|
||||
print(f" Hour: {cal.get(Calendar.HOUR_OF_DAY)}")
|
||||
print(f" Minute: {cal.get(Calendar.MINUTE)}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("=" * 60)
|
||||
print("Android-pyApi 综合测试")
|
||||
print("=" * 60)
|
||||
|
||||
test_intent()
|
||||
test_collections()
|
||||
test_string()
|
||||
test_file()
|
||||
test_base64()
|
||||
test_math()
|
||||
test_calendar()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有测试完成!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,400 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
扩展 Android API 测试脚本
|
||||
测试 android.os, android.content, android.app, android.widget, android.graphics 等模块
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
def test_os_module():
|
||||
"""测试 android.os 模块"""
|
||||
print("\n" + "=" * 60)
|
||||
print("android.os 模块测试")
|
||||
print("=" * 60)
|
||||
|
||||
from android_pyapi.android.os import (
|
||||
Build, SystemClock, Environment, Process,
|
||||
Bundle, Handler, Looper, Message,
|
||||
BatteryManager, PowerManager, Vibrator,
|
||||
AsyncTask, CountDownTimer, Messenger,
|
||||
FileObserver, FileUtils, PatternMatcher,
|
||||
)
|
||||
|
||||
# Build 测试
|
||||
print("\n[Build 设备信息]")
|
||||
print(f" MODEL: {Build.MODEL}")
|
||||
print(f" MANUFACTURER: {Build.MANUFACTURER}")
|
||||
print(f" BRAND: {Build.BRAND}")
|
||||
print(f" DEVICE: {Build.DEVICE}")
|
||||
print(f" FINGERPRINT: {Build.FINGERPRINT}")
|
||||
print(f" SDK_INT: {Build.VERSION.SDK_INT}")
|
||||
print(f" RELEASE: {Build.VERSION.RELEASE}")
|
||||
|
||||
# SystemClock 测试
|
||||
print("\n[SystemClock 系统时钟]")
|
||||
print(f" 启动时间: {SystemClock.elapsedRealtime()} ms")
|
||||
print(f" 运行时间: {SystemClock.uptimeMillis()} ms")
|
||||
|
||||
# Environment 测试
|
||||
print("\n[Environment 环境目录]")
|
||||
print(f" 外部存储: {Environment.getExternalStorageDirectory()}")
|
||||
print(f" 下载目录: {Environment.getExternalStoragePublicDirectory('Download')}")
|
||||
print(f" 数据目录: {Environment.getDataDirectory()}")
|
||||
print(f" 根目录: {Environment.getRootDirectory()}")
|
||||
|
||||
# Process 测试
|
||||
print("\n[Process 进程信息]")
|
||||
print(f" PID: {Process.myPid()}")
|
||||
print(f" TID: {Process.myTid()}")
|
||||
|
||||
# Bundle 测试
|
||||
print("\n[Bundle 数据容器]")
|
||||
bundle = Bundle()
|
||||
bundle.putString("name", "Android-pyApi")
|
||||
bundle.putInt("version", 100)
|
||||
bundle.putBoolean("windows", True)
|
||||
print(f" name: {bundle.getString('name')}")
|
||||
print(f" version: {bundle.getInt('version')}")
|
||||
print(f" windows: {bundle.getBoolean('windows')}")
|
||||
print(f" size: {bundle.size()}")
|
||||
|
||||
# BatteryManager 测试
|
||||
print("\n[BatteryManager 电池管理]")
|
||||
bm = BatteryManager()
|
||||
print(f" 电量: {bm.getLevel()}%")
|
||||
print(f" 充电: {bm.isCharging()}")
|
||||
print(f" 存在: {bm.isPresent()}")
|
||||
print(f" 技术: {bm.getTechnology()}")
|
||||
|
||||
# PowerManager 测试
|
||||
print("\n[PowerManager 电源管理]")
|
||||
pm = PowerManager()
|
||||
print(f" 交互: {pm.isInteractive()}")
|
||||
print(f" 屏幕: {pm.isScreenOn()}")
|
||||
|
||||
# Vibrator 测试
|
||||
print("\n[Vibrator 振动器]")
|
||||
vib = Vibrator()
|
||||
print(f" 有振动器: {vib.hasVibrator()}")
|
||||
|
||||
# PatternMatcher 测试
|
||||
print("\n[PatternMatcher 模式匹配]")
|
||||
pm_literal = PatternMatcher("/sdcard/test.txt", PatternMatcher.PATTERN_LITERAL)
|
||||
pm_prefix = PatternMatcher("/sdcard/", PatternMatcher.PATTERN_PREFIX)
|
||||
print(f" literal match: {pm_literal.match('/sdcard/test.txt')}")
|
||||
print(f" prefix match: {pm_prefix.match('/sdcard/test.txt')}")
|
||||
|
||||
# FileUtils 测试
|
||||
print("\n[FileUtils 文件工具]")
|
||||
test_file = os.path.join(os.environ.get('TEMP', '/tmp'), 'android_pyapi_test.txt')
|
||||
FileUtils.stringToFile(test_file, "Hello, Android-pyApi!")
|
||||
content = FileUtils.readFileToString(test_file)
|
||||
print(f" 写入读取: {content}")
|
||||
os.remove(test_file)
|
||||
|
||||
|
||||
def test_content_module():
|
||||
"""测试 android.content 模块"""
|
||||
print("\n" + "=" * 60)
|
||||
print("android.content 模块测试")
|
||||
print("=" * 60)
|
||||
|
||||
from android_pyapi.android.content import (
|
||||
Uri, ContentValues, ContentResolver, Context,
|
||||
SharedPreferences, IntentFilter, BroadcastReceiver,
|
||||
ComponentName, ClipData, ClipDescription,
|
||||
ContentProvider, ContentProviderClient,
|
||||
ClipboardManager,
|
||||
)
|
||||
|
||||
# Uri 测试
|
||||
print("\n[Uri 统一资源标识符]")
|
||||
uri = Uri.parse("https://example.com/path?query=1&foo=bar#fragment")
|
||||
print(f" scheme: {uri.getScheme()}")
|
||||
print(f" host: {uri.getHost()}")
|
||||
print(f" path: {uri.getPath()}")
|
||||
print(f" query: {uri.getQuery()}")
|
||||
print(f" fragment: {uri.getFragment()}")
|
||||
print(f" toString: {uri.toString()}")
|
||||
|
||||
# ContentValues 测试
|
||||
print("\n[ContentValues 内容值]")
|
||||
cv = ContentValues()
|
||||
cv.put("title", "Test")
|
||||
cv.put("value", 42)
|
||||
cv.put("flag", True)
|
||||
print(f" title: {cv.getAsString('title')}")
|
||||
print(f" value: {cv.getAsInteger('value')}")
|
||||
print(f" flag: {cv.getAsBoolean('flag')}")
|
||||
print(f" size: {cv.size()}")
|
||||
|
||||
# Context 测试
|
||||
print("\n[Context 应用上下文]")
|
||||
ctx = Context()
|
||||
print(f" packageName: {ctx.getPackageName()}")
|
||||
print(f" filesDir: {ctx.getFilesDir()}")
|
||||
print(f" cacheDir: {ctx.getCacheDir()}")
|
||||
print(f" dataDir: {ctx.getDataDir()}")
|
||||
|
||||
# SharedPreferences 测试
|
||||
print("\n[SharedPreferences 持久化存储]")
|
||||
sp = ctx.getSharedPreferences("test_prefs")
|
||||
editor = sp.edit()
|
||||
editor.putString("username", "test_user")
|
||||
editor.putInt("login_count", 5)
|
||||
editor.putBoolean("remember", True)
|
||||
editor.commit()
|
||||
print(f" username: {sp.getString('username')}")
|
||||
print(f" login_count: {sp.getInt('login_count')}")
|
||||
print(f" remember: {sp.getBoolean('remember')}")
|
||||
|
||||
# ComponentName 测试
|
||||
print("\n[ComponentName 组件名]")
|
||||
cn = ComponentName("com.example.app", "com.example.app.MainActivity")
|
||||
print(f" package: {cn.getPackageName()}")
|
||||
print(f" class: {cn.getClassName()}")
|
||||
print(f" short: {cn.getShortClassName()}")
|
||||
print(f" flatten: {cn.flattenToString()}")
|
||||
|
||||
# ClipData 测试
|
||||
print("\n[ClipData 剪贴板数据]")
|
||||
clip = ClipData.newPlainText("label", "Hello, Clipboard!")
|
||||
item = clip.getItemAt(0)
|
||||
print(f" text: {item.getText()}")
|
||||
print(f" itemCount: {clip.getItemCount()}")
|
||||
|
||||
# ClipboardManager 测试
|
||||
print("\n[ClipboardManager 剪贴板管理]")
|
||||
cm = ClipboardManager()
|
||||
cm.setText("Test Clipboard")
|
||||
print(f" getText: {cm.getText()}")
|
||||
print(f" hasText: {cm.hasText()}")
|
||||
|
||||
|
||||
def test_app_module():
|
||||
"""测试 android.app 模块"""
|
||||
print("\n" + "=" * 60)
|
||||
print("android.app 模块测试")
|
||||
print("=" * 60)
|
||||
|
||||
from android_pyapi.android.app import (
|
||||
Intent, Activity, Service,
|
||||
Notification, NotificationManager,
|
||||
AlarmManager, ActivityManager,
|
||||
)
|
||||
|
||||
# Intent 测试
|
||||
print("\n[Intent 意图]")
|
||||
intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setData("https://example.com")
|
||||
intent.putExtra("title", "Test")
|
||||
intent.putExtra("count", 42)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
print(f" action: {intent.getAction()}")
|
||||
print(f" data: {intent.getData()}")
|
||||
print(f" title: {intent.getStringExtra('title')}")
|
||||
print(f" count: {intent.getIntExtra('count')}")
|
||||
print(f" flags: {hex(intent.getFlags())}")
|
||||
|
||||
# Notification 测试
|
||||
print("\n[Notification 通知]")
|
||||
notif = Notification()
|
||||
notif.title = "Test Notification"
|
||||
notif.text = "This is a test"
|
||||
print(f" title: {notif.title}")
|
||||
print(f" text: {notif.text}")
|
||||
|
||||
# NotificationManager 测试
|
||||
print("\n[NotificationManager 通知管理]")
|
||||
nm = NotificationManager()
|
||||
print(f" 类型: {type(nm).__name__}")
|
||||
|
||||
# AlarmManager 测试
|
||||
print("\n[AlarmManager 闹钟管理]")
|
||||
am = AlarmManager()
|
||||
print(f" 类型: {type(am).__name__}")
|
||||
|
||||
|
||||
def test_widget_module():
|
||||
"""测试 android.widget 模块"""
|
||||
print("\n" + "=" * 60)
|
||||
print("android.widget 模块测试")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from android_pyapi.android.widget import (
|
||||
Toast, AlertDialog, Button, TextView,
|
||||
EditText, ListView, RecyclerView,
|
||||
)
|
||||
|
||||
# Toast 测试
|
||||
print("\n[Toast 提示]")
|
||||
toast = Toast.makeText(None, "Hello, Toast!", Toast.LENGTH_SHORT)
|
||||
print(f" text: {toast._text}")
|
||||
print(f" duration: {toast._duration}")
|
||||
|
||||
# AlertDialog 测试
|
||||
print("\n[AlertDialog 对话框]")
|
||||
dialog = AlertDialog(None)
|
||||
print(f" 类型: {type(dialog).__name__}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f" widget 模块导入失败: {e}")
|
||||
|
||||
|
||||
def test_graphics_module():
|
||||
"""测试 android.graphics 模块"""
|
||||
print("\n" + "=" * 60)
|
||||
print("android.graphics 模块测试")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from android_pyapi.android.graphics import (
|
||||
Bitmap, Canvas, Paint, Color,
|
||||
Rect, RectF, Path, Matrix,
|
||||
)
|
||||
|
||||
# Color 测试
|
||||
print("\n[Color 颜色]")
|
||||
print(f" RED: {hex(Color.RED)}")
|
||||
print(f" GREEN: {hex(Color.GREEN)}")
|
||||
print(f" BLUE: {hex(Color.BLUE)}")
|
||||
print(f" WHITE: {hex(Color.WHITE)}")
|
||||
print(f" BLACK: {hex(Color.BLACK)}")
|
||||
print(f" argb(255,128,64,32): {hex(Color.argb(255, 128, 64, 32))}")
|
||||
|
||||
# Bitmap 测试
|
||||
print("\n[Bitmap 位图]")
|
||||
bm = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
|
||||
print(f" width: {bm.getWidth()}")
|
||||
print(f" height: {bm.getHeight()}")
|
||||
print(f" config: {bm.getConfig()}")
|
||||
|
||||
# Paint 测试
|
||||
print("\n[Paint 画笔]")
|
||||
paint = Paint()
|
||||
paint.setColor(Color.RED)
|
||||
paint.setTextSize(24.0)
|
||||
print(f" color: {hex(paint.getColor())}")
|
||||
print(f" textSize: {paint.getTextSize()}")
|
||||
|
||||
# Rect 测试
|
||||
print("\n[Rect 矩形]")
|
||||
rect = Rect(10, 20, 100, 200)
|
||||
print(f" left: {rect.left}")
|
||||
print(f" top: {rect.top}")
|
||||
print(f" right: {rect.right}")
|
||||
print(f" bottom: {rect.bottom}")
|
||||
print(f" width: {rect.width()}")
|
||||
print(f" height: {rect.height()}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f" graphics 模块导入失败: {e}")
|
||||
|
||||
|
||||
def test_java_module():
|
||||
"""测试 java 模块"""
|
||||
print("\n" + "=" * 60)
|
||||
print("java 模块测试")
|
||||
print("=" * 60)
|
||||
|
||||
from android_pyapi.java.lang import (
|
||||
String, StringBuilder, Integer, Math, System, Thread,
|
||||
)
|
||||
from android_pyapi.java.io import (
|
||||
File, FileInputStream, FileOutputStream,
|
||||
)
|
||||
from android_pyapi.java.util import (
|
||||
ArrayList, HashMap, HashSet,
|
||||
)
|
||||
from android_pyapi.org.json import JSONObject, JSONArray
|
||||
|
||||
# String 测试
|
||||
print("\n[String 字符串]")
|
||||
s = String("Hello, Android-pyApi!")
|
||||
print(f" length: {s.length()}")
|
||||
print(f" upper: {s.toUpperCase()}")
|
||||
print(f" contains('Android'): {s.contains('Android')}")
|
||||
print(f" substring(7): {s.substring(7)}")
|
||||
|
||||
# StringBuilder 测试
|
||||
print("\n[StringBuilder 字符串构建器]")
|
||||
sb = StringBuilder()
|
||||
sb.append("Hello")
|
||||
sb.append(", ")
|
||||
sb.append("World!")
|
||||
print(f" result: {sb.toString()}")
|
||||
print(f" length: {sb.length()}")
|
||||
|
||||
# Integer 测试
|
||||
print("\n[Integer 整数]")
|
||||
print(f" parseInt('42'): {Integer.parseInt('42')}")
|
||||
print(f" MAX_VALUE: {Integer.MAX_VALUE}")
|
||||
print(f" MIN_VALUE: {Integer.MIN_VALUE}")
|
||||
|
||||
# Math 测试
|
||||
print("\n[Math 数学]")
|
||||
print(f" PI: {Math.PI}")
|
||||
print(f" abs(-42): {Math.abs(-42)}")
|
||||
print(f" max(10, 20): {Math.max(10, 20)}")
|
||||
print(f" sqrt(144): {Math.sqrt(144)}")
|
||||
print(f" pow(2, 10): {Math.pow(2, 10)}")
|
||||
|
||||
# ArrayList 测试
|
||||
print("\n[ArrayList 列表]")
|
||||
lst = ArrayList()
|
||||
lst.add("Apple")
|
||||
lst.add("Banana")
|
||||
lst.add("Cherry")
|
||||
print(f" size: {lst.size()}")
|
||||
print(f" first: {lst.get(0)}")
|
||||
print(f" contains('Banana'): {lst.contains('Banana')}")
|
||||
|
||||
# HashMap 测试
|
||||
print("\n[HashMap 哈希表]")
|
||||
m = HashMap()
|
||||
m.put("name", "Android-pyApi")
|
||||
m.put("version", "1.0.0")
|
||||
print(f" name: {m.get('name')}")
|
||||
print(f" size: {m.size()}")
|
||||
print(f" containsKey('name'): {m.containsKey('name')}")
|
||||
|
||||
# JSONObject 测试
|
||||
print("\n[JSONObject JSON 对象]")
|
||||
json = JSONObject()
|
||||
json.put("platform", "Windows")
|
||||
json.put("python", True)
|
||||
json.put("version", 3.12)
|
||||
print(f" toString: {json.toString()}")
|
||||
print(f" platform: {json.getString('platform')}")
|
||||
|
||||
# File 测试
|
||||
print("\n[File 文件]")
|
||||
f = File(os.environ.get('TEMP', '/tmp'), "test_file.txt")
|
||||
print(f" path: {f.getAbsolutePath()}")
|
||||
print(f" name: {f.getName()}")
|
||||
print(f" parent: {f.getParent()}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("=" * 60)
|
||||
print("Android-pyApi 扩展 API 测试")
|
||||
print("=" * 60)
|
||||
|
||||
test_os_module()
|
||||
test_content_module()
|
||||
test_app_module()
|
||||
test_widget_module()
|
||||
test_graphics_module()
|
||||
test_java_module()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有测试完成!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
网络测试脚本 - 基于 Android-pyApi
|
||||
模拟 Operit 的 network_test.js 功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from android_pyapi import (
|
||||
ConnectivityManager, NetworkInfo, Intent, Uri
|
||||
)
|
||||
from android_pyapi._windows_impl import WindowsNetwork
|
||||
|
||||
|
||||
def test_network():
|
||||
"""测试网络功能"""
|
||||
print("=" * 60)
|
||||
print("Android-pyApi 网络测试")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 获取 IP 地址
|
||||
print("\n[1] IP 地址:")
|
||||
ip = WindowsNetwork.get_ip_address()
|
||||
print(f" 本机 IP: {ip}")
|
||||
|
||||
# 2. 网络连通性
|
||||
print("\n[2] 网络连通性:")
|
||||
connected = WindowsNetwork.get_connectivity()
|
||||
print(f" 连接状态: {'已连接' if connected else '未连接'}")
|
||||
|
||||
# 3. WiFi 信息
|
||||
print("\n[3] WiFi 信息:")
|
||||
wifi = WindowsNetwork.get_wifi_info()
|
||||
print(f" SSID: {wifi.get('ssid', 'N/A') or 'N/A'}")
|
||||
print(f" 信号: {wifi.get('signal', 0)}%")
|
||||
print(f" 状态: {wifi.get('status', 'unknown')}")
|
||||
|
||||
# 4. DNS 服务器
|
||||
print("\n[4] DNS 服务器:")
|
||||
dns = WindowsNetwork.get_dns_servers()
|
||||
for d in dns[:3]:
|
||||
print(f" - {d}")
|
||||
|
||||
# 5. 默认网关
|
||||
print("\n[5] 默认网关:")
|
||||
gw = WindowsNetwork.get_gateway()
|
||||
print(f" 网关: {gw}")
|
||||
|
||||
# 6. MAC 地址
|
||||
print("\n[6] MAC 地址:")
|
||||
mac = WindowsNetwork.get_mac_address()
|
||||
print(f" MAC: {mac}")
|
||||
|
||||
# 7. 网络接口
|
||||
print("\n[7] 网络接口:")
|
||||
interfaces = WindowsNetwork.get_interfaces()
|
||||
for iface in interfaces[:5]:
|
||||
print(f" - {iface.get('ifname', 'N/A')}: {iface.get('operstate', 'N/A')}")
|
||||
|
||||
# 8. 流量统计
|
||||
print("\n[8] 流量统计:")
|
||||
traffic = WindowsNetwork.get_rx_tx_bytes()
|
||||
print(f" 接收: {traffic.get('rx_bytes', 0) / 1024 / 1024:.2f} MB")
|
||||
print(f" 发送: {traffic.get('tx_bytes', 0) / 1024 / 1024:.2f} MB")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_network()
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
系统工具测试脚本 - 基于 Android-pyApi
|
||||
模拟 Operit 的 system_tools.js 功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from android_pyapi import (
|
||||
Build, PowerManager, BatteryManager, ActivityManager,
|
||||
Environment, Log
|
||||
)
|
||||
from android_pyapi._windows_impl import (
|
||||
WindowsPower, WindowsProcess, WindowsDisplay, WindowsShell
|
||||
)
|
||||
|
||||
|
||||
def test_system():
|
||||
"""测试系统工具功能"""
|
||||
print("=" * 60)
|
||||
print("Android-pyApi 系统工具测试")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 设备信息 (Build)
|
||||
print("\n[1] 设备信息 (Build):")
|
||||
print(f" MODEL: {Build.MODEL}")
|
||||
print(f" MANUFACTURER: {Build.MANUFACTURER}")
|
||||
print(f" BRAND: {Build.BRAND}")
|
||||
print(f" DEVICE: {Build.DEVICE}")
|
||||
print(f" PRODUCT: {Build.PRODUCT}")
|
||||
print(f" FINGERPRINT: {Build.FINGERPRINT}")
|
||||
|
||||
# 2. 系统信息
|
||||
print("\n[2] 系统信息:")
|
||||
sys_info = WindowsProcess.get_system_info()
|
||||
print(f" 主机名: {sys_info.get('hostname', 'N/A')}")
|
||||
print(f" 平台: {sys_info.get('platform', 'N/A')}")
|
||||
print(f" 版本: {sys_info.get('windows_version', 'N/A')}")
|
||||
print(f" 架构: {sys_info.get('arch', 'N/A')}")
|
||||
print(f" Python: {sys_info.get('python', 'N/A')}")
|
||||
if 'cpu_count' in sys_info:
|
||||
print(f" CPU 核心: {sys_info['cpu_count']}")
|
||||
if 'memory_total' in sys_info:
|
||||
print(f" 内存总量: {sys_info['memory_total'] / 1024 / 1024 / 1024:.2f} GB")
|
||||
|
||||
# 3. 电源信息
|
||||
print("\n[3] 电源信息:")
|
||||
battery = WindowsPower.get_battery_info()
|
||||
print(f" 电池存在: {battery['present']}")
|
||||
print(f" 电量: {battery['level']}%")
|
||||
print(f" 状态: {battery['status']}")
|
||||
|
||||
power = WindowsPower.get_power_status()
|
||||
print(f" 充电中: {power['is_charging']}")
|
||||
|
||||
# 4. CPU 和内存
|
||||
print("\n[4] CPU 和内存:")
|
||||
cpu = WindowsPower.get_cpu_usage()
|
||||
print(f" CPU 使用率: {cpu}%")
|
||||
|
||||
mem = WindowsPower.get_memory_info()
|
||||
print(f" 内存使用率: {mem['percent']}%")
|
||||
print(f" 可用内存: {mem['available'] / 1024 / 1024 / 1024:.2f} GB")
|
||||
|
||||
# 5. 显示信息
|
||||
print("\n[5] 显示信息:")
|
||||
w, h = WindowsDisplay.get_screen_size()
|
||||
print(f" 屏幕尺寸: {w}x{h}")
|
||||
|
||||
x, y = WindowsDisplay.get_mouse_position()
|
||||
print(f" 鼠标位置: ({x}, {y})")
|
||||
|
||||
# 6. 进程列表
|
||||
print("\n[6] 进程列表 (前10个):")
|
||||
procs = WindowsProcess.list_processes()
|
||||
for p in procs[:10]:
|
||||
print(f" - {p.get('name', 'N/A')} (PID: {p.get('pid', 'N/A')})")
|
||||
|
||||
# 7. 运行中的应用
|
||||
print("\n[7] 运行中的应用 (前5个):")
|
||||
apps = WindowsProcess.get_running_apps()
|
||||
for app in apps[:5]:
|
||||
print(f" - {app.get('name', 'N/A')}")
|
||||
|
||||
# 8. Shell 命令测试
|
||||
print("\n[8] Shell 命令测试:")
|
||||
result = WindowsShell.exec("echo Hello from Android-pyApi!")
|
||||
print(f" 输出: {result.get('stdout', 'N/A')}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_system()
|
||||
Whitespace-only changes.
+145
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Windows 实现层测试脚本
|
||||
"""
|
||||
|
||||
import sys
|
||||
import platform
|
||||
|
||||
print("=" * 60)
|
||||
print(f"平台: {platform.system()} {platform.release()}")
|
||||
print(f"Python: {platform.python_version()}")
|
||||
print("=" * 60)
|
||||
|
||||
# 测试基本导入
|
||||
print("\n[1] 测试基本导入...")
|
||||
try:
|
||||
from android_pyapi import Intent, Uri, Bundle, JSONObject, ArrayList
|
||||
print(" ✓ 基本 Android API 导入成功")
|
||||
except Exception as e:
|
||||
print(f" ✗ 基本导入失败: {e}")
|
||||
|
||||
# 测试 Intent
|
||||
print("\n[2] 测试 Intent...")
|
||||
try:
|
||||
intent = Intent("android.intent.action.VIEW")
|
||||
intent.setData(Uri.parse("https://example.com"))
|
||||
intent.putExtra("key", "value")
|
||||
print(f" ✓ Action: {intent.getAction()}")
|
||||
print(f" ✓ Data: {intent.getData()}")
|
||||
print(f" ✓ Extra: {intent.getStringExtra('key')}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Intent 测试失败: {e}")
|
||||
|
||||
# 测试集合类
|
||||
print("\n[3] 测试集合类...")
|
||||
try:
|
||||
lst = ArrayList()
|
||||
lst.add("hello")
|
||||
lst.add("world")
|
||||
print(f" ✓ ArrayList size: {lst.size()}")
|
||||
print(f" ✓ ArrayList[0]: {lst.get(0)}")
|
||||
|
||||
json = JSONObject()
|
||||
json.put("name", "test")
|
||||
json.put("version", 42)
|
||||
print(f" ✓ JSONObject: {json.toString()}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 集合类测试失败: {e}")
|
||||
|
||||
# 测试 Windows 实现层
|
||||
print("\n[4] 测试 Windows 实现层...")
|
||||
try:
|
||||
from android_pyapi._windows_impl import (
|
||||
IS_WINDOWS, get_windows_capabilities,
|
||||
WindowsNetwork, WindowsPower, WindowsDisplay,
|
||||
WindowsClipboard, WindowsProcess, WindowsLocation
|
||||
)
|
||||
print(f" ✓ IS_WINDOWS: {IS_WINDOWS}")
|
||||
|
||||
caps = get_windows_capabilities()
|
||||
print(f" ✓ 能力检测: {sum(caps.values())}/{len(caps)} 项可用")
|
||||
except Exception as e:
|
||||
print(f" ✗ Windows 实现层导入失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# 测试网络功能
|
||||
print("\n[5] 测试网络功能...")
|
||||
try:
|
||||
ip = WindowsNetwork.get_ip_address()
|
||||
print(f" ✓ IP 地址: {ip}")
|
||||
|
||||
connected = WindowsNetwork.get_connectivity()
|
||||
print(f" ✓ 网络连通: {connected}")
|
||||
|
||||
wifi = WindowsNetwork.get_wifi_info()
|
||||
print(f" ✓ WiFi SSID: {wifi.get('ssid', 'N/A')}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 网络测试失败: {e}")
|
||||
|
||||
# 测试电源信息
|
||||
print("\n[6] 测试电源信息...")
|
||||
try:
|
||||
battery = WindowsPower.get_battery_info()
|
||||
print(f" ✓ 电池存在: {battery['present']}")
|
||||
print(f" ✓ 电池电量: {battery['level']}%")
|
||||
print(f" ✓ 电池状态: {battery['status']}")
|
||||
|
||||
cpu = WindowsPower.get_cpu_usage()
|
||||
print(f" ✓ CPU 使用率: {cpu}%")
|
||||
|
||||
mem = WindowsPower.get_memory_info()
|
||||
print(f" ✓ 内存使用: {mem['percent']}%")
|
||||
except Exception as e:
|
||||
print(f" ✗ 电源测试失败: {e}")
|
||||
|
||||
# 测试显示信息
|
||||
print("\n[7] 测试显示信息...")
|
||||
try:
|
||||
w, h = WindowsDisplay.get_screen_size()
|
||||
print(f" ✓ 屏幕尺寸: {w}x{h}")
|
||||
|
||||
x, y = WindowsDisplay.get_mouse_position()
|
||||
print(f" ✓ 鼠标位置: ({x}, {y})")
|
||||
except Exception as e:
|
||||
print(f" ✗ 显示测试失败: {e}")
|
||||
|
||||
# 测试剪贴板
|
||||
print("\n[8] 测试剪贴板...")
|
||||
try:
|
||||
test_text = "Android-pyApi 测试文本"
|
||||
WindowsClipboard.write(test_text)
|
||||
read_text = WindowsClipboard.read()
|
||||
if read_text == test_text:
|
||||
print(f" ✓ 剪贴板读写成功")
|
||||
else:
|
||||
print(f" ⚠ 剪贴板内容不匹配: '{read_text}'")
|
||||
except Exception as e:
|
||||
print(f" ✗ 剪贴板测试失败: {e}")
|
||||
|
||||
# 测试进程管理
|
||||
print("\n[9] 测试进程管理...")
|
||||
try:
|
||||
procs = WindowsProcess.list_processes()
|
||||
print(f" ✓ 进程数量: {len(procs)}")
|
||||
|
||||
sys_info = WindowsProcess.get_system_info()
|
||||
print(f" ✓ 主机名: {sys_info['hostname']}")
|
||||
print(f" ✓ 系统: {sys_info['platform']} {sys_info['windows_version']}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 进程管理测试失败: {e}")
|
||||
|
||||
# 测试位置信息
|
||||
print("\n[10] 测试位置信息...")
|
||||
try:
|
||||
loc = WindowsLocation.get_location()
|
||||
print(f" ✓ 来源: {loc['source']}")
|
||||
print(f" ✓ 经纬度: {loc['latitude']}, {loc['longitude']}")
|
||||
if loc.get('city'):
|
||||
print(f" ✓ 城市: {loc['city']}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 位置测试失败: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成!")
|
||||
print("=" * 60)
|
||||
Reference in new issue
Block a user