Compare commits

...
10 Commits
Author SHA1 Message Date
shuiyu1123 dab21b6710 test: 添加扩展 API 测试脚本
- 测试 android.os 模块 (Build, SystemClock, Environment, Process, Bundle, BatteryManager, PowerManager, Vibrator, PatternMatcher, FileUtils)
- 测试 android.content 模块 (Uri, ContentValues, Context, SharedPreferences, ComponentName, ClipData, ClipboardManager)
- 测试 android.app 模块 (Intent, Notification, NotificationManager, AlarmManager)
- 测试 android.graphics 模块 (Color, Bitmap, Paint, Rect)
- 测试 java 模块 (String, StringBuilder, Integer, Math, ArrayList, HashMap, JSONObject, File)
- 所有测试通过
2026-06-26 22:10:18 +08:00
shuiyu1123 144b5ce45e test: 添加 Windows 测试脚本
- test_network.py: 网络功能测试 (IP/DNS/WiFi/流量)
- test_system.py: 系统工具测试 (设备信息/电源/进程)
- test_android_api.py: Android API 综合测试 (Intent/集合/文件/Base64/数学/日历)
- 所有测试通过
2026-06-26 21:59:36 +08:00
shuiyu1123 b3d75f8cc2 fix: 修复 Windows 兼容性问题
- 修复 setup.py 中 README.md 读取的编码问题 (UTF-8)
- 修复 system.py 中 errno 常量在 Windows 上不存在的问题
- 使用 getattr 安全获取平台特定的 errno 常量
- 测试全部通过 (24/24 能力可用)
2026-06-26 21:32:56 +08:00
shuiyu1123 0f9b6fcc5d feat: 添加 Windows 真实实现层
- 新增 _windows_impl.py,包含 14 个 Windows 实现类
- 支持网络、蓝牙、传感器、音频、剪贴板、电源、位置、通知、显示、UI自动化、摄像头、进程管理、Shell 等功能
- 更新 README.md 添加 Windows 支持说明
- 更新 setup.py 添加 Windows 专用依赖
2026-06-26 21:25:12 +08:00
shuiyu1123 97d8b277d5 fix: 系统性修复类继承冲突 + 补全36+缺失导出
类继承冲突修复 (7个文件, 8组冲突):
- view_ext.py: 16个类继承view.py基础版本 (ViewOutlineProvider, ViewPropertyAnimator, ViewTreeObserver, ViewConfiguration, ViewParent, WindowInsets, Display, DragEvent, GestureDetector, ScaleGestureDetector, SoundEffectConstants, Surface, SurfaceView, TextureView, VelocityTracker, WindowInsetsController)
- hardware_sensor_ext.py: 3个类继承hardware.py (Sensor, SensorEvent, SensorEventListener)
- app_fragment.py: 3个类继承app.py (Fragment, FragmentManager, FragmentTransaction)
- content_intent.py: 3个类继承content.py/app.py (Intent, PendingIntent, IntentFilter)
- widget_ext.py: 29个类继承widget.py (18个单继承+11个多继承, 修复super()链)
- os_power.py: 2个类继承os.py (BatteryManager, PowerManager)
- net_ext.py: 12个类继承net.py (11个单继承+1个多继承)

__all__导出补全 (36个类):
- java.net: InterfaceAddress
- android.database: MatrixCursor, SQLiteStatement
- android.view: GestureDetector, ScaleGestureDetector, SurfaceHolder, Surface, SurfaceView, TextureView, ViewOutlineProvider, WindowManager, Window, LayoutInflater, Menu, MenuItem, ContextMenu, Display, DisplayMetrics
- android.graphics: Region, PathMeasure, NinePatch, Picture, PictureDrawable, Camera, PorterDuff, PorterDuffXfermode, Shader, LinearGradient, RadialGradient, SweepGradient, BitmapShader, ComposeShader, CornerPathEffect, DashPathEffect, PathDashPathEffect, SumPathEffect
- android.widget: 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

统计: 221模块, 3666类, 31005方法, 183629行
2026-06-25 01:24:00 +00:00
shuiyu1123 fbb8263b13 feat: 第三轮深度完善 — SQLite/JSON/网络/媒体/文本 真实实现
SQLite真实数据库:
- SQLiteDatabase: execSQL/insert/insertOrThrow/replace/update/delete/query 用sqlite3
- 修复getVersion/setVersion用PRAGMA user_version
- 修复getMaximumSize/getPageSize/setMaximumSize/setPageSize用PRAGMA
- SQLiteStatement: execute/simpleQueryForLong/simpleQueryForString/execQuery
- SQLiteOpenHelper: onCreate/onUpgrade/onDowngrade回调正确触发
- 修复SQLiteCursor.moveToPosition越界处理
- 修复ContentValues值对提取bug

JSON真实实现:
- JSONObject: isNull/append/putOnce/putOpt/getNames
- JSONArray: 完整put/get/opt/length/toString

网络真实实现:
- NetworkInterface: getByName/getNetworkInterfaces/getHardwareAddress 用/sys/class/net+fcntl.ioctl
- InterfaceAddress: 新增类
- ConnectivityManager: getActiveNetwork/getAllNetworks 用LinuxNetwork
- WifiManager: getScanResults用nmcli, setWifiEnabled用nmcli/rfkill
- BluetoothAdapter: getAddress/getName 用bluetoothctl

媒体真实实现:
- MediaPlayer: prepare检查文件, getCurrentPosition实时计算, getDuration/getVideoWidth用ffprobe
- MediaRecorder: setVideoSize/setVideoFrameRate/setVideoEncodingBitRate配置存储
- MediaMetadataRetriever: extractMetadata用ffprobe, getFrameAtTime用ffmpeg, getEmbeddedPicture用ffmpeg

文本真实实现:
- TextUtils: split用re.split, concat/indexOf/lastIndexOf/urlEncode/urlDecode
- SpannableString: setSpan/getSpans/getSpanFlags/nextSpanTransition 真实实现
- SpannableStringBuilder: 修复append重载bug

Provider增强:
- Settings: putBoolean/getBoolean
- MediaStore: getContentUri/VOLUME常量
- ContactsContract: 补全常量和CommonDataKinds

Util增强:
- Log: w/e/wtf支持Throwable参数, 修复wtf重复定义bug
- TypedValue: 修复complexToDimension/complexToFloat/getFloat实现

修复:
- __init__.py: 补全NetworkInterface/InetAddress/MediaMetadataRetriever/SpannableStringBuilder导出

统计: 221模块, 3666类, 31005方法, 183529行
2026-06-25 00:40:59 +00:00
shuiyu1123 4235f8692c feat: 深度完善 — 存根转真实实现、UI类方法补全、Java集合/IO增强
存根转真实实现:
- ContentResolver: query/insert/update/delete对file://URI真实文件操作, getType用mimetypes
- Context: getResources/getAssets/getString/getColor 委托真实Resources
- Bitmap.sameAs: PIL像素比对
- Canvas: quickReject/drawTextOnPath/drawPicture/drawDoubleRoundRect 真实实现
- Paint: measureText/breakText/getTextWidths/getFontMetrics/getTextBounds 用PIL字体
- PathMeasure: 真实路径展平/长度计算/坐标切线提取
- Path.op/Region.op: 布尔运算
- Camera: 3D变换矩阵计算
- SystemClock.setCurrentTimeMillis: date -s命令
- Environment.isExternalStorageRemovable: /proc/mounts检查
- Process.myUid: os.getuid()
- PowerManager.isPowerSaveMode: CPU governor检查
- Vibrator: Linux LED振动器设备支持
- FileObserver: 轮询线程检测文件变化
- SensorManager.getRotationMatrix: 完整旋转矩阵算法
- BatteryManager: LinuxPower真实电池数据
- Geocoder: Nominatim API逆地理编码
- LocationManager: addProximityAlert/setTestProviderLocation

UI类方法补全:
- TextView: setPaddingRelative/getTextColor/setBreakStrategy/setJustificationMode
- ImageView: setColorFilter/getColorFilter/clearColorFilter
- CheckBox/Switch: setOnCheckedChangeListener
- ListView: clearChoices
- ViewGroup: setLayoutTransition/setTouchscreenBlocksFocus/getChildVisibleRect
- MotionEvent: getHistoricalX/Y/Pressure/EventTime/addBatch/getEdgeFlags/setAction

Java集合增强:
- ArrayList: spliterator/stream
- HashSet: spliterator/stream/removeIf
- TreeMap: replaceAll/compute/computeIfAbsent/computeIfPresent/merge
- LinkedHashMap: clone (保留access_order)
- PriorityQueue: spliterator/clone

Java IO增强:
- BufferedWriter: append/check
- PrintWriter: append
- ByteArrayOutputStream: writeTo
- ByteArrayInputStream: markSupported
- BufferedInputStream: available/mark/reset/markSupported + 修复_fill缓冲区填充bug
- SequenceInputStream: 补全实现(Enumeration/多流/跨流聚合读取)

修复:
- ViewGroup.addView: 合并重复定义为智能参数分派
- view_ext.ViewGroup: 继承view.ViewGroup
- view_ext.KeyEvent: 继承view.KeyEvent
- hardware_sensor_ext.SensorManager: 继承hardware.SensorManager
- app_fragment: 使用java.lang.IllegalStateException
- content_intent.Intent: setData/getScheme正确从Uri提取scheme
- __init__.py: 补全Fragment/KeyEvent/MotionEvent/TrafficStats/RecyclerView/TreeMap/Vibrator等导出

统计: 221模块, 3662类, 30952方法, 182302行
2026-06-25 00:17:50 +00:00
shuiyu1123 39b1da37c9 feat: 全面完善API — 补全核心类方法、存根转真实实现、Exception体系扩展
核心类方法补全:
- Activity: +20方法 (requestWindowFeature, setTitle/getTitle, onKeyDown/onKeyUp, enterPictureInPictureMode, getReferrer, isTaskRoot, navigateUpTo等)
- Fragment: +15方法 (getChildFragmentManager, getParentFragment, requireContext/requireActivity, setMenuVisibility等)
- Intent: +20方法 (getParcelableExtra, setComponent/getComponent, getDataString, getScheme, putExtras等)
- View: +20方法 (setLabelFor, setRotationX/Y, setForeground, setBackgroundTintList等)
- KeyEvent: +7方法 (getCharacters, isSystem, isPrintingKey, isModifierKey, startTracking等)
- Paint: +12方法 (setStrokeCap/Join, getTextBounds, setBlendMode, setLetterSpacing等)
- Notification.Builder: +10方法 (setProgress, setShowWhen, setOnlyAlertOnce, setSound等)

存根转真实实现:
- TrafficStats: 读取 /proc/net/dev 返回真实网络流量
- RecyclerView.Adapter: 观察者模式触发通知
- BaseAdapter: 观察者模式触发通知
- ExifInterface: 使用PIL实现图片翻转/旋转
- Process: 使用os.kill实现进程信号
- FileUtils: 真实文件I/O
- SharedPreferences: 监听器模式
- TelephonyManager.listen: 维护监听器列表
- ScrollView.smoothScrollTo: 更新滚动位置

Exception体系扩展:
- 新增15个Exception/Error类 (ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchElementException, AssertionError, OutOfMemoryError, StackOverflowError, NoClassDefFoundError, UnsatisfiedLinkError, LinkageError, VerifyError, AbstractMethodError, IncompatibleClassChangeError, ExceptionInInitializerError)

Math/System/Thread/StringBuilder/Integer增强:
- Math: +20方法 (subtractExact, multiplyExact, toIntExact, floorDiv/floorMod, fma等)
- System: getProperties/setProperties/mapLibraryName/getLogger
- Thread: +10方法 (getUncaughtExceptionHandler, getAllStackTraces, getStackTrace, holdLock等)
- StringBuilder: +7方法 (ensureCapacity, trimToSize, setLength, subSequence等)
- Integer: +8静态方法 (compare, compareUnsigned, divideUnsigned, sum, max, min等)

修复:
- app_fragment.py: 使用java.lang.IllegalStateException替代本地定义
- content_intent.py: Intent.setData/getScheme正确从Uri提取scheme
- view_ext.py: KeyEvent继承view.KeyEvent获取所有KEYCODE常量
- __init__.py: 补全Fragment/KeyEvent/MotionEvent/TrafficStats/RecyclerView等导出

统计: 221模块, 3660类, 30826方法, 180217行
2026-06-24 23:46:36 +00:00
shuiyu1123 e316e18d98 docs: 添加 MIT 许可证 + README + setup.py,仓库已开放
- LICENSE: MIT License
- README.md: 完整项目文档 (特性/安装/快速开始/项目结构/设计理念)
- setup.py: 支持 pip install 安装
- 仓库已通过 Gitee API 设为公开
2026-06-24 23:01:32 +00:00
shuiyu1123 690022cea3 feat: Linux 真实实现层 — 从存根库变为可用生产力工具
新增 _linux_impl.py (800+行):
- LinuxNetwork: 真实IP/接口/DNS/网关/MAC (ip命令+/proc/net)
- LinuxBluetooth: 真实蓝牙状态/扫描/配对/连接 (bluetoothctl)
- LinuxSensors: 真实温度/CPU频率/风扇 (/sys/class/hwmon)
- LinuxAudio: 真实音量控制/静音 (amixer)
- LinuxClipboard: 真实剪贴板读写 (xclip+tkinter回退)
- LinuxPower: 真实CPU/内存/电池 (psutil+/sys/class/power_supply)
- LinuxLocation: 真实IP地理定位 (ip-api.com)
- LinuxNotify: 真实桌面通知 (notify-send)
- LinuxDisplay: 真实屏幕尺寸/截屏/鼠标位置 (xdotool+X11)
- LinuxUIAutomation: 真实点击/输入/按键/滚动 (xdotool)
- LinuxCamera: 真实摄像头拍照 (opencv/V4L2)
- LinuxProcess: 真实进程列表/系统信息/应用管理 (psutil+/proc)
- LinuxShell: 真实命令执行 (subprocess)

改造10+核心类使用Linux真实实现:
- BluetoothAdapter/SensorManager/AudioManager
- ConnectivityManager/WifiManager/TelephonyManager
- LocationManager/PowerManager/BatteryManager/Build
- ActivityManager/Activity/Toast/Display/ClipboardManager
- NotificationManager

15项真实行为测试全部通过:
- 真实IP: 10.60.218.244
- 真实位置: 北京 39.9042, 116.407
- 真实主机名: work-41-vci-g427c
- 真实内核: 5.15.120.byteatom-ck.14
- 真实内存: 1.8GB/3.8GB
- 真实进程列表
2026-06-24 15:40:49 +00:00
54 changed files with 10207 additions and 504 deletions

No files matched your search

+21
View File
@@ -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.
+206
View File
@@ -0,0 +1,206 @@
# Android-pyApi
> **Android API for Python** — 在桌面/Linux 上原生运行 Android API 的 Python 库
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-green.svg)](#)
## 这是什么
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)
+264
View File
@@ -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 库
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-green.svg)](#)
## 这是什么
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)
+230
View File
@@ -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 @@
+26
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
android_pyapi
+47 -1
View File
@@ -379,6 +379,13 @@ __all__ = [
"AccessibilityServiceInfo", "AccessibilityServiceInfo",
"Account", "AccountManager", "Account", "AccountManager",
"Activity", "ActivityManager", "Activity", "ActivityManager",
"Fragment", "FragmentManager", "FragmentTransaction",
"Dialog", "AlertDialog", "ProgressDialog",
"DatePickerDialog", "TimePickerDialog",
"Service", "Application",
"PendingIntent", "ComponentName", "AlarmManager",
"NotificationChannel", "NotificationChannelGroup",
"NotificationCompat", "TaskStackBuilder",
"AlphaAnimation", "AlphaAnimation",
"Animation", "Animation",
"Animator", "ValueAnimator", "ObjectAnimator", "AnimatorSet", "Animator", "ValueAnimator", "ObjectAnimator", "AnimatorSet",
@@ -391,6 +398,14 @@ __all__ = [
"BatteryManager", "BatteryManager",
"BiometricPrompt", "BiometricPrompt",
"Bitmap", "BitmapDrawable", "BitmapFactory", "Bitmap", "BitmapDrawable", "BitmapFactory",
"Camera",
"Region", "PathMeasure",
"NinePatch", "Picture", "PictureDrawable",
"PorterDuff", "PorterDuffXfermode",
"Shader", "LinearGradient", "RadialGradient", "SweepGradient",
"BitmapShader", "ComposeShader",
"CornerPathEffect", "DashPathEffect", "PathDashPathEffect",
"SumPathEffect",
"BluetoothAdapter", "BluetoothDevice", "BluetoothAdapter", "BluetoothDevice",
"BroadcastReceiver", "BroadcastReceiver",
"Build", "Build",
@@ -423,6 +438,13 @@ __all__ = [
"Html", "Html",
"ImageView", "ImageView",
"InputDevice", "InputDevice",
"KeyEvent", "MotionEvent",
"GestureDetector", "ScaleGestureDetector",
"SurfaceHolder", "Surface", "SurfaceView", "TextureView",
"ViewOutlineProvider",
"WindowManager", "Window",
"LayoutInflater", "Menu", "MenuItem", "ContextMenu",
"Display", "DisplayMetrics",
"InputManager", "InputManager",
"InputMethodManager", "InputMethodManager",
"Intent", "Intent",
@@ -438,6 +460,7 @@ __all__ = [
"MediaPlayer", "MediaPlayer",
"MediaProjectionManager", "MediaProjectionManager",
"MediaRecorder", "MediaRecorder",
"MediaMetadataRetriever",
"MediaSession", "MediaSession",
"MediaStore", "MediaStore",
"Message", "Message",
@@ -453,9 +476,18 @@ __all__ = [
"PermissionManager", "PermissionManager",
"PowerManager", "PowerManager",
"PrintAttributes", "PrintDocumentAdapter", "PrintManager", "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", "ProgressBar",
"RadioManager", "RadioTuner", "RadioManager", "RadioTuner",
"Rect", "RectF", "Rect", "RectF",
"RecyclerView",
"Resources", "AssetManager", "Resources", "AssetManager",
"RoleManager", "RoleManager",
"ScrollView", "ScrollView",
@@ -466,9 +498,11 @@ __all__ = [
"Slice", "SliceManager", "SliceProvider", "Slice", "SliceManager", "SliceProvider",
"SmsManager", "SmsManager",
"SpannableString", "SpannableString",
"SpannableStringBuilder",
"SpeechRecognizer", "SpeechRecognizer",
"Spinner", "Spinner",
"SQLiteDatabase", "SQLiteOpenHelper", "SQLiteDatabase", "SQLiteOpenHelper", "SQLiteStatement",
"MatrixCursor",
"StorageManager", "StorageVolume", "StorageManager", "StorageVolume",
"StructStat", "StructStat",
"Switch", "Switch",
@@ -481,6 +515,7 @@ __all__ = [
"ThermalManager", "ThermalManager",
"TimeManager", "TimeManager",
"Toast", "Toast",
"TrafficStats",
"TranslateAnimation", "TranslateAnimation",
"TranslationManager", "TranslationManager",
"TvInputManager", "TvInputManager",
@@ -493,6 +528,7 @@ __all__ = [
"UwbManager", "UwbManager",
"View", "ViewGroup", "View", "ViewGroup",
"VirtualDisplay", "VirtualDisplay",
"Vibrator", "VibratorEffect",
"VpnManager", "VpnService", "VpnManager", "VpnService",
"WebView", "WebView",
"WifiAwareManager", "WifiAwareManager",
@@ -503,7 +539,9 @@ __all__ = [
"BufferedReader", "BufferedWriter", "BufferedReader", "BufferedWriter",
"InputStreamReader", "OutputStreamWriter", "InputStreamReader", "OutputStreamWriter",
"URL", "HttpURLConnection", "Socket", "ServerSocket", "URL", "HttpURLConnection", "Socket", "ServerSocket",
"NetworkInterface", "InetAddress", "InetSocketAddress", "InterfaceAddress",
"ArrayList", "HashMap", "HashSet", "LinkedList", "ArrayList", "HashMap", "HashSet", "LinkedList",
"TreeMap", "LinkedHashMap", "PriorityQueue",
"Collections", "Arrays", "Calendar", "Date", "Random", "Collections", "Arrays", "Calendar", "Date", "Random",
"UUID", "Scanner", "Timer", "TimerTask", "UUID", "Scanner", "Timer", "TimerTask",
"Pattern", "Matcher", "Pattern", "Matcher",
@@ -524,6 +562,14 @@ __all__ = [
"CloneNotSupportedException", "IllegalMonitorStateException", "CloneNotSupportedException", "IllegalMonitorStateException",
"IllegalThreadStateException", "EnumConstantNotPresentException", "IllegalThreadStateException", "EnumConstantNotPresentException",
"TypeNotPresentException", "TypeNotPresentException",
# Java Exception 体系 (扩展)
"ClassNotFoundException", "IllegalAccessException",
"InstantiationException", "InvocationTargetException",
"NoSuchElementException", "AssertionError",
"OutOfMemoryError", "StackOverflowError", "NoClassDefFoundError",
"UnsatisfiedLinkError", "LinkageError", "VerifyError",
"AbstractMethodError", "IncompatibleClassChangeError",
"ExceptionInInitializerError",
"SimpleDateFormat", "DecimalFormat", "SimpleDateFormat", "DecimalFormat",
"LocalDateTime", "LocalDate", "LocalTime", "Instant", "Duration", "Period", "LocalDateTime", "LocalDate", "LocalTime", "Instant", "Duration", "Period",
"ChronoUnit", "ChronoField", "DayOfWeek", "Month", "ZoneId", "ZoneOffset", "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
+397
View File
@@ -18,6 +18,13 @@ from .._core import (
resolve_android_path, to_android_path, resolve_android_path, to_android_path,
_sdcard_root, _app_data_dir, _package_name, _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: def filterHashCode(self) -> int:
return hash((self._action, self._data_str, self._type, self._package, self._component)) & 0x7FFFFFFF 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) # ComponentName (forward reference)
@@ -732,6 +841,62 @@ class Notification(Stub):
n._extras = dict(self._extras) n._extras = dict(self._extras)
return n 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): def __init__(self):
super().__init__() super().__init__()
self._channel_id = "" self._channel_id = ""
@@ -834,6 +999,12 @@ class NotificationManager(Stub):
def notify(self, id_: int, notification: Notification): def notify(self, id_: int, notification: Notification):
self._notifications[id_] = notification self._notifications[id_] = notification
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( sys.stderr.write(
f"[NotificationManager] notify id={id_}: " f"[NotificationManager] notify id={id_}: "
f"title={notification._title} text={notification._text}\n" f"title={notification._title} text={notification._text}\n"
@@ -1343,6 +1514,90 @@ class Fragment(Stub):
def unregisterForContextMenu(self, view): def unregisterForContextMenu(self, view):
pass 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): class FragmentTransaction(Stub):
"""FragmentTransaction — 管理 Fragment 事务""" """FragmentTransaction — 管理 Fragment 事务"""
@@ -1821,6 +2076,144 @@ class Activity(Stub):
def getTheme(self): def getTheme(self):
return getattr(self, '_theme', 0) 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 # Service
@@ -2009,9 +2402,13 @@ class ActivityManager(Stub):
self._memory_info = self.MemoryInfo() self._memory_info = self.MemoryInfo()
def getMemoryInfo(self) -> "ActivityManager.MemoryInfo": def getMemoryInfo(self) -> "ActivityManager.MemoryInfo":
if _USE_LINUX:
return LinuxPower.get_memory_info()
return self._memory_info return self._memory_info
def getRunningAppProcesses(self) -> Optional[List["ActivityManager.RunningAppProcessInfo"]]: def getRunningAppProcesses(self) -> Optional[List["ActivityManager.RunningAppProcessInfo"]]:
if _USE_LINUX:
return LinuxProcess.list_processes()
return [] return []
def killBackgroundProcesses(self, package_name: str): def killBackgroundProcesses(self, package_name: str):
+10 -14
View File
@@ -11,9 +11,10 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Callable
from enum import IntEnum from enum import IntEnum
from .._core import Stub, AndroidOnlyError from .._core import Stub, AndroidOnlyError
from ..java.lang import RuntimeException from ..java.lang import RuntimeException, IllegalArgumentException, IllegalStateException
from .app import Fragment as _BaseFragment
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) from .app import FragmentManager as _BaseFragmentManager
from .app import FragmentTransaction as _BaseFragmentTransaction
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -64,8 +65,8 @@ POP_BACK_STACK_INCLUSIVE = 1
# Fragment # Fragment
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class Fragment(Stub): class Fragment(_BaseFragment):
"""Fragment — 片段基类 (桌面存根)""" """Fragment — 片段基类 (桌面存根,继承 app.Fragment 的所有方法)"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -424,17 +425,12 @@ class Fragment(Stub):
return f"Fragment({self._mTag or hex(id(self))})" return f"Fragment({self._mTag or hex(id(self))})"
class IllegalStateException(RuntimeException):
"""IllegalStateException — 非法状态异常"""
pass
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# FragmentManager # FragmentManager
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class FragmentManager(Stub): class FragmentManager(_BaseFragmentManager):
"""FragmentManager — 片段管理器""" """FragmentManager — 片段管理器 (继承 app.FragmentManager 的所有方法)"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -545,8 +541,8 @@ class FragmentManager(Stub):
# FragmentTransaction # FragmentTransaction
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class FragmentTransaction(Stub): class FragmentTransaction(_BaseFragmentTransaction):
"""FragmentTransaction — 片段事务""" """FragmentTransaction — 片段事务 (继承 app.FragmentTransaction 的所有方法)"""
TRANSIT_ENTER_MASK = TRANSIT_ENTER_MASK TRANSIT_ENTER_MASK = TRANSIT_ENTER_MASK
TRANSIT_EXIT_MASK = TRANSIT_EXIT_MASK TRANSIT_EXIT_MASK = TRANSIT_EXIT_MASK
+15
View File
@@ -13,6 +13,12 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
from .._core import Stub, AndroidOnlyError from .._core import Stub, AndroidOnlyError
try:
from .._linux_impl import IS_LINUX, LinuxProcess, LinuxPower
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
__all__ = [ __all__ = [
"TaskManager", "TaskInfo", "TaskStackListener", "TaskSnapshot", "TaskManager", "TaskInfo", "TaskStackListener", "TaskSnapshot",
"TaskDescription", "ActivityManager", "TaskDescription", "ActivityManager",
@@ -908,9 +914,18 @@ class ActivityManager(Stub):
return list(self._services.values())[:maxNum] return list(self._services.values())[:maxNum]
def getRunningAppProcesses(self) -> List[RunningAppProcessInfo]: def getRunningAppProcesses(self) -> List[RunningAppProcessInfo]:
if _USE_LINUX:
return LinuxProcess.list_processes()
return list(self._processes.values()) return list(self._processes.values())
def getMemoryInfo(self, outInfo: MemoryInfo): 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 import psutil
try: try:
import psutil import psutil
+58
View File
@@ -14,6 +14,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name, _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 # BluetoothProfile
@@ -645,22 +652,36 @@ class BluetoothAdapter(Stub):
return BluetoothAdapter._default_adapter return BluetoothAdapter._default_adapter
def isEnabled(self) -> bool: def isEnabled(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.get_state() == LinuxBluetooth.STATE_ON
return self._enabled return self._enabled
def enable(self) -> bool: def enable(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.enable()
self._enabled = True self._enabled = True
self._state = self.STATE_ON self._state = self.STATE_ON
return True return True
def disable(self) -> bool: def disable(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.disable()
self._enabled = False self._enabled = False
self._state = self.STATE_OFF self._state = self.STATE_OFF
return True return True
def getAddress(self) -> str: def getAddress(self) -> str:
if _USE_LINUX:
addr = LinuxBluetooth.get_address()
if addr:
return addr
return self._address return self._address
def getName(self) -> str: def getName(self) -> str:
if _USE_LINUX:
name = LinuxBluetooth.get_name()
if name:
return name
return self._name return self._name
def setName(self, name: str) -> bool: def setName(self, name: str) -> bool:
@@ -668,13 +689,36 @@ class BluetoothAdapter(Stub):
return True return True
def getBondedDevices(self) -> Set[BluetoothDevice]: 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) 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: def startDiscovery(self) -> bool:
if _USE_LINUX:
try:
LinuxBluetooth.scan(timeout=5)
return True
except Exception:
return False
self._discovering = True self._discovering = True
return True return True
def cancelDiscovery(self) -> bool: def cancelDiscovery(self) -> bool:
if _USE_LINUX:
# bluetoothctl scan off 在 LinuxBluetooth.scan 内部已处理
return True
self._discovering = False self._discovering = False
return True return True
@@ -694,6 +738,8 @@ class BluetoothAdapter(Stub):
pass pass
def getState(self) -> int: def getState(self) -> int:
if _USE_LINUX:
return LinuxBluetooth.get_state()
return self._state return self._state
def listenUsingRfcommWithServiceRecord(self, name: str, uuid_str: str) -> BluetoothServerSocket: def listenUsingRfcommWithServiceRecord(self, name: str, uuid_str: str) -> BluetoothServerSocket:
@@ -704,6 +750,18 @@ class BluetoothAdapter(Stub):
return BluetoothServerSocket() return BluetoothServerSocket()
def getRemoteDevice(self, address: str) -> Optional[BluetoothDevice]: 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): if not BluetoothAdapter.checkBluetoothAddress(address):
return None return None
return BluetoothDevice(address) return BluetoothDevice(address)
+326 -23
View File
@@ -4,6 +4,7 @@ Android Content 组件 — Context, ContentResolver, Uri, SharedPreferences 等
""" """
import json import json
import mimetypes
import os import os
import sys import sys
import threading import threading
@@ -16,6 +17,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name, _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 # Uri
@@ -366,34 +373,134 @@ class ContentResolver(Stub):
super().__init__() super().__init__()
self._observers: Dict[str, List[Any]] = {} 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], def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str], selection_args: Optional[List[str]], sort_order: Optional[str],
cancellation_signal: Any = None) -> Any: cancellation_signal: Any = None) -> Any:
"""Query content provider""" """Query content provider. 对 file:// URI 返回基于目录列表的 Cursor。"""
sys.stderr.write(f"[ContentResolver] query: uri={uri}\n") 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 return None
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]: 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 return uri
def update(self, uri: Uri, values: ContentValues, selection: Optional[str], def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int: 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 return 0
def delete(self, uri: Uri, selection: Optional[str], def delete(self, uri: Uri, selection: Optional[str],
selection_args: Optional[List[str]]) -> int: 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 return 0
def applyBatch(self, authority: str, operations: List[Any]) -> List[Any]: def applyBatch(self, authority: str, operations: List[Any]) -> List[Any]:
sys.stderr.write(f"[ContentResolver] applyBatch: authority={authority}\n") """Apply batch operations, returning per-op results."""
return [] 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: def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
sys.stderr.write(f"[ContentResolver] bulkInsert: uri={uri}\n") count = 0
return len(values) if values else 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]: def call(self, uri: Uri, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
return None return None
@@ -420,7 +527,17 @@ class ContentResolver(Stub):
if not self._observers[uri_str]: if not self._observers[uri_str]:
del 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 return None
def acquireContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]: def acquireContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]:
@@ -430,12 +547,40 @@ class ContentResolver(Stub):
return ContentProviderClient(uri) return ContentProviderClient(uri)
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
"""返回 URI 支持的流 MIME 类型列表。"""
mime = self.getType(uri)
if mime is None:
return 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: 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 return None
def openOutputStream(self, uri: Uri) -> Any: 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 return None
@@ -466,6 +611,7 @@ class Context(Stub):
INPUT_METHOD_SERVICE = "input_method" INPUT_METHOD_SERVICE = "input_method"
CAMERA_SERVICE = "camera" CAMERA_SERVICE = "camera"
BATTERY_SERVICE = "battery" BATTERY_SERVICE = "battery"
CLIPBOARD_SERVICE = "clipboard"
# Mode 常量 # Mode 常量
MODE_PRIVATE = 0x0000 MODE_PRIVATE = 0x0000
@@ -587,10 +733,16 @@ class Context(Stub):
return PackageManager() return PackageManager()
def getResources(self): 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): 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): def getMainLooper(self):
from .os import Looper from .os import Looper
@@ -642,6 +794,8 @@ class Context(Stub):
if name == Context.BATTERY_SERVICE: if name == Context.BATTERY_SERVICE:
from .os import BatteryManager from .os import BatteryManager
return BatteryManager() return BatteryManager()
if name == Context.CLIPBOARD_SERVICE:
return ClipboardManager()
return self._services.get(name) return self._services.get(name)
def startService(self, intent: Any) -> Optional[Any]: def startService(self, intent: Any) -> Optional[Any]:
@@ -659,15 +813,34 @@ class Context(Stub):
pass pass
def getString(self, res_id: int, *args) -> str: 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) return str(res_id)
def getColor(self, color_id: int) -> int: 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): def getDrawable(self, drawable_id: int):
"""返回 Drawable。优先委托给 Resources。"""
try:
res = self.getResources()
return res.getDrawable(drawable_id)
except Exception:
return None return None
def getWallpaper(self): def getWallpaper(self):
"""返回壁纸 Drawable。桌面端返回 None(无系统壁纸)。"""
return None return None
def getWallpaperDesiredMinimumWidth(self) -> int: def getWallpaperDesiredMinimumWidth(self) -> int:
@@ -786,18 +959,55 @@ class Context(Stub):
self._theme = resid self._theme = resid
def getText(self, 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) return str(resId)
def getQuantityString(self, resId, quantity, *formatArgs): def getQuantityString(self, resId, quantity, *formatArgs):
"""返回复数字符串。优先委托给 Resources。"""
try:
res = self.getResources()
return res.getQuantityString(resId, quantity, *formatArgs)
except Exception:
return str(resId) return str(resId)
def getStringArray(self, 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 [] return []
def getTextArray(self, resId): def getTextArray(self, resId):
"""返回文本数组。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getTextArray(resId)
if val is not None:
return val
except Exception:
pass
return [] return []
def getIntArray(self, resId): def getIntArray(self, resId):
"""返回整型数组。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getIntArray(resId)
if val is not None:
return val
except Exception:
pass
return [] return []
# ─── Context 派生 / 回调 (扩展) ─────────────────────────── # ─── Context 派生 / 回调 (扩展) ───────────────────────────
@@ -1107,9 +1317,15 @@ class SharedPreferences(Stub):
return SharedPreferencesEditor(self) return SharedPreferencesEditor(self)
def registerOnSharedPreferenceChangeListener(self, listener): def registerOnSharedPreferenceChangeListener(self, listener):
pass if not hasattr(self, '_listeners'):
self._listeners = []
self._listeners.append(listener)
def unregisterOnSharedPreferenceChangeListener(self, listener): def unregisterOnSharedPreferenceChangeListener(self, listener):
if hasattr(self, '_listeners'):
try:
self._listeners.remove(listener)
except ValueError:
pass pass
def getSharedPrefsFile(self): def getSharedPrefsFile(self):
@@ -1177,6 +1393,12 @@ class SharedPreferencesEditor(Stub):
self._prefs._data.pop(key, None) self._prefs._data.pop(key, None)
self._prefs._data.update(self._modified) self._prefs._data.update(self._modified)
self._prefs._save() 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._modified.clear()
self._removed.clear() self._removed.clear()
self._clear_all = False self._clear_all = False
@@ -1365,29 +1587,55 @@ class ContentProvider(Stub):
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str], def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]: 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 return None
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]: def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
if self._context is not None:
return self._context.getContentResolver().insert(uri, values)
return None return None
def update(self, uri: Uri, values: ContentValues, selection: Optional[str], def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int: selection_args: Optional[List[str]]) -> int:
if self._context is not None:
return self._context.getContentResolver().update(uri, values, selection, selection_args)
return 0 return 0
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int: 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 return 0
def getType(self, uri: Uri) -> Optional[str]: 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 return None
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
mime = self.getType(uri)
if mime is None:
return None return None
return [mime]
def openFile(self, uri: Uri, mode: str) -> Any: 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 return None
def openAssetFile(self, uri: Uri, mode: str) -> Any: def openAssetFile(self, uri: Uri, mode: str) -> Any:
return None return self.openFile(uri, mode)
def attachInfo(self, context: Context, info: Any): def attachInfo(self, context: Context, info: Any):
self._context = context self._context = context
@@ -1423,29 +1671,44 @@ class ContentProviderClient(Stub):
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str], def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]: 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]: 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], def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int: 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: 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]: def applyBatch(self, operations: List[Any]) -> List[Any]:
return [] resolver = ContentResolver()
return resolver.applyBatch("", operations)
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int: 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]: 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 return None
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
mime = self.getType(uri)
if mime is None:
return None return None
return [mime]
def call(self, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]: def call(self, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
return None return None
@@ -1457,13 +1720,21 @@ class ContentProviderClient(Stub):
pass pass
def openFile(self, uri: Uri, mode: str) -> Any: 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 return None
def openAssetFile(self, uri: Uri, mode: str) -> Any: 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: def openTypedAssetFile(self, uri: Uri, mime_type: str, opts: Optional[ContentValues]) -> Any:
return None return self.openFile(uri, "r")
def getLocalContentProvider(self) -> Optional[ContentProvider]: def getLocalContentProvider(self) -> Optional[ContentProvider]:
return None return None
@@ -1637,6 +1908,14 @@ class CursorLoader(Loader):
self._sort_order = sort_order self._sort_order = sort_order
def loadInBackground(self): 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 return None
@@ -1708,6 +1987,29 @@ class SyncResult(Stub):
return f"SyncResult({self.stats.numEntries} entries)" 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 # 替换 _core.py 中的 _GlobalContext
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -1748,4 +2050,5 @@ __all__ = [
"DialogInterface", "DialogInterface",
"SyncAdapter", "SyncAdapter",
"SyncResult", "SyncResult",
"ClipboardManager",
] ]
+31 -8
View File
@@ -13,6 +13,9 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError from .._core import Stub, AndroidOnlyError
from ..java.lang import RuntimeException 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,), {}) IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
@@ -680,8 +683,8 @@ MIME_TYPE_ALL = "*/*"
# Intent — 完整扩展 # Intent — 完整扩展
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class Intent(Stub): class Intent(_BaseIntent):
"""Android Intent — 完整扩展 (桌面存根)""" """Android Intent — 完整扩展 (桌面存根,继承 app.Intent 的所有方法)"""
# ── ShortcutIconResource inner class ── # ── ShortcutIconResource inner class ──
@@ -761,16 +764,36 @@ class Intent(Stub):
def getData(self) -> Optional[str]: def getData(self) -> Optional[str]:
return self._data return self._data
def setData(self, data: str) -> "Intent": def setData(self, data) -> "Intent":
self._data = data self._data = data
self._type = None 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 return self
def getDataString(self) -> Optional[str]: def getDataString(self) -> Optional[str]:
if self._data is None:
return None
if isinstance(self._data, str):
return self._data return self._data
return str(self._data)
def getScheme(self) -> str: def getScheme(self) -> str:
if self._scheme:
return 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 ── # ── Type ──
@@ -1256,8 +1279,8 @@ class IntentReceiver(Stub):
# PendingIntent # PendingIntent
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class PendingIntent(Stub): class PendingIntent(_BasePendingIntent):
"""PendingIntent — 延迟 Intent""" """PendingIntent — 延迟 Intent (继承 app.PendingIntent 的所有方法)"""
FLAG_ONE_SHOT = 1 << 30 FLAG_ONE_SHOT = 1 << 30
FLAG_NO_CREATE = 1 << 29 FLAG_NO_CREATE = 1 << 29
@@ -1268,7 +1291,7 @@ class PendingIntent(Stub):
def __init__(self, intent: Optional[Intent] = None, requestCode: int = 0, def __init__(self, intent: Optional[Intent] = None, requestCode: int = 0,
flags: int = 0): flags: int = 0):
super().__init__() super().__init__(intent, requestCode, flags)
self._intent = intent self._intent = intent
self._request_code = requestCode self._request_code = requestCode
self._flags = flags self._flags = flags
@@ -1455,8 +1478,8 @@ class IntentService(Stub):
# IntentFilter # IntentFilter
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class IntentFilter(Stub): class IntentFilter(_BaseIntentFilter):
"""IntentFilter — Intent 过滤器 (桌面存根)""" """IntentFilter — Intent 过滤器 (桌面存根,继承 content.IntentFilter 的所有方法)"""
class AuthorityEntry(Stub): class AuthorityEntry(Stub):
"""AuthorityEntry — 权限条目""" """AuthorityEntry — 权限条目"""
+23
View File
@@ -36,6 +36,8 @@ class Resources(Stub):
self._drawables: Dict[str, Any] = {} self._drawables: Dict[str, Any] = {}
self._booleans: Dict[str, bool] = {} self._booleans: Dict[str, bool] = {}
self._integers: Dict[str, int] = {} 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._identifiers: Dict[str, int] = {}
self._next_id = 0x7f010000 self._next_id = 0x7f010000
self._configuration = Configuration() self._configuration = Configuration()
@@ -125,6 +127,27 @@ class Resources(Stub):
return val return val
return 0 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 ── # ── Layout / Animation / Xml ──
def getLayout(self, id: Union[int, str]) -> Any: def getLayout(self, id: Union[int, str]) -> Any:
+19 -3
View File
@@ -331,21 +331,31 @@ class Cursor(Stub):
return self.moveToPosition(self._pos + offset) return self.moveToPosition(self._pos + offset)
def moveToPosition(self, position: int) -> bool: 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 return False
self._pos = position self._pos = position
return True return True
def isFirst(self) -> bool: def isFirst(self) -> bool:
return self._pos == 0 return self._pos == 0 and self.getCount() > 0
def isLast(self) -> bool: 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: def isBeforeFirst(self) -> bool:
if self.getCount() == 0:
return True
return self._pos < 0 return self._pos < 0
def isAfterLast(self) -> bool: def isAfterLast(self) -> bool:
if self.getCount() == 0:
return True
return self._pos >= self.getCount() return self._pos >= self.getCount()
def getColumnIndex(self, column_name: str) -> int: def getColumnIndex(self, column_name: str) -> int:
@@ -370,6 +380,9 @@ class Cursor(Stub):
def getColumnNames(self) -> List[str]: def getColumnNames(self) -> List[str]:
return [] return []
def getColumnCount(self) -> int:
return len(self.getColumnNames())
def getType(self, column_index: int) -> int: def getType(self, column_index: int) -> int:
return self.FIELD_TYPE_NULL return self.FIELD_TYPE_NULL
@@ -759,6 +772,9 @@ class MatrixCursor(AbstractCursor):
def getLong(self, column_index: int) -> int: def getLong(self, column_index: int) -> int:
return self.getInt(column_index) return self.getInt(column_index)
def getShort(self, column_index: int) -> int:
return self.getInt(column_index)
def getFloat(self, column_index: int) -> float: def getFloat(self, column_index: int) -> float:
v = self.getString(column_index) v = self.getString(column_index)
if v is None: if v is None:
+198 -45
View File
@@ -181,9 +181,13 @@ class SQLiteProgram(SQLiteClosable):
def clearBindings(self): def clearBindings(self):
self._bindings.clear() 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): def _bind_parameters(self, cursor: _sqlite3.Cursor):
for idx, val in sorted(self._bindings.items()): for idx in sorted(self._bindings.keys()):
cursor.execute(f"PRAGMA dummy={idx}") cursor.bind(idx, self._bindings[idx])
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -199,7 +203,8 @@ class SQLiteStatement(SQLiteProgram):
def execute(self): def execute(self):
try: try:
conn = self._db._acquire_connection() conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values())) conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit() conn.commit()
except _sqlite3.DatabaseError as e: except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql) raise _map_sqlite_error(str(e), self._sql)
@@ -207,7 +212,8 @@ class SQLiteStatement(SQLiteProgram):
def executeUpdateDelete(self) -> int: def executeUpdateDelete(self) -> int:
try: try:
conn = self._db._acquire_connection() conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values())) cursor = conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit() conn.commit()
return cursor.rowcount return cursor.rowcount
except _sqlite3.DatabaseError as e: except _sqlite3.DatabaseError as e:
@@ -216,7 +222,8 @@ class SQLiteStatement(SQLiteProgram):
def executeInsert(self) -> int: def executeInsert(self) -> int:
try: try:
conn = self._db._acquire_connection() conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values())) cursor = conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit() conn.commit()
return cursor.lastrowid return cursor.lastrowid
except _sqlite3.DatabaseError as e: except _sqlite3.DatabaseError as e:
@@ -225,8 +232,9 @@ class SQLiteStatement(SQLiteProgram):
def simpleQueryForLong(self) -> int: def simpleQueryForLong(self) -> int:
try: try:
conn = self._db._acquire_connection() 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() row = cursor.fetchone()
if not self._db._in_transaction:
conn.commit() conn.commit()
if row: if row:
return int(row[0]) if row[0] is not None else 0 return int(row[0]) if row[0] is not None else 0
@@ -237,8 +245,9 @@ class SQLiteStatement(SQLiteProgram):
def simpleQueryForString(self) -> Optional[str]: def simpleQueryForString(self) -> Optional[str]:
try: try:
conn = self._db._acquire_connection() 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() row = cursor.fetchone()
if not self._db._in_transaction:
conn.commit() conn.commit()
if row: if row:
return str(row[0]) if row[0] is not None else None return str(row[0]) if row[0] is not None else None
@@ -246,6 +255,33 @@ class SQLiteStatement(SQLiteProgram):
except _sqlite3.DatabaseError as e: except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql) 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 # SQLiteQuery
@@ -313,10 +349,17 @@ class SQLiteCursor(AbstractCursor):
def moveToPosition(self, position: int) -> bool: def moveToPosition(self, position: int) -> bool:
self._load() 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 return False
self._pos = position self._pos = position
if 0 <= position < len(self._rows): if 0 <= position < count:
self._current_row = self._rows[position] self._current_row = self._rows[position]
else: else:
self._current_row = None self._current_row = None
@@ -499,6 +542,39 @@ def _map_sqlite_error(msg: str, sql: str = "") -> SQLiteException:
return SQLiteException(msg) 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): class SQLiteDatabase(SQLiteClosable):
"""SQLiteDatabase — 数据库操作类""" """SQLiteDatabase — 数据库操作类"""
@@ -536,10 +612,10 @@ class SQLiteDatabase(SQLiteClosable):
try: try:
db._conn = _sqlite3.connect(path) db._conn = _sqlite3.connect(path)
db._conn.execute("PRAGMA journal_mode=WAL") 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() row = conn_cursor.fetchone()
if row: if row:
db._version = row[0] db._version = int(row[0])
except _sqlite3.DatabaseError as e: except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e)) raise _map_sqlite_error(str(e))
SQLiteDatabase._db_registry[path] = db SQLiteDatabase._db_registry[path] = db
@@ -610,23 +686,37 @@ class SQLiteDatabase(SQLiteClosable):
self._ensure_open() self._ensure_open()
if values is None: if values is None:
return -1 return -1
cols = [] items = _extract_value_pairs(values)
vals = [] if not items:
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:
cols = ["_id"] 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) col_str = ", ".join(cols)
placeholders = ", ".join(["?"] * len(vals)) placeholders = ", ".join(["?"] * len(vals))
sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})" sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})"
@@ -639,23 +729,41 @@ class SQLiteDatabase(SQLiteClosable):
except _sqlite3.DatabaseError as e: except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), sql) 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, def update(self, table: str, values: Any,
where_clause: Optional[str] = None, where_clause: Optional[str] = None,
where_args: Optional[List[Any]] = None) -> int: where_args: Optional[List[Any]] = None) -> int:
self._ensure_open() self._ensure_open()
if values is None: if values is None:
return 0 return 0
set_pairs = [] items = _extract_value_pairs(values)
vals = [] if not items:
if isinstance(values, dict):
items = list(values.items())
elif hasattr(values, "valueSet"):
items = list(values.valueSet())
else:
return 0 return 0
for k, v in items: set_pairs = [f"{k} = ?" for k, _ in items]
set_pairs.append(f"{k} = ?") vals: List[Any] = [v for _, v in items]
vals.append(v)
set_str = ", ".join(set_pairs) set_str = ", ".join(set_pairs)
sql = f"UPDATE {table} SET {set_str}" sql = f"UPDATE {table} SET {set_str}"
if where_clause: if where_clause:
@@ -748,13 +856,24 @@ class SQLiteDatabase(SQLiteClosable):
return self._path return self._path
def getVersion(self) -> int: 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 return self._version
def setVersion(self, version: int): def setVersion(self, version: int):
self._version = version self._version = int(version)
if self._conn: if self._conn:
try: 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: except _sqlite3.DatabaseError:
pass pass
@@ -784,20 +903,54 @@ class SQLiteDatabase(SQLiteClosable):
return self._enable_wal return self._enable_wal
def getMaximumSize(self) -> int: 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 return self._max_size
def getPageSize(self) -> int: 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 return self._page_size
def setMaximumSize(self, size: int) -> int: def setMaximumSize(self, size: int) -> int:
self._max_size = size self._max_size = int(size)
return size
def setPageSize(self, size: int):
self._page_size = size
if self._conn: if self._conn:
try: 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: except _sqlite3.DatabaseError:
pass pass
+653 -32
View File
@@ -8,6 +8,7 @@ import math
import struct import struct
import io import io
import os import os
import time
from enum import IntEnum from enum import IntEnum
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
@@ -732,6 +733,42 @@ class Path(Stub):
pass pass
def op(self, other: "Path", op: int) -> bool: 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 return False
def isRect(self) -> bool: def isRect(self) -> bool:
@@ -770,38 +807,219 @@ class PathMeasure(Stub):
super().__init__() super().__init__()
self._path = path self._path = path
self._force_closed = forceClosed 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): def setPath(self, path: Path, forceClosed: bool):
self._path = path self._path = path
self._force_closed = forceClosed 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: def getLength(self) -> float:
if not self._path: """返回当前轮廓的长度。"""
if self._contour_lengths:
return self._contour_lengths[0]
return 0.0 return 0.0
bounds = self._path.computeBounds()
return math.sqrt(bounds.width() ** 2 + bounds.height() ** 2)
def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool: def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool:
if not self._path: """获取指定距离处的坐标和切线方向。"""
if not self._contours:
return False 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: if len(pos) >= 2:
pos[0] = 0.0 pos[0] = x
pos[1] = 0.0 pos[1] = y
if len(tan) >= 2: if len(tan) >= 2:
tan[0] = 1.0 tan[0] = tx
tan[1] = 0.0 tan[1] = ty
return True return True
def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool: def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool:
"""获取指定距离处的变换矩阵(平移+旋转)。"""
pos = [0.0, 0.0]
tan = [0.0, 0.0]
if not self.getPosTan(distance, pos, tan):
return False 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: def getSegment(self, startD: float, stopD: float, dst: Path, startWithMoveTo: bool) -> bool:
"""提取路径中 [startD, stopD] 区间的子路径。"""
if not self._contours:
return False 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: def isClosed(self) -> bool:
if not self._path or not self._path._segments:
return self._force_closed 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: def nextContour(self) -> bool:
"""移动到下一个轮廓。"""
if len(self._contours) > 1:
self._contours.pop(0)
self._contour_lengths.pop(0)
return True
return False return False
@@ -1556,35 +1774,99 @@ class Camera(Stub):
self._location_x = 0.0 self._location_x = 0.0
self._location_y = 0.0 self._location_y = 0.0
self._location_z = -8.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): 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): 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): def translate(self, x: float, y: float, z: float):
self._location_x += x self._trans_x += x
self._location_y += y self._trans_y += y
self._location_z += z self._trans_z += z
def rotateX(self, deg: float): def rotateX(self, deg: float):
pass """绕 X 轴旋转(累积角度)。"""
self._rot_x += deg
def rotateY(self, deg: float): def rotateY(self, deg: float):
pass """绕 Y 轴旋转(累积角度)。"""
self._rot_y += deg
def rotateZ(self, deg: float): def rotateZ(self, deg: float):
pass """绕 Z 轴旋转(累积角度)。"""
self._rot_z += deg
def rotate(self, x: float, y: float, z: float): 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): def getMatrix(self, matrix: Matrix):
matrix.reset() """计算变换矩阵并写入给定的 Matrix。"""
matrix._values = self._compute_matrix()
def applyToCanvas(self, canvas: "Canvas"): 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): def setLocation(self, x: float, y: float, z: float):
self._location_x = x self._location_x = x
@@ -1592,7 +1874,17 @@ class Camera(Stub):
self._location_z = z self._location_z = z
def dotWithNormal(self, dx: float, dy: float, dz: float) -> float: 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,7 +1952,14 @@ class Picture(Stub):
self._canvas = None self._canvas = None
def draw(self, canvas: "Canvas"): def draw(self, canvas: "Canvas"):
"""回放录制的绘制命令到指定 Canvas。"""
for cmd in self._recorded: for cmd in self._recorded:
method = cmd.get("method")
args = cmd.get("args", [])
if method and hasattr(canvas, method):
try:
getattr(canvas, method)(*args)
except Exception:
pass pass
def getWidth(self) -> int: def getWidth(self) -> int:
@@ -1690,7 +1989,9 @@ class PictureDrawable(Stub):
self._picture = picture self._picture = picture
def draw(self, canvas: "Canvas"): 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__() super().__init__()
self._tex_name = texName self._tex_name = texName
self._timestamp = 0 self._timestamp = 0
self._buffer_width = 0
self._buffer_height = 0
self._attached = texName != 0
self._released = False
def getTimestamp(self) -> int: def getTimestamp(self) -> int:
return self._timestamp return self._timestamp
@@ -1712,16 +2017,24 @@ class SurfaceTexture(Stub):
self._timestamp = int(time.time() * 1e9) self._timestamp = int(time.time() * 1e9)
def release(self): def release(self):
pass """释放 SurfaceTexture 资源。"""
self._released = True
self._attached = False
def setDefaultBufferSize(self, width: int, height: int): def setDefaultBufferSize(self, width: int, height: int):
pass """设置默认缓冲区大小。"""
self._buffer_width = width
self._buffer_height = height
def attachToGLContext(self, texName: int): def attachToGLContext(self, texName: int):
pass """将 SurfaceTexture 附加到 GL 纹理。"""
self._tex_name = texName
self._attached = True
self._released = False
def detachFromGLContext(self): def detachFromGLContext(self):
pass """从 GL 纹理分离 SurfaceTexture。"""
self._attached = False
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -1768,6 +2081,59 @@ class Region(Stub):
return self._rect.contains(x, y) return self._rect.contains(x, y)
def op(self, *args) -> bool: 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 return False
def isRect(self) -> bool: def isRect(self) -> bool:
@@ -1894,10 +2260,32 @@ class Paint(Stub):
def setTextSize(self, textSize: float): def setTextSize(self, textSize: float):
self._text_size = textSize self._text_size = textSize
self._font_cache = None
def getTextSize(self) -> float: def getTextSize(self) -> float:
return self._text_size 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]): def setTypeface(self, typeface: Optional[Typeface]):
self._typeface = typeface self._typeface = typeface
@@ -2004,12 +2392,30 @@ class Paint(Stub):
self._shadow_layer = None self._shadow_layer = None
def measureText(self, text: str) -> float: 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 return len(text) * self._text_size * 0.5
def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int: def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int:
"""在最大宽度内截断文本,返回可容纳的字符数。"""
font = self._get_font()
width = 0.0 width = 0.0
count = 0 count = 0
for i, ch in enumerate(text): for i, ch in enumerate(text):
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 cw = self._text_size * 0.5
if width + cw > maxWidth: if width + cw > maxWidth:
break break
@@ -2019,9 +2425,16 @@ class Paint(Stub):
return count return count
def getTextWidths(self, text: str, widths: List[float]) -> int: 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): 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) return len(text)
def getTextPath(self, text: str, start: int, end: int, x: float, y: float, path: Path): 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) path.lineTo(x + (i - start + 1) * self._text_size * 0.3, y)
def getFontMetrics(self) -> Dict[str, float]: def getFontMetrics(self) -> Dict[str, float]:
"""返回字体度量信息。优先使用 PIL ImageFont 获取真实度量。"""
size = self._text_size 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 { return {
"ascent": -size * 0.8, "ascent": -size * 0.8,
"descent": size * 0.2, "descent": size * 0.2,
@@ -2049,6 +2477,82 @@ class Paint(Stub):
def descent(self) -> float: def descent(self) -> float:
return self._text_size * 0.2 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 # Bitmap
@@ -2249,13 +2753,30 @@ class Bitmap(Stub):
return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8) return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8)
def sameAs(self, other: "Bitmap") -> bool: def sameAs(self, other: "Bitmap") -> bool:
"""比较两个 Bitmap 的像素是否完全相同。"""
if not isinstance(other, Bitmap): if not isinstance(other, Bitmap):
return False return False
if self._width != other._width or self._height != other._height: if self._width != other._width or self._height != other._height:
return False 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 return True
def prepareToDraw(self): def prepareToDraw(self):
"""提示将位图数据上传到 GPU 缓存。桌面端为空操作。"""
pass pass
def hasAlpha(self) -> bool: def hasAlpha(self) -> bool:
@@ -2344,6 +2865,29 @@ class BitmapFactory(Stub):
@staticmethod @staticmethod
def decodeResource(res, resId: int, opts: Options = None) -> Optional[Bitmap]: 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) return Bitmap(100, 100, Bitmap.Config.ARGB_8888)
@staticmethod @staticmethod
@@ -2547,7 +3091,22 @@ class Canvas(Stub):
return Rect(r.left, r.top, r.right, r.bottom) return Rect(r.left, r.top, r.right, r.bottom)
def quickReject(self, *args) -> bool: def quickReject(self, *args) -> bool:
"""判断给定矩形是否完全在裁剪区域之外。"""
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 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): def drawColor(self, color: int):
if HAS_PIL and self._draw is not None: 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) self._draw.text((x, y), text, fill=fill_color)
def drawTextOnPath(self, text: str, path: Path, hOffset: float, vOffset: float, paint: Paint): 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): def drawPosText(self, text: str, pos: List[float], paint: Paint):
if HAS_PIL and self._draw is not None: 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) self._draw.text((pos[i * 2], pos[i * 2 + 1]), ch, fill=fill_color)
def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None): def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None):
"""回放 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 pass
def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None): 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, def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float,
inner: RectF, innerRx: float, innerRy: float, paint: Paint): 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, def drawBitmapMesh(self, bitmap: Bitmap, meshWidth: int, meshHeight: int,
verts: List[float], vertOffset: int, colors: List[int], verts: List[float], vertOffset: int, colors: List[int],
@@ -2785,10 +3404,12 @@ class Canvas(Stub):
return False return False
def setDensity(self, density: int): def setDensity(self, density: int):
pass """设置画布密度(DPI)。"""
self._density = density
def getDensity(self) -> int: def getDensity(self) -> int:
return 0 """返回画布密度(DPI),默认 0 表示未知。"""
return getattr(self, "_density", 0)
def toString(self) -> str: def toString(self) -> str:
return f"Canvas({self._width}x{self._height})" return f"Canvas({self._width}x{self._height})"
+228 -15
View File
@@ -10,6 +10,19 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL 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 # Sensor
@@ -251,14 +264,89 @@ class SensorManager(Stub):
self._sensors[type_] = Sensor(type_) self._sensors[type_] = Sensor(type_)
def getDefaultSensor(self, type_: int) -> Optional[Sensor]: 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_) 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]: def getSensorList(self, type_: int) -> List[Sensor]:
if _USE_LINUX:
return self._get_linux_sensor_list(type_)
if type_ == -1: # ALL if type_ == -1: # ALL
return list(self._sensors.values()) return list(self._sensors.values())
sensor = self._sensors.get(type_) sensor = self._sensors.get(type_)
return [sensor] if sensor else [] 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]: def getSensors(self) -> List[Sensor]:
return list(self._sensors.values()) return list(self._sensors.values())
@@ -297,18 +385,56 @@ class SensorManager(Stub):
@staticmethod @staticmethod
def getRotationMatrix(R: List[float], I: List[float], def getRotationMatrix(R: List[float], I: List[float],
gravity: List[float], geomagnetic: List[float]) -> bool: gravity: List[float], geomagnetic: List[float]) -> bool:
"""根据重力和地磁向量计算旋转矩阵 R 和倾斜矩阵 I。
算法对齐 Android SensorManager.getRotationMatrix。"""
if len(R) < 9: if len(R) < 9:
R.extend([0] * (9 - len(R))) R.extend([0.0] * (9 - len(R)))
if len(R) >= 9: gx, gy, gz = gravity[0], gravity[1], gravity[2]
R[0] = 1.0 mx, my, mz = geomagnetic[0], geomagnetic[1], geomagnetic[2]
R[1] = 0.0 # 归一化重力向量 -> H
R[2] = 0.0 g_len = math.sqrt(gx * gx + gy * gy + gz * gz)
R[3] = 0.0 if g_len < 1e-10:
R[4] = 1.0 return False
R[5] = 0.0 hx = gx / g_len
R[6] = 0.0 hy = gy / g_len
R[7] = 0.0 hz = gz / g_len
R[8] = 1.0 # 叉积 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 return True
@staticmethod @staticmethod
@@ -317,17 +443,69 @@ class SensorManager(Stub):
@staticmethod @staticmethod
def getInclination(I: List[float]) -> float: 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 return 0.0
@staticmethod @staticmethod
def remapCoordinateSystem(inR: List[float], X: int, Y: int, def remapCoordinateSystem(inR: List[float], X: int, Y: int,
outR: List[float]) -> bool: outR: List[float]) -> bool:
if len(inR) >= 9 and len(outR) >= 9: """根据指定的 X/Y 轴重映射坐标系。"""
for i in range(9): if len(inR) < 9 or len(outR) < 9:
outR[i] = inR[i] 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 return True
def getDeviceId(self) -> str: 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" return "sensor_hub_0"
@@ -470,6 +648,29 @@ class BatteryManager(Stub):
super().__init__() super().__init__()
def getIntProperty(self, id: int) -> int: 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 = { id_values = {
BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER: 3500, BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER: 3500,
BatteryManager.BATTERY_PROPERTY_CAPACITY: 85, BatteryManager.BATTERY_PROPERTY_CAPACITY: 85,
@@ -478,7 +679,19 @@ class BatteryManager(Stub):
return id_values.get(id, 0) return id_values.get(id, 0)
def getLongProperty(self, id: int) -> int: 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)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
+18 -10
View File
@@ -13,6 +13,10 @@ import random
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
from .._core import Stub, AndroidOnlyError 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__ = [ __all__ = [
"Sensor", "SensorEvent", "SensorEventListener", "Sensor", "SensorEvent", "SensorEventListener",
@@ -101,8 +105,8 @@ AXIS_MINUS_Z = 6
# Sensor (扩展) # Sensor (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class Sensor(Stub): class Sensor(_BaseSensor):
"""Sensor — 传感器 (完整版)""" """Sensor — 传感器 (完整版,继承 hardware.Sensor 的所有方法)"""
TYPE_ACCELEROMETER = TYPE_ACCELEROMETER TYPE_ACCELEROMETER = TYPE_ACCELEROMETER
TYPE_MAGNETIC_FIELD = TYPE_MAGNETIC_FIELD TYPE_MAGNETIC_FIELD = TYPE_MAGNETIC_FIELD
@@ -153,7 +157,8 @@ class Sensor(Stub):
fifo_reserved_event_count: int = 0, fifo_reserved_event_count: int = 0,
fifo_max_event_count: int = 0, fifo_max_event_count: int = 0,
id_: int = 0): id_: int = 0):
super().__init__() super().__init__(type_, name, vendor, version, resolution,
max_range, power, min_delay)
self._type = type_ self._type = type_
self._name = name self._name = name
self._vendor = vendor self._vendor = vendor
@@ -248,14 +253,17 @@ class Sensor(Stub):
# SensorEvent # SensorEvent
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class SensorEvent(Stub): class SensorEvent(_BaseSensorEvent):
"""SensorEvent — 传感器事件""" """SensorEvent — 传感器事件 (继承 hardware.SensorEvent 的所有方法)"""
def __init__(self, sensor: Optional[Sensor] = None, def __init__(self, sensor: Optional[Sensor] = None,
values: List[float] = None, values: List[float] = None,
accuracy: int = SENSOR_STATUS_ACCURACY_HIGH, accuracy: int = SENSOR_STATUS_ACCURACY_HIGH,
timestamp: int = 0): timestamp: int = 0):
super().__init__() if sensor is not None:
super().__init__(sensor, accuracy)
else:
Stub.__init__(self)
self.sensor = sensor self.sensor = sensor
self.values = values or [0.0, 0.0, 0.0] self.values = values or [0.0, 0.0, 0.0]
self.accuracy = accuracy self.accuracy = accuracy
@@ -290,8 +298,8 @@ class SensorEvent(Stub):
# SensorEventListener # SensorEventListener
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class SensorEventListener(Stub): class SensorEventListener(_BaseSensorEventListener):
"""SensorEventListener — 传感器事件监听器""" """SensorEventListener — 传感器事件监听器 (继承 hardware.SensorEventListener 的所有方法)"""
def onSensorChanged(self, event: SensorEvent): def onSensorChanged(self, event: SensorEvent):
pass pass
@@ -313,8 +321,8 @@ class SensorEventListener(Stub):
# SensorManager (扩展) # SensorManager (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class SensorManager(Stub): class SensorManager(_BaseSensorManager):
"""SensorManager — 传感器管理器 (完整版)""" """SensorManager — 传感器管理器 (完整版,继承 hardware.SensorManager)"""
AXIS_X = AXIS_X AXIS_X = AXIS_X
AXIS_Y = AXIS_Y AXIS_Y = AXIS_Y
+216 -5
View File
@@ -16,6 +16,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name, _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 # Location
@@ -614,6 +620,9 @@ class GnssStatus(Stub):
return 0.0 return 0.0
def getBasebandCn0DbHz(self, index: int) -> float: 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 return 0.0
def getCn0DbHz(self, index: int) -> float: def getCn0DbHz(self, index: int) -> float:
@@ -867,6 +876,42 @@ class Address(Stub):
def setFeatureName(self, name: str): def setFeatureName(self, name: str):
self._feature_name = name 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): def setLatitude(self, latitude: float):
self._latitude = latitude self._latitude = latitude
self._has_lat_lon = True self._has_lat_lon = True
@@ -892,29 +937,144 @@ class Address(Stub):
class Geocoder(Stub): class Geocoder(Stub):
"""Geocoder — 地理编码/逆地理编码""" """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): def __init__(self, context: Any = None, locale: Any = None):
super().__init__() super().__init__()
self._context = context 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, def getFromLocation(self, latitude: float, longitude: float,
max_results: int = 1) -> List[Address]: 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 = Address()
addr.setLatitude(latitude) addr.setLatitude(latitude)
addr.setLongitude(longitude) addr.setLongitude(longitude)
addr.setFeatureName("Unknown Location") addr.setFeatureName("Unknown Location")
addr.setLocality("Unknown") addr.setLocality("Unknown")
addr.setCountryName("Unknown") addr.setCountryName("Unknown")
return [addr] if max_results > 0 else [] results.append(addr)
return results[:max_results] if max_results > 0 else []
def _parse_nominatim_reverse(self, data: dict) -> Address:
"""解析 Nominatim 逆地理编码响应。"""
addr = Address()
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, def getFromLocationName(self, location_name: str, max_results: int = 1,
lower_left_lat: Optional[float] = None, lower_left_lat: Optional[float] = None,
lower_left_lon: Optional[float] = None, lower_left_lon: Optional[float] = None,
upper_right_lat: Optional[float] = None, upper_right_lat: Optional[float] = None,
upper_right_lon: Optional[float] = None) -> List[Address]: 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 = Address()
addr.setFeatureName(location_name) addr.setFeatureName(location_name)
addr.setLocality(location_name) addr.setLocality(location_name)
return [addr] if max_results > 0 else [] results.append(addr)
return results[:max_results] if max_results > 0 else []
def _parse_nominatim_search(self, item: dict) -> Address:
"""解析 Nominatim 搜索响应。"""
addr = Address()
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._last_known_locations: Dict[str, Location] = {}
self._enabled_providers = {self.GPS_PROVIDER, self.NETWORK_PROVIDER, self._enabled_providers = {self.GPS_PROVIDER, self.NETWORK_PROVIDER,
self.PASSIVE_PROVIDER, self.FUSED_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]: 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: if provider in self._last_known_locations:
return self._last_known_locations[provider] return self._last_known_locations[provider]
loc = Location(provider) loc = Location(provider)
@@ -969,6 +1141,18 @@ class LocationManager(Stub):
provider = provider_or_request provider = provider_or_request
else: else:
provider = self.GPS_PROVIDER 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 listener_or_callback is not None:
if provider not in self._listeners: if provider not in self._listeners:
self._listeners[provider] = [] self._listeners[provider] = []
@@ -984,9 +1168,13 @@ class LocationManager(Stub):
del self._listeners[provider] del self._listeners[provider]
def isProviderEnabled(self, provider: str) -> bool: def isProviderEnabled(self, provider: str) -> bool:
if _USE_LINUX:
return provider == "network"
return provider in self._enabled_providers return provider in self._enabled_providers
def getProviders(self, criteria: Optional[Criteria] = None, enabled_only: bool = False) -> List[str]: def getProviders(self, criteria: Optional[Criteria] = None, enabled_only: bool = False) -> List[str]:
if _USE_LINUX:
return ["network"]
providers = list(self._providers.keys()) providers = list(self._providers.keys())
if enabled_only: if enabled_only:
providers = [p for p in providers if p in self._enabled_providers] 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, def addProximityAlert(self, latitude: float, longitude: float,
radius: float, expiration: int, intent: Any): 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: def sendExtraCommand(self, provider: str, command: str, extras: Any) -> bool:
"""向位置提供者发送额外命令。桌面端无实际效果,返回成功。"""
return True return True
def clearTestProviderLocation(self, provider: str): def clearTestProviderLocation(self, provider: str):
pass """清除测试提供者的位置数据。"""
self._test_locations.pop(provider, None)
def setTestProviderLocation(self, provider: str, location: Location): def setTestProviderLocation(self, provider: str, location: Location):
"""设置测试提供者的位置。"""
self._test_locations[provider] = location
# 通知该提供者的监听器
if provider in self._listeners:
for listener in self._listeners[provider]:
try:
listener.onLocationChanged(location)
except Exception:
pass pass
def removeTestProvider(self, provider: str): 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, def addTestProvider(self, name: str, requires_network: bool, requires_satellite: bool,
requires_cell: bool, has_monetary_cost: bool, supports_altitude: bool, requires_cell: bool, has_monetary_cost: bool, supports_altitude: bool,
+53 -2
View File
@@ -20,6 +20,13 @@ from .._core import Stub, AndroidOnlyError, _app_data_dir, _sdcard_root
from ..java.lang import RuntimeException 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,), {}) IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
@@ -517,14 +524,58 @@ class ConnectivityManager(Stub):
self._active_network: Optional[Any] = None self._active_network: Optional[Any] = None
self._all_networks: List[Any] = [] 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]: def getActiveNetworkInfo(self) -> Optional[Any]:
if _USE_LINUX:
return self._get_linux_active_info()
return self._active_network_info return self._active_network_info
def getNetworkInfo(self, networkType: int) -> Optional[Any]: 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 return None
def getNetworkInfo(self, network: Any) -> Optional[Any]: def getConnectivityStatus(self) -> bool:
return None """返回当前网络连通性 (True=已连接)"""
if _USE_LINUX:
return LinuxNetwork.get_connectivity()
return False
def getAllNetworkInfo(self) -> List[Any]: def getAllNetworkInfo(self) -> List[Any]:
return self._all_networks return self._all_networks
+511 -35
View File
@@ -4,8 +4,10 @@ Android Media 组件 — MediaPlayer, MediaRecorder, SoundPool, AudioManager 等
""" """
import io import io
import json
import math import math
import os import os
import subprocess
import time import time
import struct import struct
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union 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 .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, resolve_android_path
from .graphics import PointF 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 # AudioAttributes
@@ -236,6 +352,7 @@ class MediaPlayer(Stub):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._data_source = None self._data_source = None
self._resolved_path = None
self._prepared = False self._prepared = False
self._playing = False self._playing = False
self._looping = False self._looping = False
@@ -250,6 +367,8 @@ class MediaPlayer(Stub):
self._audio_attributes = None self._audio_attributes = None
self._display = None self._display = None
self._surface = None self._surface = None
self._released = False
self._start_time = 0.0 # 用于模拟播放进度
# Listeners # Listeners
self._on_prepared_listener = None self._on_prepared_listener = None
@@ -260,43 +379,126 @@ class MediaPlayer(Stub):
self._on_video_size_changed_listener = None self._on_video_size_changed_listener = None
def setDataSource(self, path: str = None, uri=None, headers: Dict = 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): 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 self._prepared = True
def prepareAsync(self): 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 self._prepared = True
if self._on_prepared_listener: if self._on_prepared_listener:
self._on_prepared_listener.onPrepared(self) self._on_prepared_listener.onPrepared(self)
def start(self): def start(self):
if self._released:
return
self._playing = True self._playing = True
self._start_time = time.time()
def stop(self): def stop(self):
self._playing = False self._playing = False
def pause(self): 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 self._playing = False
def reset(self): def reset(self):
self._playing = False self._playing = False
self._prepared = False self._prepared = False
self._current_position = 0 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): def release(self):
self._playing = False self._playing = False
self._prepared = 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): def seekTo(self, msec: int):
self._current_position = max(0, min(msec, self._duration)) self._current_position = max(0, min(msec, self._duration))
self._start_time = time.time()
if self._on_seek_complete_listener: if self._on_seek_complete_listener:
self._on_seek_complete_listener.onSeekComplete(self) self._on_seek_complete_listener.onSeekComplete(self)
def getCurrentPosition(self) -> int: 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 return self._current_position
def getDuration(self) -> int: 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 return self._duration
def isPlaying(self) -> bool: def isPlaying(self) -> bool:
@@ -328,9 +530,25 @@ class MediaPlayer(Stub):
self._display = surface_holder self._display = surface_holder
def getVideoWidth(self) -> int: 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 return self._video_width
def getVideoHeight(self) -> int: 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 return self._video_height
def setOnPreparedListener(self, listener: "MediaPlayer.OnPreparedListener"): def setOnPreparedListener(self, listener: "MediaPlayer.OnPreparedListener"):
@@ -427,6 +645,15 @@ class MediaRecorder(Stub):
self._output_file = None self._output_file = None
self._preview_display = None self._preview_display = None
self._recording = False 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): def setAudioSource(self, audio_source: int):
self._audio_source = audio_source self._audio_source = audio_source
@@ -446,13 +673,36 @@ class MediaRecorder(Stub):
def setOutputFile(self, path: str): def setOutputFile(self, path: str):
self._output_file = path 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): def setPreviewDisplay(self, surface_holder):
self._preview_display = surface_holder self._preview_display = surface_holder
def prepare(self): def prepare(self):
pass if self._released:
return
self._prepared = True
def start(self): def start(self):
if self._released or not self._prepared:
return
self._recording = True self._recording = True
def stop(self): def stop(self):
@@ -460,11 +710,29 @@ class MediaRecorder(Stub):
def reset(self): def reset(self):
self._recording = False 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): def release(self):
self._recording = False self._recording = False
self._prepared = False
self._released = True
self._output_file = None
self._preview_display = None
def getMaxAmplitude(self) -> int: def getMaxAmplitude(self) -> int:
if self._recording:
import random
return random.randint(0, 32767)
return 0 return 0
@@ -596,15 +864,37 @@ class AudioManager(Stub):
self._parameters: Dict[str, str] = {} self._parameters: Dict[str, str] = {}
def getStreamVolume(self, streamType: int) -> int: def getStreamVolume(self, streamType: int) -> int:
if _USE_LINUX:
# STREAM_MUSIC 及其它流类型映射到 Linux 主音量
return LinuxAudio.get_volume()
return self._stream_volumes.get(streamType, 0) return self._stream_volumes.get(streamType, 0)
def getStreamMaxVolume(self, streamType: int) -> int: def getStreamMaxVolume(self, streamType: int) -> int:
if _USE_LINUX:
return 100
return 15 return 15
def setStreamVolume(self, streamType: int, index: int, flags: int = 0): 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: if streamType in self._stream_volumes:
self._stream_volumes[streamType] = max(0, min(15, index)) 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): def adjustVolume(self, direction: int, flags: int = 0):
pass pass
@@ -996,53 +1286,221 @@ class MediaMetadataRetriever(Stub):
METADATA_KEY_BITRATE = "bitrate" METADATA_KEY_BITRATE = "bitrate"
METADATA_KEY_LOCATION = "location" 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): def __init__(self):
super().__init__() super().__init__()
self._metadata: Dict[str, str] = {} self._metadata: Dict[str, str] = {}
self._resolved_path: Optional[str] = None
self._ffprobe_data: Optional[Dict[str, Any]] = None
def setDataSource(self, path: str): def setDataSource(self, path: str, headers: Optional[Dict[str, str]] = None):
resolved = resolve_android_path(path) """设置数据源并用 ffprobe 加载元数据。"""
if os.path.exists(resolved): self._resolved_path = resolve_android_path(path)
size = os.path.getsize(resolved) self._metadata = {}
self._metadata = { self._ffprobe_data = None
MediaMetadataRetriever.METADATA_KEY_DURATION: "30000", if not self._resolved_path or not os.path.exists(self._resolved_path):
MediaMetadataRetriever.METADATA_KEY_TITLE: os.path.basename(resolved), return
MediaMetadataRetriever.METADATA_KEY_MIMETYPE: "video/mp4", # 用 ffprobe 获取真实元数据(try/except 保护)
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH: "1920", try:
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT: "1080", self._ffprobe_data = _ffprobe_metadata(self._resolved_path)
MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO: "yes", except Exception:
MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO: "yes", self._ffprobe_data = None
MediaMetadataRetriever.METADATA_KEY_BITRATE: str(size * 8 // 30), if self._ffprobe_data:
} self._parse_ffprobe_metadata()
def setDataSource(self, path: str, headers: Dict[str, str]): def _parse_ffprobe_metadata(self):
self.setDataSource(path) """将 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]: def extractMetadata(self, keyCode: str) -> Optional[str]:
return self._metadata.get(keyCode) return self._metadata.get(keyCode)
def getFrameAtTime(self, timeUs: int = -1, option: int = 0) -> Optional["Bitmap"]: def getEmbeddedPicture(self) -> Optional[bytes]:
if HAS_PIL: """用 ffmpeg 提取嵌入的封面图片,返回原始字节数据。
from PIL import Image 失败返回 None。"""
from .graphics import Bitmap if not self._resolved_path or not os.path.exists(self._resolved_path):
img = Image.new("RGB", (1920, 1080), (64, 64, 64)) return None
bm = Bitmap(1920, 1080, Bitmap.Config.ARGB_8888) try:
bm._image = img return _ffmpeg_extract_cover(self._resolved_path)
return bm except Exception:
return None return None
def getScaledFrameAtTime(self, timeUs: int, option: int, width: int, height: int) -> Optional["Bitmap"]: def getFrameAtTime(self, timeUs: int = -1, option: int = 0) -> Optional["Bitmap"]:
if HAS_PIL: """用 ffmpeg 提取指定时间点的视频帧,返回 Bitmap(内含 PIL Image)。
from PIL import 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 from .graphics import Bitmap
img = Image.new("RGB", (width, height), (64, 64, 64)) w, h = img.size
bm = Bitmap(width, height, Bitmap.Config.ARGB_8888) bm = Bitmap(w, h, Bitmap.Config.ARGB_8888)
# 确保 RGBA 模式
if img.mode != "RGBA":
img = img.convert("RGBA")
bm._image = img bm._image = img
return bm return bm
except Exception:
pass
return None
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 return None
def release(self): def release(self):
self._metadata.clear() self._metadata.clear()
self._resolved_path = None
self._ffprobe_data = None
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -1788,6 +2246,7 @@ class ExifInterface(Stub):
super().__init__() super().__init__()
self._filename = resolve_android_path(filename) self._filename = resolve_android_path(filename)
self._attributes: Dict[str, str] = {} self._attributes: Dict[str, str] = {}
self._pil_image = None
self._load_exif() self._load_exif()
def _load_exif(self): def _load_exif(self):
@@ -1799,6 +2258,7 @@ class ExifInterface(Stub):
from PIL import ExifTags from PIL import ExifTags
try: try:
img = Image.open(self._filename) img = Image.open(self._filename)
self._pil_image = img
exif_data = img._getexif() exif_data = img._getexif()
if exif_data: if exif_data:
for tag_id, value in exif_data.items(): for tag_id, value in exif_data.items():
@@ -1870,13 +2330,29 @@ class ExifInterface(Stub):
pass pass
def flipHorizontally(self): 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): 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): def rotate(self, degrees):
pass 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)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
+197 -35
View File
@@ -17,6 +17,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name, _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) # Uri (re-export from content)
@@ -379,6 +386,38 @@ class NetworkInfo(Stub):
} }
return names.get(type_, "UNKNOWN") 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: def getType(self) -> int:
return self._type return self._type
@@ -1040,53 +1079,98 @@ class TrafficStats(Stub):
UNSUPPORTED = -1 UNSUPPORTED = -1
TAG_SYSTEM_UPDATE = 0 TAG_SYSTEM_UPDATE = 0
_cached_stats = None
_cache_time = 0
@staticmethod @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 return 0
@staticmethod @staticmethod
def getMobileRxBytes() -> int: def getMobileRxPackets():
return 0 return 0
@staticmethod @staticmethod
def getTotalTxBytes() -> int: def getMobileTxPackets():
return 0 return 0
@staticmethod @staticmethod
def getTotalRxBytes() -> int: def getTotalRxBytes():
return TrafficStats._read_net_dev()["rx_bytes"]
@staticmethod
def getTotalTxBytes():
return TrafficStats._read_net_dev()["tx_bytes"]
@staticmethod
def getTotalRxPackets():
return TrafficStats._read_net_dev()["rx_packets"]
@staticmethod
def getTotalTxPackets():
return TrafficStats._read_net_dev()["tx_packets"]
@staticmethod
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 return 0
@staticmethod @staticmethod
def getMobileTxPackets() -> int: 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 return 0
@staticmethod @staticmethod
def getMobileRxPackets() -> int: def getUidRxPackets(uid):
return 0 return TrafficStats.getUidRxBytes(uid)
@staticmethod @staticmethod
def getTotalTxPackets() -> int: def getUidTxPackets(uid):
return 0 return TrafficStats.getUidTxBytes(uid)
@staticmethod @staticmethod
def getTotalRxPackets() -> int: def getThreadStatsTag():
return 0 return getattr(TrafficStats, '_thread_tag', 0)
@staticmethod @staticmethod
def getUidTxBytes(uid: int) -> int: def setThreadStatsTag(tag):
return 0 TrafficStats._thread_tag = tag
@staticmethod @staticmethod
def getUidRxBytes(uid: int) -> int: def clearThreadStatsTag():
return 0 TrafficStats._thread_tag = 0
@staticmethod
def getUidTxPackets(uid: int) -> int:
return 0
@staticmethod
def getUidRxPackets(uid: int) -> int:
return 0
@staticmethod @staticmethod
def getUidTcpTxBytes(uid: int) -> int: def getUidTcpTxBytes(uid: int) -> int:
@@ -1104,18 +1188,6 @@ class TrafficStats(Stub):
def getUidUdpRxBytes(uid: int) -> int: def getUidUdpRxBytes(uid: int) -> int:
return 0 return 0
@staticmethod
def setThreadStatsTag(tag: int):
pass
@staticmethod
def clearThreadStatsTag():
pass
@staticmethod
def getThreadStatsTag() -> int:
return 0
@staticmethod @staticmethod
def setThreadStatsUid(uid: int): def setThreadStatsUid(uid: int):
pass pass
@@ -1230,22 +1302,112 @@ class ConnectivityManager(Stub):
self._network_caps: Dict[int, NetworkCapabilities] = {} self._network_caps: Dict[int, NetworkCapabilities] = {}
self._link_props: Dict[int, LinkProperties] = {} 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]: def getActiveNetworkInfo(self) -> Optional[NetworkInfo]:
if _USE_LINUX:
return self._get_linux_active_info()
return self._active_network_info return self._active_network_info
def getNetworkInfo(self, network_type: int) -> Optional[NetworkInfo]: 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) network = self._networks.get(network_type)
if network: if network:
return NetworkInfo(type_=network_type, state="CONNECTED", is_available=True) return NetworkInfo(type_=network_type, state="CONNECTED", is_available=True)
return self._active_network_info 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]: def getNetworkInfoForUid(self, network: Network, uid: int) -> Optional[NetworkInfo]:
return self._active_network_info return self._active_network_info
def getAllNetworks(self) -> List[Network]: 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()) return list(self._networks.values())
def getActiveNetwork(self) -> Optional[Network]: 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: if self._networks:
nid = next(iter(self._networks)) nid = next(iter(self._networks))
return self._networks[nid] return self._networks[nid]
+78 -12
View File
@@ -14,6 +14,25 @@ import time
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError 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__ = [ __all__ = [
"Network", "NetworkCapabilities", "NetworkRequest", "NetworkSpecifier", "Network", "NetworkCapabilities", "NetworkRequest", "NetworkSpecifier",
@@ -30,7 +49,7 @@ __all__ = [
# Network # Network
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class Network(Stub): class Network(_BaseNetwork):
"""Network — 网络""" """Network — 网络"""
def __init__(self, netId: int = 0): def __init__(self, netId: int = 0):
@@ -74,7 +93,7 @@ class Network(Stub):
# NetworkCapabilities # NetworkCapabilities
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class NetworkCapabilities(Stub): class NetworkCapabilities(_BaseNetworkCapabilities):
"""NetworkCapabilities — 网络能力""" """NetworkCapabilities — 网络能力"""
TRANSPORT_CELLULAR = 0 TRANSPORT_CELLULAR = 0
@@ -236,7 +255,7 @@ class NetworkCapabilities(Stub):
# NetworkSpecifier / MatchAllNetworkSpecifier # NetworkSpecifier / MatchAllNetworkSpecifier
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class NetworkSpecifier(Stub): class NetworkSpecifier(_BaseNetworkSpecifier):
"""NetworkSpecifier — 网络指定器""" """NetworkSpecifier — 网络指定器"""
def satisfiedBy(self, other: Optional["NetworkSpecifier"]) -> bool: def satisfiedBy(self, other: Optional["NetworkSpecifier"]) -> bool:
@@ -252,7 +271,7 @@ class NetworkSpecifier(Stub):
return id(self) & 0x7FFFFFFF return id(self) & 0x7FFFFFFF
class MatchAllNetworkSpecifier(NetworkSpecifier): class MatchAllNetworkSpecifier(_BaseMatchAllNetworkSpecifier, NetworkSpecifier):
"""MatchAllNetworkSpecifier — 匹配所有网络""" """MatchAllNetworkSpecifier — 匹配所有网络"""
def satisfiedBy(self, other: Optional["NetworkSpecifier"]) -> bool: def satisfiedBy(self, other: Optional["NetworkSpecifier"]) -> bool:
@@ -266,7 +285,7 @@ class MatchAllNetworkSpecifier(NetworkSpecifier):
# NetworkRequest # NetworkRequest
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class NetworkRequest(Stub): class NetworkRequest(_BaseNetworkRequest):
"""NetworkRequest — 网络请求""" """NetworkRequest — 网络请求"""
def __init__(self, capabilities: Optional[NetworkCapabilities] = None, def __init__(self, capabilities: Optional[NetworkCapabilities] = None,
@@ -430,7 +449,7 @@ class NetworkAgent(Stub):
# NetworkInfo (扩展) # NetworkInfo (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class NetworkInfo(Stub): class NetworkInfo(_BaseNetworkInfo):
"""NetworkInfo — 网络信息 (完整版)""" """NetworkInfo — 网络信息 (完整版)"""
TYPE_NONE = -1 TYPE_NONE = -1
@@ -558,7 +577,7 @@ class NetworkInfo(Stub):
# LinkAddress # LinkAddress
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class LinkAddress(Stub): class LinkAddress(_BaseLinkAddress):
"""LinkAddress — 链路地址""" """LinkAddress — 链路地址"""
def __init__(self, address: str = "0.0.0.0", prefixLength: int = 24, def __init__(self, address: str = "0.0.0.0", prefixLength: int = 24,
@@ -600,7 +619,7 @@ class LinkAddress(Stub):
# LinkProperties # LinkProperties
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class LinkProperties(Stub): class LinkProperties(_BaseLinkProperties):
"""LinkProperties — 链路属性""" """LinkProperties — 链路属性"""
def __init__(self): def __init__(self):
@@ -874,7 +893,7 @@ class MobileLinkQualityInfo(LinkQualityInfo):
# TrafficStats # TrafficStats
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class TrafficStats(Stub): class TrafficStats(_BaseTrafficStats):
"""TrafficStats — 流量统计""" """TrafficStats — 流量统计"""
UID_REMOVED = -4 UID_REMOVED = -4
@@ -969,7 +988,7 @@ class TrafficStats(Stub):
# ProxyInfo # ProxyInfo
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ProxyInfo(Stub): class ProxyInfo(_BaseProxyInfo):
"""ProxyInfo — 代理信息""" """ProxyInfo — 代理信息"""
def __init__(self, host: str = "", port: int = 0, def __init__(self, host: str = "", port: int = 0,
@@ -1012,7 +1031,7 @@ class ProxyInfo(Stub):
# DhcpInfo / DhcpInfoInternal # DhcpInfo / DhcpInfoInternal
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class DhcpInfo(Stub): class DhcpInfo(_BaseDhcpInfo):
"""DhcpInfo — DHCP 信息""" """DhcpInfo — DHCP 信息"""
def __init__(self): def __init__(self):
@@ -1245,7 +1264,7 @@ class ConnectivityDiagnosticsManager(Stub):
# ConnectivityManager (扩展) # ConnectivityManager (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ConnectivityManager(Stub): class ConnectivityManager(_BaseConnectivityManager):
"""ConnectivityManager — 连接管理器 (扩展版)""" """ConnectivityManager — 连接管理器 (扩展版)"""
TYPE_NONE = -1 TYPE_NONE = -1
@@ -1291,17 +1310,64 @@ class ConnectivityManager(Stub):
def getActiveNetwork(self) -> Optional[Network]: def getActiveNetwork(self) -> Optional[Network]:
return self._active_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]: def getActiveNetworkInfo(self) -> Optional[NetworkInfo]:
if _USE_LINUX:
return self._get_linux_active_info()
if self._active_network: if self._active_network:
return self._network_infos.get(self._active_network.getNetId()) return self._network_infos.get(self._active_network.getNetId())
return None return None
def getNetworkInfo(self, networkType: int) -> Optional[NetworkInfo]: 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(): for info in self._network_infos.values():
if info.getType() == networkType: if info.getType() == networkType:
return info return info
return None 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]: def getNetworkInfoForNetwork(self, network: Network) -> Optional[NetworkInfo]:
return self._network_infos.get(network.getNetId()) return self._network_infos.get(network.getNetId())
+116
View File
@@ -16,6 +16,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name, _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 # SupplicantState
@@ -620,6 +627,8 @@ class WifiManager(Stub):
self._p2p_channels: List[int] = [] self._p2p_channels: List[int] = []
def getConnectionInfo(self) -> Optional[WifiInfo]: def getConnectionInfo(self) -> Optional[WifiInfo]:
if _USE_LINUX:
return self._get_linux_connection_info()
if self._connection_info is None: if self._connection_info is None:
info = WifiInfo() info = WifiInfo()
info._ssid = '"mock_wifi"' info._ssid = '"mock_wifi"'
@@ -632,20 +641,127 @@ class WifiManager(Stub):
self._connection_info = info self._connection_info = info
return self._connection_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]: 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) 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: def startScan(self) -> bool:
return True return True
def setWifiEnabled(self, enabled: bool) -> bool: 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_enabled = enabled
self._wifi_state = self.WIFI_STATE_ENABLED if enabled else self.WIFI_STATE_DISABLED self._wifi_state = self.WIFI_STATE_ENABLED if enabled else self.WIFI_STATE_DISABLED
return True return True
def isWifiEnabled(self) -> bool: def isWifiEnabled(self) -> bool:
if _USE_LINUX:
return self._linux_has_wireless_interface()
return self._wifi_enabled 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: def getWifiState(self) -> int:
return self._wifi_state return self._wifi_state
+541 -30
View File
@@ -20,6 +20,13 @@ from .._core import (
HAS_PSUTIL, 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 # Build
@@ -108,16 +115,95 @@ class Build(Stub):
@staticmethod @staticmethod
def getRadioVersion() -> str: def getRadioVersion() -> str:
return "" """返回基带/无线电版本。桌面端无无线电硬件,返回空字符串。"""
return Build.RADIO
@staticmethod @staticmethod
def getSerial() -> str: 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 @staticmethod
def getFingerprint() -> str: def getFingerprint() -> str:
return Build.FINGERPRINT 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 # SystemClock
@@ -150,6 +236,16 @@ class SystemClock(Stub):
@staticmethod @staticmethod
def setCurrentTimeMillis(millis: int) -> bool: def setCurrentTimeMillis(millis: int) -> bool:
"""尝试设置系统时间。需要 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 return False
@@ -217,6 +313,20 @@ class Environment(Stub):
@staticmethod @staticmethod
def isExternalStorageRemovable(path: Optional[str] = None) -> bool: 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 return False
@staticmethod @staticmethod
@@ -258,26 +368,41 @@ class Process(Stub):
@staticmethod @staticmethod
def myUid() -> int: def myUid() -> int:
return 10001 """返回当前进程的真实 UID。"""
return _os.getuid()
@staticmethod @staticmethod
def myPid() -> int: def myPid() -> int:
return _os.getpid() return _os.getpid()
@staticmethod @staticmethod
def killProcess(pid: int): def killProcess(pid):
import signal
try:
_os.kill(pid, signal.SIGKILL)
except (OSError, ProcessLookupError):
pass pass
@staticmethod @staticmethod
def sendSignal(pid: int, signal: int): def sendSignal(pid, signal_num):
try:
_os.kill(pid, signal_num)
except (OSError, ProcessLookupError):
pass pass
@staticmethod @staticmethod
def setThreadPriority(priority: int): def setThreadPriority(*args):
pass """设置线程优先级。支持 setThreadPriority(priority) 和 setThreadPriority(tid, priority)。"""
if len(args) == 1:
@staticmethod tid = 0
def setThreadPriority(tid: int, priority: int): priority = args[0]
elif len(args) == 2:
tid, priority = args
else:
return
try:
_os.setpriority(_os.PRIO_PROCESS, tid, priority)
except (OSError, PermissionError):
pass pass
@staticmethod @staticmethod
@@ -945,6 +1070,19 @@ class Looper(Stub):
return self._message_queue return self._message_queue
def dump(self, pw: Any, prefix: str): def dump(self, pw: Any, prefix: str):
"""将 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 pass
def toString(self) -> str: def toString(self) -> str:
@@ -1299,9 +1437,34 @@ class PowerManager(Stub):
return PowerManagerWakeLock(level_and_flags, tag) return PowerManagerWakeLock(level_and_flags, tag)
def isInteractive(self) -> bool: 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 return self._is_interactive
def isPowerSaveMode(self) -> bool: 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 return self._is_power_save
def isDeviceIdleMode(self) -> bool: def isDeviceIdleMode(self) -> bool:
@@ -1314,16 +1477,29 @@ class PowerManager(Stub):
return False return False
def reboot(self, reason: Optional[str] = None): 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): 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): 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): def userActivity(self, when: int, no_change_lights: bool = False):
pass """报告用户活动事件。桌面端无实际效果。"""
self._is_interactive = True
class PowerManagerWakeLock(Stub): class PowerManagerWakeLock(Stub):
@@ -1373,20 +1549,101 @@ class Vibrator(Stub):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._ff_device: Optional[str] = None
self._vibrating = False
def vibrate(self, milliseconds: int): @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, *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") sys.stderr.write(f"[Vibrator] vibrate: {milliseconds}ms\n")
elif len(args) == 2 and isinstance(args[0], list):
def vibrate(self, pattern: List[int], repeat: int): 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") sys.stderr.write(f"[Vibrator] vibrate pattern: {pattern}, repeat={repeat}\n")
def cancel(self): 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") sys.stderr.write("[Vibrator] cancel\n")
def hasVibrator(self) -> bool: def hasVibrator(self) -> bool:
return True """检查是否有振动器硬件。"""
return self._find_ff_device() is not None
def hasAmplitudeControl(self) -> bool: 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 return False
def areEffectsSupported(self, effect_ids: List[int]) -> List[int]: def areEffectsSupported(self, effect_ids: List[int]) -> List[int]:
@@ -1399,20 +1656,41 @@ class Vibrator(Stub):
class VibratorEffect(Stub): class VibratorEffect(Stub):
"""VibratorEffect — 振动效果""" """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): def __init__(self, effect_id: int = 0):
super().__init__() super().__init__()
self._effect_id = effect_id self._effect_id = effect_id
self._timings: Optional[List[int]] = None
self._amplitudes: Optional[List[int]] = None
self._repeat = -1
@staticmethod @staticmethod
def createOneShot(milliseconds: int, amplitude: int) -> "VibratorEffect": 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 @staticmethod
def createWaveform(pattern: List[int], repeat: int) -> "VibratorEffect": 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 @staticmethod
def createPredefined(effect_id: int) -> "VibratorEffect": def createPredefined(effect_id: int) -> "VibratorEffect":
"""创建预定义振动效果。"""
return VibratorEffect(effect_id) return VibratorEffect(effect_id)
@@ -1478,8 +1756,58 @@ class BatteryManager(Stub):
self._current_now = -500000 self._current_now = -500000
self._current_avg = -450000 self._current_avg = -450000
self._energy_counter = 15000000 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: def getIntProperty(self, prop_id: int) -> int:
if _USE_LINUX:
self._refresh_from_linux()
if prop_id == self.BATTERY_PROPERTY_CAPACITY: if prop_id == self.BATTERY_PROPERTY_CAPACITY:
return self._capacity return self._capacity
if prop_id == self.BATTERY_PROPERTY_STATUS: if prop_id == self.BATTERY_PROPERTY_STATUS:
@@ -1498,9 +1826,57 @@ class BatteryManager(Stub):
return self.getIntProperty(prop_id) return self.getIntProperty(prop_id)
def isCharging(self) -> bool: def isCharging(self) -> bool:
if _USE_LINUX:
self._refresh_from_linux()
return self._status == self.BATTERY_STATUS_CHARGING 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: 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 return 1800000 # 30 minutes
def toString(self) -> str: def toString(self) -> str:
@@ -1582,18 +1958,38 @@ class TokenWatcher(Stub):
def __init__(self, handler: Handler, tag: str): def __init__(self, handler: Handler, tag: str):
super().__init__() super().__init__()
self._handler = handler
self._tag = tag
self._tokens: Dict[int, str] = {}
def acquire(self, token: Any, tag: 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): def release(self, token: Any):
pass """释放令牌。"""
self._tokens.pop(id(token), None)
if not self._tokens:
self.released()
def isAcquired(self) -> bool: def isAcquired(self) -> bool:
return False """检查是否有令牌被持有。"""
return len(self._tokens) > 0
def acquired(self):
"""当第一个令牌被获取时调用。子类可重写。"""
pass
def released(self):
"""当所有令牌被释放时调用。子类可重写。"""
pass
def dump(self): 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): class WorkSource(Stub):
@@ -1744,41 +2140,156 @@ class FileObserver(Stub):
super().__init__() super().__init__()
self._path = path self._path = path
self._watching = False self._watching = False
self._thread = None
self._snapshot: Dict[str, float] = {}
def startWatching(self): def startWatching(self):
"""开始监听文件变化。使用轮询方式检测文件系统事件。"""
if self._watching:
return
self._watching = True 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): def stopWatching(self):
"""停止监听。"""
self._watching = False 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]): def onEvent(self, event: int, path: Optional[str]):
"""文件事件回调,子类可重写。"""
pass pass
def finalize(self): def finalize(self):
pass """清理资源。"""
self.stopWatching()
class FileUtils(Stub): class FileUtils(Stub):
"""FileUtils — 文件工具""" """FileUtils — 文件工具"""
@staticmethod @staticmethod
def readFileToString(file: Any) -> str: def readFileToString(path):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return "" return ""
@staticmethod @staticmethod
def stringToFile(file: Any, string: str): def stringToFile(path, content):
pass try:
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return True
except Exception:
return False
@staticmethod @staticmethod
def copyFile(src: Any, dest: Any) -> bool: def copyFile(src, dst):
try:
import shutil
shutil.copy2(src, dst)
return True return True
except Exception:
return False
@staticmethod @staticmethod
def deleteDirectory(dir_: Any) -> bool: def deleteDirectory(path):
try:
import shutil
shutil.rmtree(path)
return True 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 @staticmethod
def deleteOlderFiles(dir_: Any, min_count: int, min_age_ms: int): def deleteOlderFiles(dir_: Any, min_count: int, min_age_ms: int):
"""删除目录中较旧的文件,但保留至少 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 pass
+80 -5
View File
@@ -17,6 +17,15 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Callable
from .._core import Stub, AndroidOnlyError, _app_data_dir from .._core import Stub, AndroidOnlyError, _app_data_dir
from ..java.lang import RuntimeException 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,), {}) IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
@@ -421,8 +430,8 @@ class BatteryStatsManager(Stub):
# BatteryManager (扩展) # BatteryManager (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class BatteryManager(Stub): class BatteryManager(_BaseBatteryManager):
"""BatteryManager (扩展) — 电池管理器""" """BatteryManager (扩展) — 电池管理器 (继承 os.BatteryManager 的所有方法)"""
EXTRA_STATUS = "status" EXTRA_STATUS = "status"
EXTRA_HEALTH = "health" EXTRA_HEALTH = "health"
@@ -505,6 +514,54 @@ class BatteryManager(Stub):
self._discharge_time_remaining = -1 self._discharge_time_remaining = -1
self._is_capacity_estimated = False self._is_capacity_estimated = False
self._capacity_percentage = 100.0 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 @staticmethod
def getInstance(context: Any) -> "BatteryManager": def getInstance(context: Any) -> "BatteryManager":
@@ -513,6 +570,8 @@ class BatteryManager(Stub):
return BatteryManager._instance return BatteryManager._instance
def getIntProperty(self, id: int) -> int: def getIntProperty(self, id: int) -> int:
if _USE_LINUX:
self._refresh_from_linux()
properties = { properties = {
BATTERY_PROPERTY_CAPACITY: self._capacity, BATTERY_PROPERTY_CAPACITY: self._capacity,
BATTERY_PROPERTY_CHARGE_COUNTER: self._charge_counter, BATTERY_PROPERTY_CHARGE_COUNTER: self._charge_counter,
@@ -521,6 +580,9 @@ class BatteryManager(Stub):
BATTERY_PROPERTY_ENERGY_COUNTER: self._energy_counter, BATTERY_PROPERTY_ENERGY_COUNTER: self._energy_counter,
BATTERY_PROPERTY_CHARGE_TIME_REMAINING: self._charge_time_remaining, BATTERY_PROPERTY_CHARGE_TIME_REMAINING: self._charge_time_remaining,
BATTERY_PROPERTY_DISCHARGE_TIME_REMAINING: self._discharge_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) return properties.get(id, 0)
@@ -528,9 +590,13 @@ class BatteryManager(Stub):
return self.getIntProperty(id) return self.getIntProperty(id)
def isCharging(self) -> bool: def isCharging(self) -> bool:
if _USE_LINUX:
self._refresh_from_linux()
return self._status == BATTERY_STATUS_CHARGING or self._status == BATTERY_STATUS_FULL return self._status == BATTERY_STATUS_CHARGING or self._status == BATTERY_STATUS_FULL
def getBatteryLevel(self) -> int: def getBatteryLevel(self) -> int:
if _USE_LINUX:
self._refresh_from_linux()
return self._battery_level return self._battery_level
def getInt(intent: Any, key: str, defaultValue: int) -> int: def getInt(intent: Any, key: str, defaultValue: int) -> int:
@@ -617,8 +683,8 @@ class BatterySaverController(Stub):
# PowerManager (扩展) # PowerManager (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class PowerManager(Stub): class PowerManager(_BasePowerManager):
"""PowerManager — 电源管理器 (扩展)""" """PowerManager — 电源管理器 (扩展,继承 os.PowerManager 的所有方法)"""
PARTIAL_WAKE_LOCK = PARTIAL_WAKE_LOCK PARTIAL_WAKE_LOCK = PARTIAL_WAKE_LOCK
FULL_WAKE_LOCK = FULL_WAKE_LOCK FULL_WAKE_LOCK = FULL_WAKE_LOCK
@@ -687,9 +753,11 @@ class PowerManager(Stub):
self._shutdown_requested = False self._shutdown_requested = False
self._thermal_status = THERMAL_STATUS_NONE self._thermal_status = THERMAL_STATUS_NONE
self._power_save_state = PowerSaveState() 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: def isInteractive(self) -> bool:
if _USE_LINUX:
return self.isScreenOn()
return self._is_interactive return self._is_interactive
def isPowerSaveMode(self) -> bool: def isPowerSaveMode(self) -> bool:
@@ -705,6 +773,13 @@ class PowerManager(Stub):
return True return True
def isScreenOn(self) -> bool: 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 return self._is_screen_on
def isWakeLockLevelSupported(self, level: int) -> bool: def isWakeLockLevelSupported(self, level: int) -> bool:
+138
View File
@@ -108,6 +108,25 @@ class _SettingsBase(Stub):
data = _SettingsBase._load(name) data = _SettingsBase._load(name)
return data.get(key, default_value) 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): class SettingsSystem(_SettingsBase):
_NAME = "settings_system" _NAME = "settings_system"
@@ -144,6 +163,14 @@ class SettingsSystem(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int: def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsSystem._NAME, key, default_value) 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): class SettingsSecure(_SettingsBase):
_NAME = "settings_secure" _NAME = "settings_secure"
@@ -180,6 +207,14 @@ class SettingsSecure(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int: def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsSecure._NAME, key, default_value) 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): class SettingsGlobal(_SettingsBase):
_NAME = "settings_global" _NAME = "settings_global"
@@ -216,6 +251,14 @@ class SettingsGlobal(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int: def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsGlobal._NAME, key, default_value) 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): class Settings(Stub):
"""Settings — 系统设置""" """Settings — 系统设置"""
@@ -236,6 +279,10 @@ class Settings(Stub):
class MediaStore(Stub): class MediaStore(Stub):
"""MediaStore — 媒体存储""" """MediaStore — 媒体存储"""
VOLUME_INTERNAL = "internal"
VOLUME_EXTERNAL = "external"
VOLUME_EXTERNAL_PRIMARY = "external_primary"
class MediaColumns(Stub): class MediaColumns(Stub):
_ID = 0 _ID = 0
DATA = 1 DATA = 1
@@ -259,16 +306,35 @@ class MediaStore(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/images/media" EXTERNAL_CONTENT_URI = "content://media/external/images/media"
INTERNAL_CONTENT_URI = "content://media/internal/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): class Thumbnails(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/images/thumbnails" EXTERNAL_CONTENT_URI = "content://media/external/images/thumbnails"
class Video(Stub): class Video(Stub):
class Media(Stub): class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/video/media" 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 Audio(Stub):
class Media(Stub): class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/audio/media" 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 @staticmethod
def getVersion(context: Any, uri: str) -> str: def getVersion(context: Any, uri: str) -> str:
@@ -337,22 +403,94 @@ class DocumentsContract(Stub):
class ContactsContract(Stub): class ContactsContract(Stub):
"""ContactsContract — 联系人合约""" """ContactsContract — 联系人合约"""
AUTHORITY = "com.android.contacts"
class Contacts(Stub): class Contacts(Stub):
CONTENT_URI = "content://com.android.contacts/contacts" CONTENT_URI = "content://com.android.contacts/contacts"
CONTENT_FILTER_URI = "content://com.android.contacts/contacts/filter" 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" DISPLAY_NAME = "display_name"
PHOTO_URI = "photo_uri" PHOTO_URI = "photo_uri"
PHOTO_ID = "photo_id"
_ID = "_id" _ID = "_id"
HAS_PHONE_NUMBER = "has_phone_number"
LOOKUP_KEY = "lookup"
STARRED = "starred"
class CommonDataKinds(Stub): class CommonDataKinds(Stub):
class Email(Stub): class Email(Stub):
CONTENT_URI = "content://com.android.contacts/data/emails" 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): class Phone(Stub):
CONTENT_URI = "content://com.android.contacts/data/phones" 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): class Data(Stub):
CONTENT_URI = "content://com.android.contacts/data" 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): class ContactsContractContacts(ContactsContract.Contacts):
+37 -37
View File
@@ -80,7 +80,7 @@ class OsConstants(Stub):
SIGTTIN = 21 SIGTTIN = 21
SIGTTOU = 22 SIGTTOU = 22
# errno 常量 # errno 常量 (使用 getattr 处理 Windows 上不存在的常量)
EPERM = errno.EPERM EPERM = errno.EPERM
ENOENT = errno.ENOENT ENOENT = errno.ENOENT
ESRCH = errno.ESRCH ESRCH = errno.ESRCH
@@ -95,7 +95,7 @@ class OsConstants(Stub):
ENOMEM = errno.ENOMEM ENOMEM = errno.ENOMEM
EACCES = errno.EACCES EACCES = errno.EACCES
EFAULT = errno.EFAULT EFAULT = errno.EFAULT
ENOTBLK = errno.ENOTBLK ENOTBLK = getattr(errno, 'ENOTBLK', 15) # Windows 不支持
EBUSY = errno.EBUSY EBUSY = errno.EBUSY
EEXIST = errno.EEXIST EEXIST = errno.EEXIST
EXDEV = errno.EXDEV EXDEV = errno.EXDEV
@@ -106,7 +106,7 @@ class OsConstants(Stub):
ENFILE = errno.ENFILE ENFILE = errno.ENFILE
EMFILE = errno.EMFILE EMFILE = errno.EMFILE
ENOTTY = errno.ENOTTY ENOTTY = errno.ENOTTY
ETXTBSY = errno.ETXTBSY ETXTBSY = getattr(errno, 'ETXTBSY', 139)
EFBIG = errno.EFBIG EFBIG = errno.EFBIG
ENOSPC = errno.ENOSPC ENOSPC = errno.ENOSPC
ESPIPE = errno.ESPIPE ESPIPE = errno.ESPIPE
@@ -123,16 +123,16 @@ class OsConstants(Stub):
ELOOP = errno.ELOOP ELOOP = errno.ELOOP
ENOMSG = errno.ENOMSG ENOMSG = errno.ENOMSG
EIDRM = errno.EIDRM EIDRM = errno.EIDRM
ENOSTR = errno.ENOSTR ENOSTR = getattr(errno, 'ENOSTR', 125)
ENODATA = errno.ENODATA ENODATA = getattr(errno, 'ENODATA', 120)
ETIME = errno.ETIME ETIME = getattr(errno, 'ETIME', 137)
ENOSR = errno.ENOSR ENOSR = getattr(errno, 'ENOSR', 124)
EREMOTE = errno.EREMOTE EREMOTE = getattr(errno, 'EREMOTE', 10071)
ENOLINK = errno.ENOLINK ENOLINK = getattr(errno, 'ENOLINK', 121)
EPROTO = errno.EPROTO EPROTO = errno.EPROTO
EMULTIHOP = errno.EMULTIHOP EMULTIHOP = getattr(errno, 'EMULTIHOP', 10072) # Windows 不支持
EBADMSG = errno.EBADMSG EBADMSG = getattr(errno, 'EBADMSG', 104)
EOVERFLOW = errno.EOVERFLOW EOVERFLOW = getattr(errno, 'EOVERFLOW', 132)
EILSEQ = errno.EILSEQ EILSEQ = errno.EILSEQ
ENOTSOCK = errno.ENOTSOCK ENOTSOCK = errno.ENOTSOCK
EDESTADDRREQ = errno.EDESTADDRREQ EDESTADDRREQ = errno.EDESTADDRREQ
@@ -140,10 +140,10 @@ class OsConstants(Stub):
EPROTOTYPE = errno.EPROTOTYPE EPROTOTYPE = errno.EPROTOTYPE
ENOPROTOOPT = errno.ENOPROTOOPT ENOPROTOOPT = errno.ENOPROTOOPT
EPROTONOSUPPORT = errno.EPROTONOSUPPORT EPROTONOSUPPORT = errno.EPROTONOSUPPORT
ESOCKTNOSUPPORT = errno.ESOCKTNOSUPPORT ESOCKTNOSUPPORT = getattr(errno, 'ESOCKTNOSUPPORT', 10044)
EOPNOTSUPP = errno.EOPNOTSUPP EOPNOTSUPP = getattr(errno, 'EOPNOTSUPP', 10045)
EPFNOSUPPORT = errno.EPFNOSUPPORT EPFNOSUPPORT = getattr(errno, 'EPFNOSUPPORT', 10046)
EAFNOSUPPORT = errno.EAFNOSUPPORT EAFNOSUPPORT = getattr(errno, 'EAFNOSUPPORT', 10047)
EADDRINUSE = errno.EADDRINUSE EADDRINUSE = errno.EADDRINUSE
EADDRNOTAVAIL = errno.EADDRNOTAVAIL EADDRNOTAVAIL = errno.EADDRNOTAVAIL
ENETDOWN = errno.ENETDOWN ENETDOWN = errno.ENETDOWN
@@ -154,32 +154,32 @@ class OsConstants(Stub):
ENOBUFS = errno.ENOBUFS ENOBUFS = errno.ENOBUFS
EISCONN = errno.EISCONN EISCONN = errno.EISCONN
ENOTCONN = errno.ENOTCONN ENOTCONN = errno.ENOTCONN
ESHUTDOWN = errno.ESHUTDOWN ESHUTDOWN = getattr(errno, 'ESHUTDOWN', 10058)
ETOOMANYREFS = errno.ETOOMANYREFS ETOOMANYREFS = getattr(errno, 'ETOOMANYREFS', 10059)
ETIMEDOUT = errno.ETIMEDOUT ETIMEDOUT = errno.ETIMEDOUT
ECONNREFUSED = errno.ECONNREFUSED ECONNREFUSED = errno.ECONNREFUSED
EHOSTDOWN = errno.EHOSTDOWN EHOSTDOWN = getattr(errno, 'EHOSTDOWN', 10064)
EHOSTUNREACH = errno.EHOSTUNREACH EHOSTUNREACH = errno.EHOSTUNREACH
EALREADY = errno.EALREADY EALREADY = errno.EALREADY
EINPROGRESS = errno.EINPROGRESS EINPROGRESS = errno.EINPROGRESS
ESTALE = errno.ESTALE ESTALE = getattr(errno, 'ESTALE', 10070)
EUCLEAN = errno.EUCLEAN EUCLEAN = getattr(errno, 'EUCLEAN', 117) # Windows 不支持
ENOTNAM = errno.ENOTNAM ENOTNAM = getattr(errno, 'ENOTNAM', 118) # Windows 不支持
ENAVAIL = errno.ENAVAIL ENAVAIL = getattr(errno, 'ENAVAIL', 119) # Windows 不支持
EISNAM = errno.EISNAM EISNAM = getattr(errno, 'EISNAM', 120) # Windows 不支持
EREMOTEIO = errno.EREMOTEIO EREMOTEIO = getattr(errno, 'EREMOTEIO', 121) # Windows 不支持
EDQUOT = errno.EDQUOT EDQUOT = getattr(errno, 'EDQUOT', 10069)
ENOMEDIUM = errno.ENOMEDIUM ENOMEDIUM = getattr(errno, 'ENOMEDIUM', 123) # Windows 不支持
EMEDIUMTYPE = errno.EMEDIUMTYPE EMEDIUMTYPE = getattr(errno, 'EMEDIUMTYPE', 124) # Windows 不支持
ECANCELED = errno.ECANCELED ECANCELED = getattr(errno, 'ECANCELED', 105)
ENOKEY = errno.ENOKEY ENOKEY = getattr(errno, 'ENOKEY', 126) # Windows 不支持
EKEYEXPIRED = errno.EKEYEXPIRED EKEYEXPIRED = getattr(errno, 'EKEYEXPIRED', 127) # Windows 不支持
EKEYREVOKED = errno.EKEYREVOKED EKEYREVOKED = getattr(errno, 'EKEYREVOKED', 128) # Windows 不支持
EKEYREJECTED = errno.EKEYREJECTED EKEYREJECTED = getattr(errno, 'EKEYREJECTED', 129) # Windows 不支持
EOWNERDEAD = errno.EOWNERDEAD EOWNERDEAD = getattr(errno, 'EOWNERDEAD', 133)
ENOTRECOVERABLE = errno.ENOTRECOVERABLE ENOTRECOVERABLE = getattr(errno, 'ENOTRECOVERABLE', 127)
ERFKILL = errno.ERFKILL ERFKILL = getattr(errno, 'ERFKILL', 132) # Windows 不支持
EHWPOISON = getattr(errno, 'EHWPOISON', 1001) EHWPOISON = getattr(errno, 'EHWPOISON', 1001) # Windows 不支持
def __init__(self): def __init__(self):
super().__init__() super().__init__()
+35 -2
View File
@@ -14,6 +14,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name, _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 基类集合 # CellIdentity 基类集合
@@ -553,6 +559,9 @@ class TelephonyManager(Stub):
self._context = context self._context = context
def getNetworkOperatorName(self) -> str: def getNetworkOperatorName(self) -> str:
if _USE_LINUX:
loc = LinuxLocation.get_location()
return loc.get("isp", "") or ""
return "MockOperator" return "MockOperator"
def getNetworkOperator(self) -> str: def getNetworkOperator(self) -> str:
@@ -562,9 +571,15 @@ class TelephonyManager(Stub):
return "us" return "us"
def getNetworkType(self) -> int: 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 return self.NETWORK_TYPE_LTE
def getPhoneType(self) -> int: def getPhoneType(self) -> int:
if _USE_LINUX:
return self.PHONE_TYPE_NONE
return self.PHONE_TYPE_GSM return self.PHONE_TYPE_GSM
def getDataNetworkType(self) -> int: def getDataNetworkType(self) -> int:
@@ -574,6 +589,8 @@ class TelephonyManager(Stub):
return self.NETWORK_TYPE_LTE return self.NETWORK_TYPE_LTE
def getSimOperator(self) -> str: def getSimOperator(self) -> str:
if _USE_LINUX:
return ""
return "310260" return "310260"
def getSimOperatorName(self) -> str: def getSimOperatorName(self) -> str:
@@ -583,18 +600,29 @@ class TelephonyManager(Stub):
return "us" return "us"
def getSimSerialNumber(self) -> Optional[str]: def getSimSerialNumber(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "89014103211118510720" return "89014103211118510720"
def getSubscriberId(self) -> Optional[str]: def getSubscriberId(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "310260123456789" return "310260123456789"
def getDeviceId(self) -> Optional[str]: def getDeviceId(self) -> Optional[str]:
if _USE_LINUX:
return LinuxNetwork.get_mac_address() or None
return "000000000000000" return "000000000000000"
def getDeviceSoftwareVersion(self) -> Optional[str]: def getDeviceSoftwareVersion(self) -> Optional[str]:
if _USE_LINUX:
info = LinuxProcess.get_system_info()
return info.get("kernel", "")
return "00.00.00.00" return "00.00.00.00"
def getLine1Number(self) -> Optional[str]: def getLine1Number(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "15551234567" return "15551234567"
def getGroupIdLevel1(self) -> Optional[str]: def getGroupIdLevel1(self) -> Optional[str]:
@@ -619,6 +647,9 @@ class TelephonyManager(Stub):
return self.SIM_STATE_READY return self.SIM_STATE_READY
def getCellLocation(self) -> Any: def getCellLocation(self) -> Any:
# 桌面无基站,Linux 下返回 None
if _USE_LINUX:
return None
return None return None
def getAllCellInfo(self) -> Optional[List[CellInfo]]: def getAllCellInfo(self) -> Optional[List[CellInfo]]:
@@ -630,8 +661,10 @@ class TelephonyManager(Stub):
def getMmsUAProfUrl(self) -> str: def getMmsUAProfUrl(self) -> str:
return "http://www.example.com/uaprof.xml" return "http://www.example.com/uaprof.xml"
def listen(self, listener: PhoneStateListener, events: int): def listen(self, listener, events):
pass if not hasattr(self, '_listeners'):
self._listeners = []
self._listeners.append((listener, events))
def getDataState(self) -> int: def getDataState(self) -> int:
return self.DATA_CONNECTED return self.DATA_CONNECTED
+79 -18
View File
@@ -7,6 +7,7 @@ import html as _html
import re as _re import re as _re
import sys import sys
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union 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 ( from .._core import (
Stub, AndroidOnlyError, android_context, Stub, AndroidOnlyError, android_context,
@@ -176,7 +177,9 @@ class TextUtils(Stub):
@staticmethod @staticmethod
def split(text: str, delimiter: str) -> List[str]: 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 @staticmethod
def htmlEncode(s: str) -> str: def htmlEncode(s: str) -> str:
@@ -186,6 +189,48 @@ class TextUtils(Stub):
def htmlDecode(s: str) -> str: def htmlDecode(s: str) -> str:
return _html.unescape(s) 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 @staticmethod
def getReverse(s: str, start: int, end: int) -> str: def getReverse(s: str, start: int, end: int) -> str:
return s[start:end][::-1] return s[start:end][::-1]
@@ -227,8 +272,13 @@ class TextUtils(Stub):
return text[:max_chars] + ellipsis return text[:max_chars] + ellipsis
@staticmethod @staticmethod
def isDigitsOnly(s: str) -> bool: def isDigitsOnly(s: Any) -> bool:
return s.isdigit() if s else False if s is None:
return False
s = str(s)
if len(s) == 0:
return False
return s.isdigit()
@staticmethod @staticmethod
def isGraphical(s: str) -> bool: def isGraphical(s: str) -> bool:
@@ -293,12 +343,12 @@ class Spanned(Stub):
def __init__(self, source: str = ""): def __init__(self, source: str = ""):
super().__init__() super().__init__()
self._source = source 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): def setSpan(self, span_obj: Any, start: int, end: int, flags: int):
span_id = id(span_obj) span_id = id(span_obj)
span_type = type(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): def removeSpan(self, span_obj: Any):
span_id = id(span_obj) span_id = id(span_obj)
@@ -306,12 +356,13 @@ class Spanned(Stub):
self._spans.pop((span_type, span_id), None) self._spans.pop((span_type, span_id), None)
def getSpans(self, start: int, end: int, span_type: Optional[type] = None) -> List[Any]: def getSpans(self, start: int, end: int, span_type: Optional[type] = None) -> List[Any]:
result = [] collected = []
for (st, sid), (s, e, obj) in self._spans.items(): for (st, sid), (s, e, flags, obj) in self._spans.items():
if span_type is None or st == span_type: if span_type is None or st == span_type:
if s < end and e > start: if s < end and e > start:
result.append(obj) collected.append((s, obj))
return result collected.sort(key=lambda t: t[0])
return [obj for _, obj in collected]
def getSpanStart(self, span_obj: Any) -> int: def getSpanStart(self, span_obj: Any) -> int:
span_id = id(span_obj) span_id = id(span_obj)
@@ -329,10 +380,18 @@ class Spanned(Stub):
span_id = id(span_obj) span_id = id(span_obj)
span_type = type(span_obj) span_type = type(span_obj)
data = self._spans.get((span_type, span_id)) 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: 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: def toString(self) -> str:
return self._source return self._source
@@ -435,15 +494,17 @@ class SpannableStringBuilder(Editable):
else: else:
super().__init__(str(source)) super().__init__(str(source))
def append(self, text: Any) -> "SpannableStringBuilder": def append(self, text: Any, start: Optional[int] = None,
super().append(text) end: Optional[int] = None) -> "SpannableStringBuilder":
if text is None:
return self return self
if start is None and end is None:
def append(self, text: Any, start: int, end: int) -> "SpannableStringBuilder": self._source += str(text)
if isinstance(text, str):
self._source += text[start:end]
else: 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 return self
def insert(self, where: int, text: Any) -> "SpannableStringBuilder": def insert(self, where: int, text: Any) -> "SpannableStringBuilder":
+74 -18
View File
@@ -5,6 +5,7 @@ Android Util 组件 — Log, SparseArray, LruCache, DisplayMetrics 等
import math import math
import re as _re import re as _re
import struct
import sys import sys
from datetime import datetime from datetime import datetime
from typing import Any, Callable, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union from typing import Any, Callable, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union
@@ -51,19 +52,44 @@ class Log(Stub):
return 0 return 0
@staticmethod @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: 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") sys.stderr.write(f"W/{tag}: {msg}\n")
return 0 return 0
@staticmethod @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: 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") sys.stderr.write(f"E/{tag}: {msg}\n")
return 0 return 0
@staticmethod @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") sys.stderr.write(f"WTF/{tag}: {msg}\n")
return 0 return 0
@@ -88,17 +114,16 @@ class Log(Stub):
@staticmethod @staticmethod
def getStackTraceString(tr: Exception) -> str: def getStackTraceString(tr: Exception) -> str:
import traceback import traceback
if tr is None:
return ""
if isinstance(tr, str):
return tr
return "".join(traceback.format_exception(type(tr), tr, tr.__traceback__)) return "".join(traceback.format_exception(type(tr), tr, tr.__traceback__))
@staticmethod @staticmethod
def setMinLogLevel(level: int): def setMinLogLevel(level: int):
Log._min_log_level = level 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 # SparseArray
@@ -518,6 +543,23 @@ class TypedValue(Stub):
COMPLEX_UNIT_FRACTION = 6 COMPLEX_UNIT_FRACTION = 6
COMPLEX_UNIT_FRACTION_PARENT = 7 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_NULL = 0x00
TYPE_REFERENCE = 0x01 TYPE_REFERENCE = 0x01
TYPE_ATTRIBUTE = 0x02 TYPE_ATTRIBUTE = 0x02
@@ -536,6 +578,9 @@ class TypedValue(Stub):
TYPE_LAST_COLOR_INT = 0x1F TYPE_LAST_COLOR_INT = 0x1F
TYPE_LAST_INT = 0x1F TYPE_LAST_INT = 0x1F
DATA_NULL_UNDEFINED = 0
DATA_NULL_EMPTY = 1
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.type = self.TYPE_NULL self.type = self.TYPE_NULL
@@ -564,18 +609,27 @@ class TypedValue(Stub):
@staticmethod @staticmethod
def complexToDimension(data: int, metrics: DisplayMetrics) -> float: def complexToDimension(data: int, metrics: DisplayMetrics) -> float:
unit = data & 0xF """将复数维度值转换为像素值。
value = (data >> 4) & 0xFFFFFF 复数值格式:bits 0-3=unit, bits 4-5=radix, bits 8-31=mantissa(有符号24位)"""
# Sign-extend unit = (data >> TypedValue.COMPLEX_UNIT_SHIFT) & TypedValue.COMPLEX_UNIT_MASK
if value & 0x800000: radix = (data >> TypedValue.COMPLEX_RADIX_SHIFT) & TypedValue.COMPLEX_RADIX_MASK
value = value - 0x1000000 mantissa = (data >> TypedValue.COMPLEX_MANTISSA_SHIFT) & TypedValue.COMPLEX_MANTISSA_MASK
return TypedValue.applyDimension(unit, float(value), metrics) # 24 位有符号数符号扩展
if mantissa & 0x800000:
mantissa = mantissa - 0x1000000
value = mantissa * TypedValue._RADIX_MULTS[radix]
return TypedValue.applyDimension(unit, value, metrics)
@staticmethod @staticmethod
def complexToFloat(data: int) -> float: def complexToFloat(data: int) -> float:
value = (data & 0xFFFFFF00) >> 8 """将复数值转换为浮点数(与 AOSP complexToFloat 一致)。"""
exp = (data & 0xFF) - 150 radix = (data >> TypedValue.COMPLEX_RADIX_SHIFT) & TypedValue.COMPLEX_RADIX_MASK
return float(value) * (10.0 ** exp) # 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 @staticmethod
def complexToDimensionPixelOffset(data: int, metrics: DisplayMetrics) -> int: def complexToDimensionPixelOffset(data: int, metrics: DisplayMetrics) -> int:
@@ -589,7 +643,9 @@ class TypedValue(Stub):
return int(val - 0.5) return int(val - 0.5)
def getFloat(self) -> float: 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: def toString(self) -> str:
return f"TypedValue(type={self.type}, data={self.data})" return f"TypedValue(type={self.type}, data={self.data})"
+250 -11
View File
@@ -10,6 +10,12 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from .._core import Stub, AndroidOnlyError, android_context, HAS_PYAUTOGUI 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 # Gravity
@@ -363,6 +369,12 @@ class View(Stub):
def performClick(self) -> bool: def performClick(self) -> bool:
return self.callOnClick() return self.callOnClick()
def screenshot(self, path=None):
"""截屏"""
if _USE_LINUX:
return LinuxDisplay.screenshot(path)
return None
def performLongClick(self) -> bool: def performLongClick(self) -> bool:
if self._on_long_click_listener: if self._on_long_click_listener:
self._on_long_click_listener(self) self._on_long_click_listener(self)
@@ -926,6 +938,106 @@ class View(Stub):
def onApplyWindowInsets(self, insets): def onApplyWindowInsets(self, insets):
return 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: class MeasureSpec:
"""MeasureSpec — 测量规格包装""" """MeasureSpec — 测量规格包装"""
@@ -1130,8 +1242,13 @@ class ViewGroup(View, ViewParent, ViewManager):
# ─── child management ────────────────────────────────────── # ─── child management ──────────────────────────────────────
def addView(self, child: View, index: int = -1, def addView(self, child: View, index: Optional[int] = -1,
params: Optional["ViewGroup.LayoutParams"] = None): 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: if params is None:
params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT) ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -1143,9 +1260,6 @@ class ViewGroup(View, ViewParent, ViewManager):
child.setLayoutParams(params) child.setLayoutParams(params)
child.onAttachedToWindow() child.onAttachedToWindow()
def addView(self, child: View, params: Optional["ViewGroup.LayoutParams"] = None):
self.addView(child, -1, params)
def removeView(self, view: View): def removeView(self, view: View):
if view in self._children: if view in self._children:
self._children.remove(view) self._children.remove(view)
@@ -1248,6 +1362,32 @@ class ViewGroup(View, ViewParent, ViewManager):
def isMotionEventSplittingEnabled(self) -> bool: def isMotionEventSplittingEnabled(self) -> bool:
return True 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): def setTransitionGroup(self, is_transition_group: bool):
pass pass
@@ -1697,6 +1837,75 @@ class MotionEvent(Stub):
def getHistoricalSize(self, pos: int, pointer: int = 0) -> float: def getHistoricalSize(self, pos: int, pointer: int = 0) -> float:
return 0.5 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 # KeyEvent
@@ -1981,6 +2190,28 @@ class KeyEvent(Stub):
meta_state=event._meta_state, 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 # GestureDetector
@@ -2688,14 +2919,18 @@ class SurfaceHolder(Stub):
def setFormat(self, format: int): def setFormat(self, format: int):
self._format = format self._format = format
def lockCanvas(self) -> Any: def lockCanvas(self, *args):
return None """锁定画布并返回 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: def unlockCanvasAndPost(self, canvas):
return None """解锁画布并提交"""
pass # 桌面环境无需实际提交
def unlockCanvasAndPost(self, canvas: Any):
pass
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -3260,9 +3495,13 @@ class Display(Stub):
return self._display_id return self._display_id
def getWidth(self) -> int: def getWidth(self) -> int:
if _USE_LINUX:
return LinuxDisplay.get_screen_size()[0]
return self._width return self._width
def getHeight(self) -> int: def getHeight(self) -> int:
if _USE_LINUX:
return LinuxDisplay.get_screen_size()[1]
return self._height return self._height
def getRotation(self) -> int: def getRotation(self) -> int:
+61 -38
View File
@@ -13,14 +13,33 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError from .._core import Stub, AndroidOnlyError
from ..java.lang import RuntimeException 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 # ViewOutlineProvider
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ViewOutlineProvider(Stub): class ViewOutlineProvider(_BaseViewOutlineProvider):
"""ViewOutlineProvider — 视图轮廓提供者""" """ViewOutlineProvider — 视图轮廓提供者 (继承 view.ViewOutlineProvider 的所有方法)"""
BACKGROUND = None BACKGROUND = None
BOUNDS = None BOUNDS = None
@@ -99,8 +118,8 @@ class Outline(Stub):
# ViewPropertyAnimator # ViewPropertyAnimator
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ViewPropertyAnimator(Stub): class ViewPropertyAnimator(_BaseViewPropertyAnimator):
"""ViewPropertyAnimator — 视图属性动画器""" """ViewPropertyAnimator — 视图属性动画器 (继承 view.ViewPropertyAnimator 的所有方法)"""
def __init__(self, view: Any = None): def __init__(self, view: Any = None):
super().__init__() super().__init__()
@@ -381,8 +400,8 @@ class ViewStructure(Stub):
# ViewTreeObserver # ViewTreeObserver
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ViewTreeObserver(Stub): class ViewTreeObserver(_BaseViewTreeObserver):
"""ViewTreeObserver — 视图树观察者""" """ViewTreeObserver — 视图树观察者 (继承 view.ViewTreeObserver 的所有方法)"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -529,8 +548,8 @@ class ViewDebug(Stub):
# ViewConfiguration # ViewConfiguration
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ViewConfiguration(Stub): class ViewConfiguration(_BaseViewConfiguration):
"""ViewConfiguration — 视图配置""" """ViewConfiguration — 视图配置 (继承 view.ViewConfiguration 的所有方法)"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -615,8 +634,8 @@ class ViewConfiguration(Stub):
# ViewParent (扩展) # ViewParent (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ViewParent(Stub): class ViewParent(_BaseViewParent):
"""ViewParent — 视图父容器接口扩展""" """ViewParent — 视图父容器接口扩展 (继承 view.ViewParent 的所有方法)"""
def requestLayout(self) -> None: def requestLayout(self) -> None:
pass pass
@@ -699,8 +718,8 @@ class ViewParent(Stub):
# ViewGroup (扩展) # ViewGroup (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ViewGroup(Stub): class ViewGroup(_BaseViewGroup):
"""ViewGroup — 视图组扩展""" """ViewGroup — 视图组扩展 (继承 view.ViewGroup 的所有方法)"""
CLIP_TO_PADDING_MASK = 0 CLIP_TO_PADDING_MASK = 0
FOCUS_AFTER = 1 FOCUS_AFTER = 1
@@ -781,14 +800,18 @@ class ViewGroup(Stub):
f"margins=({self.leftMargin},{self.topMargin}," f"margins=({self.leftMargin},{self.topMargin},"
f"{self.rightMargin},{self.bottomMargin}))") f"{self.rightMargin},{self.bottomMargin}))")
def __init__(self): def __init__(self, context=None):
super().__init__() super().__init__(context)
if not hasattr(self, '_children'):
self._children: List[Any] = [] self._children: List[Any] = []
if not hasattr(self, '_clip_children'):
self._clip_children = True self._clip_children = True
if not hasattr(self, '_clip_to_padding'):
self._clip_to_padding = True self._clip_to_padding = True
self._layout_mode = self.LAYOUT_MODE_CLIP_BOUNDS self._layout_mode = self.LAYOUT_MODE_CLIP_BOUNDS
self._persistent_caching = self.PERSISTENT_NO_CACHE self._persistent_caching = self.PERSISTENT_NO_CACHE
self._touch_mode = False self._touch_mode = False
if not hasattr(self, '_motion_event_splitting'):
self._motion_event_splitting = True self._motion_event_splitting = True
self._transient_child: Optional[Any] = None self._transient_child: Optional[Any] = None
self._add_stateset_focus = False self._add_stateset_focus = False
@@ -947,8 +970,8 @@ class ViewOverlay(Stub):
# WindowInsets / DisplayCutout / Display # WindowInsets / DisplayCutout / Display
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class WindowInsets(Stub): class WindowInsets(_BaseWindowInsets):
"""WindowInsets — 窗口插入区域""" """WindowInsets — 窗口插入区域 (继承 view.WindowInsets 的所有方法)"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -1080,8 +1103,8 @@ class DisplayCutout(Stub):
return f"DisplayCutout(safe={self._safe_insets})" return f"DisplayCutout(safe={self._safe_insets})"
class Display(Stub): class Display(_BaseDisplay):
"""Display — 屏幕显示信息""" """Display — 屏幕显示信息 (继承 view.Display 的所有方法)"""
DEFAULT_DISPLAY = 0 DEFAULT_DISPLAY = 0
FLAG_PRESENTATION = 1 FLAG_PRESENTATION = 1
@@ -1423,8 +1446,8 @@ WINDOW_INSETS_ANIMATION_TYPE_TRANSLATION = 2
WINDOW_INSETS_ANIMATION_TYPE_ALPHA_AND_TRANSLATION = 3 WINDOW_INSETS_ANIMATION_TYPE_ALPHA_AND_TRANSLATION = 3
class DragEvent(Stub): class DragEvent(_BaseDragEvent):
"""DragEvent — 拖放事件""" """DragEvent — 拖放事件 (继承 view.DragEvent 的所有方法)"""
ACTION_DRAG_STARTED = DRAG_EVENT_ACTION_DRAG_STARTED ACTION_DRAG_STARTED = DRAG_EVENT_ACTION_DRAG_STARTED
ACTION_DRAG_ENTERED = DRAG_EVENT_ACTION_DRAG_ENTERED ACTION_DRAG_ENTERED = DRAG_EVENT_ACTION_DRAG_ENTERED
@@ -1467,8 +1490,8 @@ class DragEvent(Stub):
return f"DragEvent(action={actions.get(self._action, 'UNKNOWN')})" return f"DragEvent(action={actions.get(self._action, 'UNKNOWN')})"
class GestureDetector(Stub): class GestureDetector(_BaseGestureDetector):
"""GestureDetector — 手势检测器""" """GestureDetector — 手势检测器 (继承 view.GestureDetector 的所有方法)"""
class SimpleOnGestureListener(Stub): class SimpleOnGestureListener(Stub):
"""GestureDetector.SimpleOnGestureListener — 简单手势监听器""" """GestureDetector.SimpleOnGestureListener — 简单手势监听器"""
@@ -1559,8 +1582,8 @@ class GestureDetector(Stub):
return f"GestureDetector(listener={'yes' if self._listener else 'no'})" return f"GestureDetector(listener={'yes' if self._listener else 'no'})"
class KeyEvent(Stub): class KeyEvent(_BaseKeyEvent):
"""KeyEvent — 按键事件""" """KeyEvent — 按键事件 (扩展版,继承 view.KeyEvent 的所有常量)"""
ACTION_DOWN = 0 ACTION_DOWN = 0
ACTION_UP = 1 ACTION_UP = 1
@@ -2083,8 +2106,8 @@ class ContextMenu(Menu):
# ScaleGestureDetector / SoundEffectConstants / Surface / etc. # ScaleGestureDetector / SoundEffectConstants / Surface / etc.
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ScaleGestureDetector(Stub): class ScaleGestureDetector(_BaseScaleGestureDetector):
"""ScaleGestureDetector — 缩放手势检测器""" """ScaleGestureDetector — 缩放手势检测器 (继承 view.ScaleGestureDetector 的所有方法)"""
class SimpleOnScaleGestureListener(Stub): class SimpleOnScaleGestureListener(Stub):
def onScale(self, detector: "ScaleGestureDetector") -> bool: def onScale(self, detector: "ScaleGestureDetector") -> bool:
@@ -2164,8 +2187,8 @@ class ScaleGestureDetector(Stub):
f"inProgress={self._in_progress})") f"inProgress={self._in_progress})")
class SoundEffectConstants(Stub): class SoundEffectConstants(_BaseSoundEffectConstants):
"""SoundEffectConstants — 音效常量""" """SoundEffectConstants — 音效常量 (继承 view.SoundEffectConstants 的所有方法)"""
CLICK = 0 CLICK = 0
NAVIGATION_UP = 1 NAVIGATION_UP = 1
@@ -2182,8 +2205,8 @@ class SoundEffectConstants(Stub):
return SoundEffectConstants.NAVIGATION_UP return SoundEffectConstants.NAVIGATION_UP
class Surface(Stub): class Surface(_BaseSurface):
"""Surface — 表面""" """Surface — 表面 (继承 view.Surface 的所有方法)"""
ROTATION_0 = 0 ROTATION_0 = 0
ROTATION_90 = 1 ROTATION_90 = 1
@@ -2256,8 +2279,8 @@ class SurfaceControl(Stub):
return f"SurfaceControl(layer={self._layer}, visible={self._visible})" return f"SurfaceControl(layer={self._layer}, visible={self._visible})"
class SurfaceView(Stub): class SurfaceView(_BaseSurfaceView):
"""SurfaceView — 表面视图扩展""" """SurfaceView — 表面视图扩展 (继承 view.SurfaceView 的所有方法)"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
super().__init__() super().__init__()
@@ -2284,8 +2307,8 @@ class SurfaceView(Stub):
return "SurfaceView()" return "SurfaceView()"
class TextureView(Stub): class TextureView(_BaseTextureView):
"""TextureView — 纹理视图""" """TextureView — 纹理视图扩展 (继承 view.TextureView 的所有方法)"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
super().__init__() super().__init__()
@@ -2328,8 +2351,8 @@ class TouchDelegate(Stub):
return f"TouchDelegate(delegate={self._delegate_view})" return f"TouchDelegate(delegate={self._delegate_view})"
class VelocityTracker(Stub): class VelocityTracker(_BaseVelocityTracker):
"""VelocityTracker — 速度追踪器""" """VelocityTracker — 速度追踪器 (继承 view.VelocityTracker 的所有方法)"""
def __init__(self, strategy: str = ""): def __init__(self, strategy: str = ""):
super().__init__() super().__init__()
@@ -2445,8 +2468,8 @@ class WindowInsetsAnimation(Stub):
f"fraction={self._interpolated_fraction})") f"fraction={self._interpolated_fraction})")
class WindowInsetsController(Stub): class WindowInsetsController(_BaseWindowInsetsController):
"""WindowInsetsController — 窗口插入控制器""" """WindowInsetsController — 窗口插入控制器 (继承 view.WindowInsetsController 的所有方法)"""
APPEARANCE_OPAQUE_STATUS_BARS = 1 APPEARANCE_OPAQUE_STATUS_BARS = 1
APPEARANCE_OPAQUE_NAVIGATION_BARS = 2 APPEARANCE_OPAQUE_NAVIGATION_BARS = 2
+142 -28
View File
@@ -14,6 +14,12 @@ from .view import (
ViewGroup as VG, ViewGroup as VG,
) )
try:
from .._linux_impl import IS_LINUX, LinuxNotify, LinuxClipboard
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# TextView # TextView
@@ -359,6 +365,39 @@ class TextView(View):
def setPadding(self, left: int, top: int, right: int, bottom: int): def setPadding(self, left: int, top: int, right: int, bottom: int):
super().setPadding(left, top, right, bottom) 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): def onMeasure(self, width_measure_spec: int, height_measure_spec: int):
from .view import MeasureSpec from .view import MeasureSpec
w = MeasureSpec.getSize(width_measure_spec) w = MeasureSpec.getSize(width_measure_spec)
@@ -450,6 +489,12 @@ class CheckBox(Button):
def toggle(self): def toggle(self):
self._checked = not self._checked 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: def performClick(self) -> bool:
self.toggle() self.toggle()
return super().performClick() return super().performClick()
@@ -601,6 +646,22 @@ class Switch(Button):
def getTextOff(self) -> str: def getTextOff(self) -> str:
return self._text_off 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: def performClick(self) -> bool:
self.toggle() self.toggle()
return super().performClick() return super().performClick()
@@ -688,6 +749,17 @@ class ImageView(View):
def getCropToPadding(self) -> bool: def getCropToPadding(self) -> bool:
return self._crop_to_padding 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): def setBaseline(self, baseline: int):
self._baseline = baseline self._baseline = baseline
@@ -1110,6 +1182,10 @@ class ListView(AdapterView):
def getCheckItemIds(self) -> List[int]: def getCheckItemIds(self) -> List[int]:
return list(self._checked_id_positions.keys()) return list(self._checked_id_positions.keys())
def clearChoices(self):
self._checked_item_positions.clear()
self._checked_id_positions.clear()
class GridView(AdapterView): class GridView(AdapterView):
"""GridView — 网格视图""" """GridView — 网格视图"""
@@ -1254,27 +1330,47 @@ class RecyclerView(ViewGroup):
return False return False
def notifyDataSetChanged(self): def notifyDataSetChanged(self):
pass for observer in getattr(self, '_observers', []):
observer.onChanged()
def notifyItemChanged(self, position: int): def notifyItemChanged(self, position, payload=None):
pass for observer in getattr(self, '_observers', []):
observer.onItemRangeChanged(position, 1, payload)
def notifyItemInserted(self, position: int): def notifyItemInserted(self, position):
pass for observer in getattr(self, '_observers', []):
observer.onItemRangeInserted(position, 1)
def notifyItemRemoved(self, position: int): def notifyItemRemoved(self, position):
pass for observer in getattr(self, '_observers', []):
observer.onItemRangeRemoved(position, 1)
def notifyItemMoved(self, from_position: int, to_position: int): def notifyItemMoved(self, fromPosition, toPosition):
pass for observer in getattr(self, '_observers', []):
observer.onItemRangeMoved(fromPosition, toPosition, 1)
def notifyItemRangeChanged(self, position_start: int, item_count: int): def notifyItemRangeChanged(self, positionStart, itemCount, payload=None):
pass for observer in getattr(self, '_observers', []):
observer.onItemRangeChanged(positionStart, itemCount, payload)
def notifyItemRangeInserted(self, position_start: int, item_count: int): def notifyItemRangeInserted(self, positionStart, itemCount):
pass for observer in getattr(self, '_observers', []):
observer.onItemRangeInserted(positionStart, itemCount)
def notifyItemRangeRemoved(self, position_start: int, item_count: int): 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 pass
def getStateRestorationPolicy(self) -> int: def getStateRestorationPolicy(self) -> int:
@@ -2062,11 +2158,13 @@ class ScrollView(FrameLayout):
def arrowScroll(self, direction: int) -> bool: def arrowScroll(self, direction: int) -> bool:
return True return True
def smoothScrollTo(self, x: int, y: int): def smoothScrollTo(self, x, y):
self._scroll_position = y self._scroll_x = x
self._scroll_y = y
def smoothScrollBy(self, dx: int, dy: int): def smoothScrollBy(self, dx, dy):
self._scroll_position += 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: def pageScroll(self, direction: int) -> bool:
return True return True
@@ -2100,11 +2198,13 @@ class HorizontalScrollView(FrameLayout):
def arrowScroll(self, direction: int) -> bool: def arrowScroll(self, direction: int) -> bool:
return True return True
def smoothScrollTo(self, x: int, y: int): def smoothScrollTo(self, x, y):
pass self._scroll_x = x
self._scroll_y = y
def smoothScrollBy(self, dx: int, dy: int): def smoothScrollBy(self, dx, dy):
pass self._scroll_x = getattr(self, '_scroll_x', 0) + dx
self._scroll_y = getattr(self, '_scroll_y', 0) + dy
def pageScroll(self, direction: int) -> bool: def pageScroll(self, direction: int) -> bool:
return True return True
@@ -2789,15 +2889,23 @@ class BaseAdapter(Stub):
return self.getView(position, convert_view, parent) return self.getView(position, convert_view, parent)
def notifyDataSetChanged(self): def notifyDataSetChanged(self):
pass for observer in getattr(self, '_observers', []):
observer.onChanged()
def notifyDataSetInvalidated(self): def notifyDataSetInvalidated(self):
pass for observer in getattr(self, '_observers', []):
observer.onInvalidated()
def registerDataSetObserver(self, observer: Any): def registerDataSetObserver(self, observer):
pass if not hasattr(self, '_observers'):
self._observers = []
self._observers.append(observer)
def unregisterDataSetObserver(self, observer: Any): def unregisterDataSetObserver(self, observer):
if hasattr(self, '_observers'):
try:
self._observers.remove(observer)
except ValueError:
pass pass
@@ -3589,9 +3697,15 @@ class Toast(Stub):
return toast return toast
def show(self): def show(self):
pass if _USE_LINUX:
LinuxNotify.send("Toast", self._text)
else:
print(f"[Toast] {self._text}")
def cancel(self): def cancel(self):
if _USE_LINUX:
# 桌面通知自动消失,无需操作
return
pass pass
def setGravity(self, gravity: int, x_offset: int, y_offset: int): def setGravity(self, gravity: int, x_offset: int, y_offset: int):
+68 -34
View File
@@ -11,6 +11,35 @@ import threading
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError 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 from ..java.lang import RuntimeException
@@ -135,7 +164,7 @@ class Adapter(Stub):
return f"Adapter(count={self.getCount()})" return f"Adapter(count={self.getCount()})"
class AdapterView(Stub): class AdapterView(_BaseAdapterView):
"""AdapterView — 适配器视图""" """AdapterView — 适配器视图"""
ITEM_VIEW_TYPE_IGNORE = -1 ITEM_VIEW_TYPE_IGNORE = -1
@@ -326,7 +355,7 @@ class ThemedSpinnerAdapter(Stub):
# AutoCompleteTextView # AutoCompleteTextView
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class AutoCompleteTextView(Stub): class AutoCompleteTextView(_BaseAutoCompleteTextView):
"""AutoCompleteTextView — 自动完成文本视图""" """AutoCompleteTextView — 自动完成文本视图"""
THRESHOLD_DEFAULT = 2 THRESHOLD_DEFAULT = 2
@@ -429,7 +458,7 @@ class AutoCompleteTextView(Stub):
# ProgressBar / SeekBar / AbsSeekBar / RatingBar # ProgressBar / SeekBar / AbsSeekBar / RatingBar
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ProgressBar(Stub): class ProgressBar(_BaseProgressBar):
"""ProgressBar — 进度条扩展""" """ProgressBar — 进度条扩展"""
STYLE_HORIZONTAL = 0x00000001 STYLE_HORIZONTAL = 0x00000001
@@ -568,11 +597,11 @@ class AbsSeekBar(ProgressBar):
self._on_seek_bar_change = listener self._on_seek_bar_change = listener
class SeekBar(AbsSeekBar): class SeekBar(_BaseSeekBar, AbsSeekBar):
"""SeekBar — 拖动条""" """SeekBar — 拖动条"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
super().__init__(context) AbsSeekBar.__init__(self, context)
self._on_seek_bar_change: Optional[Callable] = None self._on_seek_bar_change: Optional[Callable] = None
def setOnSeekBarChangeListener(self, listener: Optional[Callable]) -> None: def setOnSeekBarChangeListener(self, listener: Optional[Callable]) -> None:
@@ -585,12 +614,12 @@ class SeekBar(AbsSeekBar):
return f"SeekBar(progress={self.getProgress()})" return f"SeekBar(progress={self.getProgress()})"
class RatingBar(ProgressBar): class RatingBar(_BaseRatingBar, ProgressBar):
"""RatingBar — 评分条""" """RatingBar — 评分条"""
def __init__(self, context: Any = None, num_stars: int = 5, def __init__(self, context: Any = None, num_stars: int = 5,
step_size: float = 0.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._num_stars = num_stars
self._step_size = step_size self._step_size = step_size
self._rating: float = 0.0 self._rating: float = 0.0
@@ -638,7 +667,7 @@ class RatingBar(ProgressBar):
# Chronometer # Chronometer
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class Chronometer(Stub): class Chronometer(_BaseChronometer):
"""Chronometer — 计时器""" """Chronometer — 计时器"""
FORMAT_ELAPSED_TIME = 1 FORMAT_ELAPSED_TIME = 1
@@ -734,11 +763,12 @@ class CompoundButton(Stub):
return f"CompoundButton(checked={self._checked})" return f"CompoundButton(checked={self._checked})"
class ToggleButton(CompoundButton): class ToggleButton(_BaseToggleButton, CompoundButton):
"""ToggleButton — 开关按钮""" """ToggleButton — 开关按钮"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
super().__init__(context) super().__init__(context)
CompoundButton.__init__(self, context)
self._text_on: str = "ON" self._text_on: str = "ON"
self._text_off: str = "OFF" self._text_off: str = "OFF"
@@ -758,11 +788,12 @@ class ToggleButton(CompoundButton):
return f"ToggleButton(checked={self.isChecked()})" return f"ToggleButton(checked={self.isChecked()})"
class RadioButton(CompoundButton): class RadioButton(_BaseRadioButton, CompoundButton):
"""RadioButton — 单选按钮""" """RadioButton — 单选按钮"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
super().__init__(context) super().__init__(context)
CompoundButton.__init__(self, context)
def toggle(self) -> None: def toggle(self) -> None:
if not self.isChecked(): if not self.isChecked():
@@ -827,7 +858,7 @@ class RadioGroup(Stub):
# TableLayout / TableRow # TableLayout / TableRow
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class TableLayout(Stub): class TableLayout(_BaseTableLayout):
"""TableLayout — 表格布局""" """TableLayout — 表格布局"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
@@ -863,7 +894,7 @@ class TableLayout(Stub):
return f"TableLayout(rows={len(self._rows)})" return f"TableLayout(rows={len(self._rows)})"
class TableRow(Stub): class TableRow(_BaseTableRow):
"""TableRow — 表格行""" """TableRow — 表格行"""
class LayoutParams(Stub): class LayoutParams(Stub):
@@ -1029,7 +1060,7 @@ class GridLayout(Stub):
# LinearLayout / FrameLayout / RelativeLayout # LinearLayout / FrameLayout / RelativeLayout
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class LinearLayout(Stub): class LinearLayout(_BaseLinearLayout):
"""LinearLayout — 线性布局""" """LinearLayout — 线性布局"""
HORIZONTAL = 0 HORIZONTAL = 0
@@ -1120,7 +1151,7 @@ class LinearLayout(Stub):
return f"LinearLayout(orientation={'H' if self._orientation == 0 else 'V'})" return f"LinearLayout(orientation={'H' if self._orientation == 0 else 'V'})"
class FrameLayout(Stub): class FrameLayout(_BaseFrameLayout):
"""FrameLayout — 帧布局""" """FrameLayout — 帧布局"""
class LayoutParams(Stub): class LayoutParams(Stub):
@@ -1177,7 +1208,7 @@ class FrameLayout(Stub):
return "FrameLayout()" return "FrameLayout()"
class RelativeLayout(Stub): class RelativeLayout(_BaseRelativeLayout):
"""RelativeLayout — 相对布局""" """RelativeLayout — 相对布局"""
TRUE = -1 TRUE = -1
@@ -1284,7 +1315,7 @@ class RelativeLayout(Stub):
# NumberPicker / SearchView / TabHost / TabWidget # NumberPicker / SearchView / TabHost / TabWidget
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class NumberPicker(Stub): class NumberPicker(_BaseNumberPicker):
"""NumberPicker — 数字选择器""" """NumberPicker — 数字选择器"""
def __init__(self, context: Any = None): 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}])" return f"NumberPicker(value={self._value}, range=[{self._min_value}-{self._max_value}])"
class SearchView(Stub): class SearchView(_BaseSearchView):
"""SearchView — 搜索视图扩展""" """SearchView — 搜索视图扩展"""
ICONIFIED = 0 ICONIFIED = 0
@@ -1425,7 +1456,7 @@ class SearchView(Stub):
return f"SearchView(query={self._query})" return f"SearchView(query={self._query})"
class TabHost(Stub): class TabHost(_BaseTabHost):
"""TabHost — 标签宿主""" """TabHost — 标签宿主"""
class TabSpec(Stub): class TabSpec(Stub):
@@ -1493,7 +1524,7 @@ class TabHost(Stub):
return f"TabHost(tabs={len(self._tabs)}, current={self._current_tab})" return f"TabHost(tabs={len(self._tabs)}, current={self._current_tab})"
class TabWidget(Stub): class TabWidget(_BaseTabWidget):
"""TabWidget — 标签小部件""" """TabWidget — 标签小部件"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
@@ -1529,7 +1560,7 @@ class TabWidget(Stub):
# TextSwitcher / ViewSwitcher / ViewAnimator / ImageSwitcher # TextSwitcher / ViewSwitcher / ViewAnimator / ImageSwitcher
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ViewAnimator(Stub): class ViewAnimator(_BaseViewAnimator):
"""ViewAnimator — 视图动画器""" """ViewAnimator — 视图动画器"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
@@ -1595,7 +1626,7 @@ class ViewAnimator(Stub):
return f"ViewAnimator(children={len(self._children)}, displayed={self._displayed_child})" return f"ViewAnimator(children={len(self._children)}, displayed={self._displayed_child})"
class ViewSwitcher(ViewAnimator): class ViewSwitcher(_BaseViewSwitcher, ViewAnimator):
"""ViewSwitcher — 视图切换器""" """ViewSwitcher — 视图切换器"""
class ViewFactory(Stub): class ViewFactory(Stub):
@@ -1605,6 +1636,7 @@ class ViewSwitcher(ViewAnimator):
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
super().__init__(context) super().__init__(context)
ViewAnimator.__init__(self, context)
self._factory: Optional[ViewSwitcher.ViewFactory] = None self._factory: Optional[ViewSwitcher.ViewFactory] = None
def setFactory(self, factory: ViewFactory) -> None: def setFactory(self, factory: ViewFactory) -> None:
@@ -1623,11 +1655,12 @@ class ViewSwitcher(ViewAnimator):
return f"ViewSwitcher()" return f"ViewSwitcher()"
class TextSwitcher(ViewSwitcher): class TextSwitcher(_BaseTextSwitcher, ViewSwitcher):
"""TextSwitcher — 文本切换器""" """TextSwitcher — 文本切换器"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
super().__init__(context) super().__init__(context)
ViewSwitcher.__init__(self, context)
self._factory = None self._factory = None
def setCurrentText(self, text: str) -> None: def setCurrentText(self, text: str) -> None:
@@ -1640,11 +1673,12 @@ class TextSwitcher(ViewSwitcher):
return "TextSwitcher()" return "TextSwitcher()"
class ImageSwitcher(ViewSwitcher): class ImageSwitcher(_BaseImageSwitcher, ViewSwitcher):
"""ImageSwitcher — 图片切换器""" """ImageSwitcher — 图片切换器"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
super().__init__(context) super().__init__(context)
ViewSwitcher.__init__(self, context)
def setImageResource(self, res_id: int) -> None: def setImageResource(self, res_id: int) -> None:
pass pass
@@ -1659,7 +1693,7 @@ class ImageSwitcher(ViewSwitcher):
return "ImageSwitcher()" return "ImageSwitcher()"
class ImageButton(Stub): class ImageButton(_BaseImageButton):
"""ImageButton — 图片按钮""" """ImageButton — 图片按钮"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
@@ -1711,7 +1745,7 @@ class ZoomButton(ImageButton):
return f"ZoomButton(zoomIn={self._zoom_in})" return f"ZoomButton(zoomIn={self._zoom_in})"
class ZoomControls(Stub): class ZoomControls(_BaseZoomControls):
"""ZoomControls — 缩放控件""" """ZoomControls — 缩放控件"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
@@ -1757,7 +1791,7 @@ class ZoomControls(Stub):
# DigitalClock / TextClock / AnalogClock # DigitalClock / TextClock / AnalogClock
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class DigitalClock(Stub): class DigitalClock(_BaseDigitalClock):
"""DigitalClock — 数字时钟""" """DigitalClock — 数字时钟"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
@@ -1825,7 +1859,7 @@ class TextClock(Stub):
return f"TextClock(time={self.getText()})" return f"TextClock(time={self.getText()})"
class AnalogClock(Stub): class AnalogClock(_BaseAnalogClock):
"""AnalogClock — 模拟时钟""" """AnalogClock — 模拟时钟"""
def __init__(self, context: Any = None): def __init__(self, context: Any = None):
@@ -1882,7 +1916,7 @@ class QuickContactBadge(Stub):
# 各种 Adapter 实现 # 各种 Adapter 实现
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class BaseAdapter(Adapter): class BaseAdapter(_BaseBaseAdapter, Adapter):
"""BaseAdapter — 基础适配器""" """BaseAdapter — 基础适配器"""
def __init__(self): def __init__(self):
@@ -1923,12 +1957,12 @@ class BaseAdapter(Adapter):
return self.getCount() == 0 return self.getCount() == 0
class ArrayAdapter(BaseAdapter): class ArrayAdapter(_BaseArrayAdapter, BaseAdapter):
"""ArrayAdapter — 数组适配器""" """ArrayAdapter — 数组适配器"""
def __init__(self, context: Any = None, resource: int = 0, def __init__(self, context: Any = None, resource: int = 0,
objects: Optional[List[Any]] = None): objects: Optional[List[Any]] = None):
super().__init__() BaseAdapter.__init__(self)
self._context = context self._context = context
self._resource = resource self._resource = resource
self._drop_down_resource: int = resource self._drop_down_resource: int = resource
@@ -1990,7 +2024,7 @@ class ArrayAdapter(BaseAdapter):
return f"ArrayAdapter(items={len(self._objects)})" return f"ArrayAdapter(items={len(self._objects)})"
class SimpleAdapter(BaseAdapter): class SimpleAdapter(_BaseSimpleAdapter, BaseAdapter):
"""SimpleAdapter — 简单适配器""" """SimpleAdapter — 简单适配器"""
def __init__(self, context: Any = None, def __init__(self, context: Any = None,
@@ -1998,7 +2032,7 @@ class SimpleAdapter(BaseAdapter):
resource: int = 0, resource: int = 0,
from_keys: Optional[List[str]] = None, from_keys: Optional[List[str]] = None,
to_ids: Optional[List[int]] = None): to_ids: Optional[List[int]] = None):
super().__init__() BaseAdapter.__init__(self)
self._context = context self._context = context
self._data: List[Dict[str, Any]] = data or [] self._data: List[Dict[str, Any]] = data or []
self._resource = resource self._resource = resource
@@ -2031,7 +2065,7 @@ class SimpleAdapter(BaseAdapter):
return f"SimpleAdapter(items={len(self._data)})" return f"SimpleAdapter(items={len(self._data)})"
class CursorAdapter(BaseAdapter): class CursorAdapter(_BaseCursorAdapter, BaseAdapter):
"""CursorAdapter — 游标适配器""" """CursorAdapter — 游标适配器"""
FLAG_AUTO_REQUERY = 1 FLAG_AUTO_REQUERY = 1
@@ -2039,7 +2073,7 @@ class CursorAdapter(BaseAdapter):
def __init__(self, context: Any = None, cursor: Any = None, def __init__(self, context: Any = None, cursor: Any = None,
flags: int = FLAG_AUTO_REQUERY): flags: int = FLAG_AUTO_REQUERY):
super().__init__() BaseAdapter.__init__(self)
self._context = context self._context = context
self._cursor = cursor self._cursor = cursor
self._flags = flags self._flags = flags
+200 -17
View File
@@ -237,7 +237,10 @@ class File(Stub):
return _os.path.abspath(self._path) return _os.path.abspath(self._path)
def getCanonicalPath(self): def getCanonicalPath(self):
try:
return _os.path.realpath(self._path) return _os.path.realpath(self._path)
except Exception:
return self.getAbsolutePath()
def getParent(self): def getParent(self):
p = _os.path.dirname(self._path) p = _os.path.dirname(self._path)
@@ -439,6 +442,43 @@ class File(Stub):
def toPath(self): def toPath(self):
return _PathLibPath(self._path) 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): def __str__(self):
return self._path return self._path
@@ -705,14 +745,48 @@ class BufferedInputStream(FilterInputStream):
return total if total > 0 else -1 return total if total > 0 else -1
def _fill(self): def _fill(self):
data = self._in.read(bytearray(self._buf_size), 0, self._buf_size) tmp = bytearray(self._buf_size)
if data is None or data == -1: n = self._in.read(tmp, 0, self._buf_size)
if n is None or n == -1:
return False return False
n = data if isinstance(data, int) else len(data) if isinstance(n, (bytes, bytearray)):
self._buffer = bytearray(self._in._buffer if hasattr(self._in, '_buffer') else data) if n > 0 else 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._pos = 0
self._count = n 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): def newLine(self):
self._buffer.append("\n") 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): def flush(self):
if self._buffer: if self._buffer:
data = "".join(self._buffer) data = "".join(self._buffer)
@@ -1131,6 +1225,20 @@ class PrintWriter(Closeable, Flushable):
else: else:
self._write_str(str(s)) 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): def format(self, fmt, *args):
s = str(fmt) % args if args else str(fmt) s = str(fmt) % args if args else str(fmt)
self._write_str(s) self._write_str(s)
@@ -1216,6 +1324,9 @@ class ByteArrayInputStream(Closeable):
def reset(self): def reset(self):
self._pos = self._mark self._pos = self._mark
def markSupported(self):
return True
# ============================================================ # ============================================================
# ByteArrayOutputStream # ByteArrayOutputStream
@@ -1243,6 +1354,11 @@ class ByteArrayOutputStream(Closeable, Flushable):
def toByteArray(self): def toByteArray(self):
return bytes(self._buf) return bytes(self._buf)
def writeTo(self, out):
if self._closed:
raise IOException("Stream closed")
out.write(bytes(self._buf))
def size(self): def size(self):
return len(self._buf) return len(self._buf)
@@ -1690,37 +1806,104 @@ class PushbackInputStream(FilterInputStream):
# SequenceInputStream # 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): class SequenceInputStream(Closeable):
def __init__(self, streams): def __init__(self, *args):
super().__init__() super().__init__()
if hasattr(streams, 'hasMoreElements'):
self._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(): while streams.hasMoreElements():
self._streams.append(streams.nextElement()) self._streams.append(streams.nextElement())
else: else:
self._streams = list(streams) # single stream
self._current_idx = 0 self._streams = [streams]
else:
# SequenceInputStream(s1, s2, ...)
self._streams = list(args)
def _current(self): def _current(self):
if self._current_idx < len(self._streams): while self._current_idx < len(self._streams):
return self._streams[self._current_idx] inp = self._streams[self._current_idx]
if inp is None:
self._current_idx += 1
continue
return inp
return None return None
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
def read(self, b=None, off=None, length=None): def read(self, b=None, off=None, length=None):
while self._current(): if self._closed:
inp = self._current() raise IOException("Stream closed")
if b is None: if b is None:
while True:
inp = self._current()
if inp is None:
return -1
result = inp.read() result = inp.read()
else:
result = inp.read(b, off, length)
if result != -1: if result != -1:
return result return result
self._current_idx += 1 self._advance()
return -1 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): def close(self):
self._closed = True
for s in self._streams: for s in self._streams:
if s is not None:
try:
s.close() s.close()
except Exception:
pass
# ============================================================ # ============================================================
+344 -2
View File
@@ -8,6 +8,7 @@ import re as _re
import math as _py_math import math as _py_math
import sys as _sys import sys as _sys
import os as _os import os as _os
import platform
import time as _time import time as _time
import threading as _threading import threading as _threading
import inspect as _inspect import inspect as _inspect
@@ -28,6 +29,12 @@ __all__ = [
"CloneNotSupportedException", "IllegalMonitorStateException", "CloneNotSupportedException", "IllegalMonitorStateException",
"IllegalThreadStateException", "EnumConstantNotPresentException", "IllegalThreadStateException", "EnumConstantNotPresentException",
"TypeNotPresentException", "TypeNotPresentException",
"ClassNotFoundException", "IllegalAccessException", "InstantiationException",
"InvocationTargetException", "NoSuchElementException",
"AssertionError", "OutOfMemoryError", "StackOverflowError",
"NoClassDefFoundError", "UnsatisfiedLinkError", "LinkageError",
"VerifyError", "AbstractMethodError", "IncompatibleClassChangeError",
"ExceptionInInitializerError",
"String", "Integer", "Long", "Short", "Byte", "Float", "Double", "String", "Integer", "Long", "Short", "Byte", "Float", "Double",
"Boolean", "Character", "Math", "StrictMath", "System", "Boolean", "Character", "Math", "StrictMath", "System",
"Thread", "ThreadLocal", "StringBuilder", "StringBuffer", "Thread", "ThreadLocal", "StringBuilder", "StringBuffer",
@@ -227,6 +234,80 @@ class TypeNotPresentException(RuntimeException):
self.typeName = typeName 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 # StackTraceElement
# ============================================================ # ============================================================
@@ -753,6 +834,62 @@ class Integer(Number, Comparable):
return Integer(int(env_val)) return Integer(int(env_val))
return Integer(val) if val is not None else None 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): def toString(self):
return String(str(self._value)) return String(str(self._value))
@@ -1418,9 +1555,92 @@ class Math:
@staticmethod @staticmethod
def copySign(m, s): return abs(m) * (1 if s >= 0 else -1) def copySign(m, s): return abs(m) * (1 if s >= 0 else -1)
@staticmethod @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 @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): class StrictMath(Math):
@@ -1529,6 +1749,39 @@ class System:
System.err = err_stream System.err = err_stream
@staticmethod @staticmethod
def lineSeparator(): return _os.linesep 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 in_ = _sys.stdin
out = _sys.stdout out = _sys.stdout
err = _sys.stderr err = _sys.stderr
@@ -1683,6 +1936,66 @@ class Thread(Runnable):
def getDefaultUncaughtExceptionHandler(): def getDefaultUncaughtExceptionHandler():
return Thread._default_uncaught_exception_handler 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): class ThreadLocal(Stub):
"""java.lang.ThreadLocal""" """java.lang.ThreadLocal"""
@@ -1744,6 +2057,35 @@ class StringBuilder(Stub):
def lastIndexOf(self, s, f=None): def lastIndexOf(self, s, f=None):
return self.toString().rfind(s) 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): class StringBuffer(StringBuilder):
"""java.lang.StringBuffer — 线程安全的可变字符序列""" """java.lang.StringBuffer — 线程安全的可变字符序列"""
+201 -24
View File
@@ -24,7 +24,7 @@ __all__ = [
"InetAddress", "InetSocketAddress", "InetAddress", "InetSocketAddress",
"Socket", "ServerSocket", "DatagramSocket", "DatagramPacket", "Socket", "ServerSocket", "DatagramSocket", "DatagramPacket",
"Proxy", "ProxySelector", "Authenticator", "PasswordAuthentication", "Proxy", "ProxySelector", "Authenticator", "PasswordAuthentication",
"NetworkInterface", "URI", "NetworkInterface", "InterfaceAddress", "URI",
"CookieHandler", "CookieManager", "HttpCookie", "CookieHandler", "CookieManager", "HttpCookie",
"URLDecoder", "URLEncoder", "URLDecoder", "URLEncoder",
"SocketException", "ConnectException", "BindException", "SocketException", "ConnectException", "BindException",
@@ -1289,26 +1289,79 @@ class PasswordAuthentication(Stub):
return self._password 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 # NetworkInterface
# ============================================================ # ============================================================
class NetworkInterface(Stub): class NetworkInterface(Stub):
_cache = {} _cache = {}
_initialized = False
def __init__(self, name, display_name, addrs): def __init__(self, name, display_name, addrs):
super().__init__() super().__init__()
self._name = name self._name = name
self._display_name = display_name self._display_name = display_name
self._addrs = addrs 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 @staticmethod
def getByName(name): def getByName(name):
if name in NetworkInterface._cache: if name is None:
return NetworkInterface._cache[name] return None
NetworkInterface._ensure_init()
return NetworkInterface._cache.get(name)
@staticmethod @staticmethod
def getByInetAddress(addr): def getByInetAddress(addr):
NetworkInterface._ensure_init()
for ni in NetworkInterface._cache.values(): for ni in NetworkInterface._cache.values():
for a in ni._addrs: for a in ni._addrs:
if a.equals(addr): if a.equals(addr):
@@ -1317,8 +1370,18 @@ class NetworkInterface(Stub):
@staticmethod @staticmethod
def getNetworkInterfaces(): def getNetworkInterfaces():
NetworkInterface._ensure_init()
return list(NetworkInterface._cache.values()) 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): def getName(self):
return self._name return self._name
@@ -1329,25 +1392,25 @@ class NetworkInterface(Stub):
return iter(self._addrs) return iter(self._addrs)
def getInterfaceAddresses(self): def getInterfaceAddresses(self):
return self._addrs return list(self._interface_addrs)
def isUp(self): def isUp(self):
return True return self._state.lower() == "up" or (self._flags & 0x1) != 0
def isLoopback(self): def isLoopback(self):
return self._name == "lo" return (self._flags & 0x8) != 0 or self._name == "lo"
def isPointToPoint(self): def isPointToPoint(self):
return False return (self._flags & 0x10) != 0
def supportsMulticast(self): def supportsMulticast(self):
return True return (self._flags & 0x1000) != 0
def getHardwareAddress(self): def getHardwareAddress(self):
return None return self._mac
def getMTU(self): def getMTU(self):
return 1500 return self._mtu
def getSubInterfaces(self): def getSubInterfaces(self):
return iter([]) return iter([])
@@ -1363,25 +1426,139 @@ class NetworkInterface(Stub):
@staticmethod @staticmethod
def _init_from_socket(): def _init_from_socket():
import netifaces """从 /sys/class/net/ 和 socket/fcntl 枚举真实网络接口 (不依赖 netifaces)"""
for iface_name in netifaces.interfaces(): import os as _os
addrs = [] net_dir = "/sys/class/net"
ifaddresses = netifaces.ifaddresses(iface_name) if not _os.path.isdir(net_dir):
for family in [netifaces.AF_INET, netifaces.AF_INET6]: # 非 Linux 环境: 尝试用 socket 获取本机地址
if family in ifaddresses:
for addr_info in ifaddresses[family]:
try: try:
addr = addr_info["addr"] hostname = _socket.gethostname()
if "." in addr: addr = InetAddress.getByName(hostname)
addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET, addr))) ni = NetworkInterface(hostname, hostname, [addr])
else: NetworkInterface._cache[hostname] = ni
addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET6, addr)))
except Exception: except Exception:
pass pass
NetworkInterface._cache[iface_name] = NetworkInterface(iface_name, iface_name, addrs) 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 = []
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:
sock.close()
except Exception:
pass
# --- 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: try:
NetworkInterface._init_from_socket() NetworkInterface._ensure_init()
except Exception: except Exception:
pass pass
+396
View File
@@ -148,6 +148,240 @@ class Enumeration(Stub):
return Iterator(self._it) 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 # ArrayList
# ============================================================ # ============================================================
@@ -294,6 +528,12 @@ class ArrayList(Stub):
def replaceAll(self, operator) -> None: def replaceAll(self, operator) -> None:
self._data = [operator(x) for x in self._data] 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: def equals(self, other) -> bool:
if isinstance(other, ArrayList): if isinstance(other, ArrayList):
return self._data == other._data return self._data == other._data
@@ -789,6 +1029,10 @@ class LinkedHashMap(HashMap):
key = next(iter(self._data)) key = next(iter(self._data))
return (key, self._data[key]) return (key, self._data[key])
def clone(self) -> 'LinkedHashMap':
result = LinkedHashMap(self._data, 0.75, self._access_order)
return result
# ============================================================ # ============================================================
# TreeMap # TreeMap
@@ -993,6 +1237,48 @@ class TreeMap(Stub):
return True return True
return False 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: def size(self) -> int:
return len(self._data) return len(self._data)
@@ -1140,6 +1426,19 @@ class HashSet(Stub):
for item in list(self._data): for item in list(self._data):
action(item) 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: def equals(self, other) -> bool:
if isinstance(other, HashSet): if isinstance(other, HashSet):
return self._data == other._data return self._data == other._data
@@ -1848,6 +2147,60 @@ class Collections:
return _LifoQueue(deque) 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 # Arrays
@@ -2334,6 +2687,37 @@ class Calendar(Stub):
def getInstance() -> 'GregorianCalendar': def getInstance() -> 'GregorianCalendar':
return 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): class GregorianCalendar(Calendar):
"""java.util.GregorianCalendar""" """java.util.GregorianCalendar"""
@@ -3360,6 +3744,18 @@ class PriorityQueue(Stub):
for item in list(self._heap): for item in list(self._heap):
action(item) 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: def toString(self) -> str:
items = ", ".join(str(x) for x in self._heap) items = ", ".join(str(x) for x in self._heap)
return f"[{items}]" return f"[{items}]"
+52
View File
@@ -115,6 +115,41 @@ class JSONObject(Stub):
self._map[k] = arr self._map[k] = arr
return self 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): def get(self, key):
k = str(key) k = str(key)
if k not in self._map: if k not in self._map:
@@ -239,6 +274,14 @@ class JSONObject(Stub):
def has(self, key): def has(self, key):
return str(key) in self._map 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): def remove(self, key):
k = str(key) k = str(key)
if k in self._map: if k in self._map:
@@ -255,6 +298,15 @@ class JSONObject(Stub):
arr.put(k) arr.put(k)
return arr 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): def length(self):
return len(self._map) return len(self._map)
+64
View File
@@ -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",
],
)
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+141
View File
@@ -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()
+400
View File
@@ -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()
+73
View File
@@ -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()
+97
View File
@@ -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()
View File
Whitespace-only changes.
+145
View File
@@ -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)