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 10265 additions and 562 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",
"Account", "AccountManager",
"Activity", "ActivityManager",
"Fragment", "FragmentManager", "FragmentTransaction",
"Dialog", "AlertDialog", "ProgressDialog",
"DatePickerDialog", "TimePickerDialog",
"Service", "Application",
"PendingIntent", "ComponentName", "AlarmManager",
"NotificationChannel", "NotificationChannelGroup",
"NotificationCompat", "TaskStackBuilder",
"AlphaAnimation",
"Animation",
"Animator", "ValueAnimator", "ObjectAnimator", "AnimatorSet",
@@ -391,6 +398,14 @@ __all__ = [
"BatteryManager",
"BiometricPrompt",
"Bitmap", "BitmapDrawable", "BitmapFactory",
"Camera",
"Region", "PathMeasure",
"NinePatch", "Picture", "PictureDrawable",
"PorterDuff", "PorterDuffXfermode",
"Shader", "LinearGradient", "RadialGradient", "SweepGradient",
"BitmapShader", "ComposeShader",
"CornerPathEffect", "DashPathEffect", "PathDashPathEffect",
"SumPathEffect",
"BluetoothAdapter", "BluetoothDevice",
"BroadcastReceiver",
"Build",
@@ -423,6 +438,13 @@ __all__ = [
"Html",
"ImageView",
"InputDevice",
"KeyEvent", "MotionEvent",
"GestureDetector", "ScaleGestureDetector",
"SurfaceHolder", "Surface", "SurfaceView", "TextureView",
"ViewOutlineProvider",
"WindowManager", "Window",
"LayoutInflater", "Menu", "MenuItem", "ContextMenu",
"Display", "DisplayMetrics",
"InputManager",
"InputMethodManager",
"Intent",
@@ -438,6 +460,7 @@ __all__ = [
"MediaPlayer",
"MediaProjectionManager",
"MediaRecorder",
"MediaMetadataRetriever",
"MediaSession",
"MediaStore",
"Message",
@@ -453,9 +476,18 @@ __all__ = [
"PermissionManager",
"PowerManager",
"PrintAttributes", "PrintDocumentAdapter", "PrintManager",
"LinearLayout", "FrameLayout", "RelativeLayout",
"TableLayout", "TableRow",
"BaseAdapter", "ArrayAdapter", "SimpleAdapter", "CursorAdapter",
"ImageButton", "ToggleButton", "RadioButton", "SeekBar", "RatingBar",
"ViewSwitcher", "TextSwitcher", "ImageSwitcher",
"SearchView", "NumberPicker", "Chronometer",
"TabHost", "TabWidget", "ViewAnimator",
"ZoomControls", "DigitalClock", "AnalogClock",
"ProgressBar",
"RadioManager", "RadioTuner",
"Rect", "RectF",
"RecyclerView",
"Resources", "AssetManager",
"RoleManager",
"ScrollView",
@@ -466,9 +498,11 @@ __all__ = [
"Slice", "SliceManager", "SliceProvider",
"SmsManager",
"SpannableString",
"SpannableStringBuilder",
"SpeechRecognizer",
"Spinner",
"SQLiteDatabase", "SQLiteOpenHelper",
"SQLiteDatabase", "SQLiteOpenHelper", "SQLiteStatement",
"MatrixCursor",
"StorageManager", "StorageVolume",
"StructStat",
"Switch",
@@ -481,6 +515,7 @@ __all__ = [
"ThermalManager",
"TimeManager",
"Toast",
"TrafficStats",
"TranslateAnimation",
"TranslationManager",
"TvInputManager",
@@ -493,6 +528,7 @@ __all__ = [
"UwbManager",
"View", "ViewGroup",
"VirtualDisplay",
"Vibrator", "VibratorEffect",
"VpnManager", "VpnService",
"WebView",
"WifiAwareManager",
@@ -503,7 +539,9 @@ __all__ = [
"BufferedReader", "BufferedWriter",
"InputStreamReader", "OutputStreamWriter",
"URL", "HttpURLConnection", "Socket", "ServerSocket",
"NetworkInterface", "InetAddress", "InetSocketAddress", "InterfaceAddress",
"ArrayList", "HashMap", "HashSet", "LinkedList",
"TreeMap", "LinkedHashMap", "PriorityQueue",
"Collections", "Arrays", "Calendar", "Date", "Random",
"UUID", "Scanner", "Timer", "TimerTask",
"Pattern", "Matcher",
@@ -524,6 +562,14 @@ __all__ = [
"CloneNotSupportedException", "IllegalMonitorStateException",
"IllegalThreadStateException", "EnumConstantNotPresentException",
"TypeNotPresentException",
# Java Exception 体系 (扩展)
"ClassNotFoundException", "IllegalAccessException",
"InstantiationException", "InvocationTargetException",
"NoSuchElementException", "AssertionError",
"OutOfMemoryError", "StackOverflowError", "NoClassDefFoundError",
"UnsatisfiedLinkError", "LinkageError", "VerifyError",
"AbstractMethodError", "IncompatibleClassChangeError",
"ExceptionInInitializerError",
"SimpleDateFormat", "DecimalFormat",
"LocalDateTime", "LocalDate", "LocalTime", "Instant", "Duration", "Period",
"ChronoUnit", "ChronoField", "DayOfWeek", "Month", "ZoneId", "ZoneOffset",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+401 -4
View File
@@ -18,6 +18,13 @@ from .._core import (
resolve_android_path, to_android_path,
_sdcard_root, _app_data_dir, _package_name,
)
from ..java.lang import IllegalStateException
try:
from .._linux_impl import IS_LINUX, LinuxNotify, LinuxShell, LinuxProcess, LinuxDisplay, LinuxUIAutomation, LinuxPower
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
@@ -241,6 +248,108 @@ class Intent(Stub):
def filterHashCode(self) -> int:
return hash((self._action, self._data_str, self._type, self._package, self._component)) & 0x7FFFFFFF
def getParcelableExtra(self, name):
return self._extras.get(name) if self._extras else None
def setComponent(self, component):
self._component = component
return self
def getComponent(self):
return getattr(self, '_component', None)
def getDataString(self):
data = self.getData()
return str(data) if data else None
def getScheme(self):
data = self.getData()
if data:
return data.getScheme()
return None
def putExtras(self, src):
if src._extras:
if self._extras is None:
self._extras = {}
self._extras.update(src._extras)
return self
def putIntegerArrayListExtra(self, name, value):
if self._extras is None:
self._extras = {}
self._extras[name] = value
return self
def putStringArrayListExtra(self, name, value):
if self._extras is None:
self._extras = {}
self._extras[name] = value
return self
def putParcelableArrayListExtra(self, name, value):
if self._extras is None:
self._extras = {}
self._extras[name] = value
return self
def getBundleExtra(self, name):
return self._extras.get(name) if self._extras else None
def putBundleExtra(self, name, value):
if self._extras is None:
self._extras = {}
self._extras[name] = value
return self
def getParcelableArrayListExtra(self, name):
return self._extras.get(name) if self._extras else None
def getStringArrayExtra(self, name):
return self._extras.get(name) if self._extras else None
def getIntArrayExtra(self, name):
return self._extras.get(name) if self._extras else None
def getBooleanArrayExtra(self, name):
return self._extras.get(name) if self._extras else None
def getCharSequenceExtra(self, name):
return self._extras.get(name) if self._extras else None
def getCharSequenceArrayListExtra(self, name):
return self._extras.get(name) if self._extras else None
def removeCategory(self, category):
if self._categories:
self._categories.discard(category)
return self
def setSelector(self, selector):
self._selector = selector
def getSelector(self):
return getattr(self, '_selector', None)
def setSourceBounds(self, bounds):
self._source_bounds = bounds
def getSourceBounds(self):
return getattr(self, '_source_bounds', None)
def makeMainActivity(self, component):
self.addCategory("android.intent.category.LAUNCHER")
self.setComponent(component)
return self
def cloneFilter(self):
new_intent = Intent(self.getAction())
if self._categories:
new_intent._categories = set(self._categories)
if self._type:
new_intent._type = self._type
return new_intent
# ═══════════════════════════════════════════════════════════════
# ComponentName (forward reference)
@@ -732,6 +841,62 @@ class Notification(Stub):
n._extras = dict(self._extras)
return n
def setProgress(self, max, progress, indeterminate):
self._progress_max = max
self._progress = progress
self._progress_indeterminate = indeterminate
return self
def setShowWhen(self, show):
self._show_when = show
return self
def setOnlyAlertOnce(self, onlyAlertOnce):
self._only_alert_once = onlyAlertOnce
return self
def setLocalOnly(self, localOnly):
self._local_only = localOnly
return self
def setSound(self, sound):
self._sound = sound
return self
def setVibrate(self, pattern):
self._vibrate = pattern
return self
def setLights(self, argb, onMs, offMs):
self._light_color = argb
self._light_on = onMs
self._light_off = offMs
return self
def setSortKey(self, sortKey):
self._sort_key = sortKey
return self
def setTimeoutAfter(self, timeoutMs):
self._timeout_after = timeoutMs
return self
def setColorized(self, colorize):
self._colorized = colorize
return self
def setUsesChronometer(self, usesChronometer):
self._uses_chronometer = usesChronometer
return self
def setShortcutId(self, shortcutId):
self._shortcut_id = shortcutId
return self
def setContent(self, views):
self._content_view = views
return self
def __init__(self):
super().__init__()
self._channel_id = ""
@@ -834,10 +999,16 @@ class NotificationManager(Stub):
def notify(self, id_: int, notification: Notification):
self._notifications[id_] = notification
sys.stderr.write(
f"[NotificationManager] notify id={id_}: "
f"title={notification._title} text={notification._text}\n"
)
if _USE_LINUX:
title = getattr(notification, "_title", "") or ""
text = getattr(notification, "_text", "") or ""
if title or text:
LinuxNotify.send(title, text)
else:
sys.stderr.write(
f"[NotificationManager] notify id={id_}: "
f"title={notification._title} text={notification._text}\n"
)
def notifyAsPackage(self, package_name: str, tag: Optional[str], id_: int, notification: Notification):
self.notify(id_, notification)
@@ -1343,6 +1514,90 @@ class Fragment(Stub):
def unregisterForContextMenu(self, view):
pass
def getChildFragmentManager(self):
if not hasattr(self, '_child_fragment_manager'):
from .app import FragmentManager
self._child_fragment_manager = FragmentManager()
return self._child_fragment_manager
def getParentFragment(self):
return getattr(self, '_parent_fragment', None)
def onCreateOptionsMenu(self, menu, inflater):
return False
def onPrepareOptionsMenu(self, menu):
return False
def onOptionsItemSelected(self, item):
return False
def onCreateContextMenu(self, menu, v, menuInfo):
pass
def onContextItemSelected(self, item):
return False
def onContextMenuClosed(self, menu):
pass
def onConfigurationChanged(self, newConfig):
pass
def onLowMemory(self):
pass
def onHiddenChanged(self, hidden):
self._hidden = hidden
def getLayoutInflater(self):
return getattr(self, '_layout_inflater', None)
def onGetLayoutInflater(self, savedInstanceState):
return self.getLayoutInflater()
def requireContext(self):
if not hasattr(self, '_context') or self._context is None:
raise IllegalStateException("Fragment " + self.__class__.__name__ + " not attached to a context")
return self._context
def requireActivity(self):
if not hasattr(self, '_activity') or self._activity is None:
raise IllegalStateException("Fragment " + self.__class__.__name__ + " not attached to an activity")
return self._activity
def requireView(self):
if not hasattr(self, '_view') or self._view is None:
raise IllegalStateException("Fragment " + self.__class__.__name__ + " view is null")
return self._view
def requireFragmentManager(self):
fm = getattr(self, '_fragment_manager', None)
if fm is None:
raise IllegalStateException("Fragment " + self.__class__.__name__ + " not associated with a fragment manager")
return fm
def setMenuVisibility(self, menuVisible):
self._menu_visible = menuVisible
def onMenuVisibilityChanged(self, menuVisible):
pass
def postponeEnterTransition(self):
pass
def startPostponedEnterTransition(self):
pass
def getEnterTransition(self):
return getattr(self, '_enter_transition', None)
def setEnterTransition(self, transition):
self._enter_transition = transition
def onInflate(self, context, attrs, savedInstanceState):
pass
class FragmentTransaction(Stub):
"""FragmentTransaction — 管理 Fragment 事务"""
@@ -1821,6 +2076,144 @@ class Activity(Stub):
def getTheme(self):
return getattr(self, '_theme', 0)
def getDeviceInfo(self):
"""获取设备信息"""
if _USE_LINUX:
return LinuxProcess.get_system_info()
return {}
def executeShell(self, command):
"""执行 Shell 命令"""
if _USE_LINUX:
return LinuxShell.exec(command)
return {"success": False, "output": "Shell not available"}
def screenshot(self, path=None):
"""截屏"""
if _USE_LINUX:
return LinuxDisplay.screenshot(path)
return None
def getScreenSize(self):
"""获取屏幕尺寸"""
if _USE_LINUX:
return LinuxDisplay.get_screen_size()
return (1920, 1080)
def click(self, x, y):
"""点击屏幕坐标"""
if _USE_LINUX:
return LinuxUIAutomation.click(x, y)
return False
def typeText(self, text):
"""输入文本"""
if _USE_LINUX:
return LinuxUIAutomation.type_text(text)
return False
def pressKey(self, key):
"""按键"""
if _USE_LINUX:
return LinuxUIAutomation.press_key(key)
return False
def requestWindowFeature(self, featureId):
self._window_features = getattr(self, '_window_features', set())
self._window_features.add(featureId)
return True
def setTitle(self, title):
self._title = title
def getTitle(self):
return getattr(self, '_title', '')
def onKeyDown(self, keyCode, event):
return False
def onKeyUp(self, keyCode, event):
return False
def onTouchEvent(self, event):
return False
def enterPictureInPictureMode(self):
pass
def getReferrer(self):
return getattr(self, '_referrer', None)
def setReferrer(self, referrer):
self._referrer = referrer
def isTaskRoot(self):
return True
def getChangingConfigurations(self):
return getattr(self, '_changing_config', 0)
def navigateUpTo(self, intent):
return True
def shouldUpRecreateTask(self, intent):
return False
def getParentActivityIntent(self):
return None
def onOptionsMenuClosed(self, menu):
pass
def setVolumeControlStream(self, streamType):
self._volume_control_stream = streamType
def getVolumeControlStream(self):
return getattr(self, '_volume_control_stream', 0)
def getLocalClassName(self):
return self.__class__.__name__
def finishAndRemoveTask(self):
self.finish()
def getLoaderManager(self):
if not hasattr(self, '_loader_manager'):
self._loader_manager = type('LoaderManager', (), {
'initLoader': lambda *a: None,
'restartLoader': lambda *a: None,
'destroyLoader': lambda *a: None,
'getLoader': lambda *a: None,
})()
return self._loader_manager
def onRetainNonConfigurationInstance(self):
return None
def getLastNonConfigurationInstance(self):
return getattr(self, '_last_non_config_instance', None)
def setProgressBarVisibility(self, visible):
pass
def setProgress(self, progress):
pass
def setSecondaryProgress(self, secondaryProgress):
pass
def onSearchRequested(self):
return False
def startSearch(self, initialQuery, selectInitial, appSearchData, globalSearch):
pass
def startIntentSender(self, intent, fillInIntent, flagsMask, flagsValues, extraFlags):
pass
def startIntentSenderForResult(self, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags):
pass
# ═══════════════════════════════════════════════════════════════
# Service
@@ -2009,9 +2402,13 @@ class ActivityManager(Stub):
self._memory_info = self.MemoryInfo()
def getMemoryInfo(self) -> "ActivityManager.MemoryInfo":
if _USE_LINUX:
return LinuxPower.get_memory_info()
return self._memory_info
def getRunningAppProcesses(self) -> Optional[List["ActivityManager.RunningAppProcessInfo"]]:
if _USE_LINUX:
return LinuxProcess.list_processes()
return []
def killBackgroundProcesses(self, package_name: str):
+10 -14
View File
@@ -11,9 +11,10 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Callable
from enum import IntEnum
from .._core import Stub, AndroidOnlyError
from ..java.lang import RuntimeException
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
from ..java.lang import RuntimeException, IllegalArgumentException, IllegalStateException
from .app import Fragment as _BaseFragment
from .app import FragmentManager as _BaseFragmentManager
from .app import FragmentTransaction as _BaseFragmentTransaction
# ═══════════════════════════════════════════════════════════════
@@ -64,8 +65,8 @@ POP_BACK_STACK_INCLUSIVE = 1
# Fragment
# ═══════════════════════════════════════════════════════════════
class Fragment(Stub):
"""Fragment — 片段基类 (桌面存根)"""
class Fragment(_BaseFragment):
"""Fragment — 片段基类 (桌面存根,继承 app.Fragment 的所有方法)"""
def __init__(self):
super().__init__()
@@ -424,17 +425,12 @@ class Fragment(Stub):
return f"Fragment({self._mTag or hex(id(self))})"
class IllegalStateException(RuntimeException):
"""IllegalStateException — 非法状态异常"""
pass
# ═══════════════════════════════════════════════════════════════
# FragmentManager
# ═══════════════════════════════════════════════════════════════
class FragmentManager(Stub):
"""FragmentManager — 片段管理器"""
class FragmentManager(_BaseFragmentManager):
"""FragmentManager — 片段管理器 (继承 app.FragmentManager 的所有方法)"""
def __init__(self):
super().__init__()
@@ -545,8 +541,8 @@ class FragmentManager(Stub):
# FragmentTransaction
# ═══════════════════════════════════════════════════════════════
class FragmentTransaction(Stub):
"""FragmentTransaction — 片段事务"""
class FragmentTransaction(_BaseFragmentTransaction):
"""FragmentTransaction — 片段事务 (继承 app.FragmentTransaction 的所有方法)"""
TRANSIT_ENTER_MASK = TRANSIT_ENTER_MASK
TRANSIT_EXIT_MASK = TRANSIT_EXIT_MASK
+15
View File
@@ -13,6 +13,12 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
from .._core import Stub, AndroidOnlyError
try:
from .._linux_impl import IS_LINUX, LinuxProcess, LinuxPower
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
__all__ = [
"TaskManager", "TaskInfo", "TaskStackListener", "TaskSnapshot",
"TaskDescription", "ActivityManager",
@@ -908,9 +914,18 @@ class ActivityManager(Stub):
return list(self._services.values())[:maxNum]
def getRunningAppProcesses(self) -> List[RunningAppProcessInfo]:
if _USE_LINUX:
return LinuxProcess.list_processes()
return list(self._processes.values())
def getMemoryInfo(self, outInfo: MemoryInfo):
if _USE_LINUX:
mem = LinuxPower.get_memory_info()
outInfo.availMem = mem.get("available", 0)
outInfo.totalMem = mem.get("total", 0)
outInfo.threshold = int(mem.get("total", 0) * 0.1)
outInfo.lowMemory = mem.get("percent", 0) > 90.0
return outInfo
import psutil
try:
import psutil
+58
View File
@@ -14,6 +14,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxBluetooth, HAS_BLUETOOTHCTL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# BluetoothProfile
@@ -645,22 +652,36 @@ class BluetoothAdapter(Stub):
return BluetoothAdapter._default_adapter
def isEnabled(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.get_state() == LinuxBluetooth.STATE_ON
return self._enabled
def enable(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.enable()
self._enabled = True
self._state = self.STATE_ON
return True
def disable(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.disable()
self._enabled = False
self._state = self.STATE_OFF
return True
def getAddress(self) -> str:
if _USE_LINUX:
addr = LinuxBluetooth.get_address()
if addr:
return addr
return self._address
def getName(self) -> str:
if _USE_LINUX:
name = LinuxBluetooth.get_name()
if name:
return name
return self._name
def setName(self, name: str) -> bool:
@@ -668,13 +689,36 @@ class BluetoothAdapter(Stub):
return True
def getBondedDevices(self) -> Set[BluetoothDevice]:
if _USE_LINUX:
devices = set()
for d in LinuxBluetooth.get_bonded_devices():
dev = BluetoothDevice(d.get("address", "00:00:00:00:00:00"))
dev._name = d.get("name", "Unknown")
dev._bond_state = BluetoothDevice.BOND_BONDED
devices.add(dev)
return devices
return set(self._bonded_devices)
def getBondedDeviceAddresses(self) -> Set[str]:
"""获取已配对设备地址集合"""
if _USE_LINUX:
return {d.get("address", "") for d in LinuxBluetooth.get_bonded_devices()}
return {d.getAddress() for d in self._bonded_devices}
def startDiscovery(self) -> bool:
if _USE_LINUX:
try:
LinuxBluetooth.scan(timeout=5)
return True
except Exception:
return False
self._discovering = True
return True
def cancelDiscovery(self) -> bool:
if _USE_LINUX:
# bluetoothctl scan off 在 LinuxBluetooth.scan 内部已处理
return True
self._discovering = False
return True
@@ -694,6 +738,8 @@ class BluetoothAdapter(Stub):
pass
def getState(self) -> int:
if _USE_LINUX:
return LinuxBluetooth.get_state()
return self._state
def listenUsingRfcommWithServiceRecord(self, name: str, uuid_str: str) -> BluetoothServerSocket:
@@ -704,6 +750,18 @@ class BluetoothAdapter(Stub):
return BluetoothServerSocket()
def getRemoteDevice(self, address: str) -> Optional[BluetoothDevice]:
if _USE_LINUX:
# 从已配对设备中查找
for d in LinuxBluetooth.get_bonded_devices():
if d.get("address", "").upper() == address.upper():
dev = BluetoothDevice(d.get("address", address))
dev._name = d.get("name", "Unknown")
dev._bond_state = BluetoothDevice.BOND_BONDED
return dev
# 未配对但地址合法则返回一个设备对象
if BluetoothAdapter.checkBluetoothAddress(address):
return BluetoothDevice(address)
return None
if not BluetoothAdapter.checkBluetoothAddress(address):
return None
return BluetoothDevice(address)
+332 -29
View File
@@ -4,6 +4,7 @@ Android Content 组件 — Context, ContentResolver, Uri, SharedPreferences 等
"""
import json
import mimetypes
import os
import sys
import threading
@@ -16,6 +17,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
try:
from .._linux_impl import IS_LINUX, LinuxClipboard, LinuxNotify, LinuxLocation, LinuxProcess
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Uri
@@ -366,34 +373,134 @@ class ContentResolver(Stub):
super().__init__()
self._observers: Dict[str, List[Any]] = {}
@staticmethod
def _uri_to_file_path(uri) -> Optional[str]:
"""将 file:// 或纯路径 Uri 转换为本地文件系统路径"""
try:
uri_str = str(uri)
except Exception:
return None
# 处理字符串输入
if isinstance(uri, str):
if uri.startswith("file://"):
return uri[7:]
if "://" not in uri:
return uri if uri else None
return None
scheme = getattr(uri, "_scheme", "") or ""
path = getattr(uri, "_path", "") or ""
if scheme == "file":
return path if path else None
if scheme == "" and path:
return path
return None
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str],
cancellation_signal: Any = None) -> Any:
"""Query content provider"""
sys.stderr.write(f"[ContentResolver] query: uri={uri}\n")
"""Query content provider. 对 file:// URI 返回基于目录列表的 Cursor。"""
path = self._uri_to_file_path(uri)
if path is not None and os.path.isdir(path):
try:
from .database import Cursor, MatrixCursor
cols = projection or ["_id", "name", "size", "last_modified"]
cursor = MatrixCursor(cols)
for idx, name in enumerate(sorted(os.listdir(path))):
full = os.path.join(path, name)
try:
st = os.stat(full)
size = st.st_size
mtime = int(st.st_mtime * 1000)
except OSError:
size = 0
mtime = 0
cursor.addRow([idx, name, size, mtime])
return cursor
except Exception:
pass
return None
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
sys.stderr.write(f"[ContentResolver] insert: uri={uri}\n")
"""Insert. 对 file:// URI 将 ContentValues 写为 JSON 文件。"""
path = self._uri_to_file_path(uri)
if path is not None and isinstance(values, ContentValues):
try:
os.makedirs(path if os.path.isdir(path) else os.path.dirname(path) or ".", exist_ok=True)
target = path if not os.path.isdir(path) else os.path.join(path, "insert.json")
data = values._values if hasattr(values, "_values") else {}
with open(target, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, default=str)
return Uri.withAppendedPath(uri, os.path.basename(target))
except OSError:
pass
return uri
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int:
sys.stderr.write(f"[ContentResolver] update: uri={uri}\n")
"""Update. 对 file:// URI 更新对应 JSON 文件。"""
path = self._uri_to_file_path(uri)
if path is not None and isinstance(values, ContentValues) and os.path.isfile(path):
try:
data = values._values if hasattr(values, "_values") else {}
with open(path, "r", encoding="utf-8") as f:
existing = json.load(f)
if isinstance(existing, dict):
existing.update(data)
with open(path, "w", encoding="utf-8") as f:
json.dump(existing, f, ensure_ascii=False, default=str)
return 1
except (OSError, ValueError):
pass
return 0
def delete(self, uri: Uri, selection: Optional[str],
selection_args: Optional[List[str]]) -> int:
sys.stderr.write(f"[ContentResolver] delete: uri={uri}\n")
"""Delete. 对 file:// URI 删除对应文件或目录。"""
path = self._uri_to_file_path(uri)
if path is not None:
try:
if os.path.isfile(path):
os.remove(path)
return 1
if os.path.isdir(path):
import shutil
shutil.rmtree(path)
return 1
except OSError:
pass
return 0
def applyBatch(self, authority: str, operations: List[Any]) -> List[Any]:
sys.stderr.write(f"[ContentResolver] applyBatch: authority={authority}\n")
return []
"""Apply batch operations, returning per-op results."""
results: List[Any] = []
for op in operations:
try:
op_type = getattr(op, "_type", None)
uri = getattr(op, "_uri", None)
values = getattr(op, "_values", None)
if op_type == 1 and uri is not None: # INSERT
results.append(ContentProviderResult(uri=self.insert(uri, values or ContentValues())))
elif op_type == 2 and uri is not None: # UPDATE
count = self.update(uri, values or ContentValues(),
getattr(op, "_selection", None),
getattr(op, "_selection_args", None))
results.append(ContentProviderResult(count=count))
elif op_type == 3 and uri is not None: # DELETE
count = self.delete(uri, getattr(op, "_selection", None),
getattr(op, "_selection_args", None))
results.append(ContentProviderResult(count=count))
else:
results.append(ContentProviderResult(count=0))
except Exception:
results.append(ContentProviderResult(count=0))
return results
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
sys.stderr.write(f"[ContentResolver] bulkInsert: uri={uri}\n")
return len(values) if values else 0
count = 0
for v in values or []:
if self.insert(uri, v) is not None:
count += 1
return count
def call(self, uri: Uri, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
return None
@@ -420,7 +527,17 @@ class ContentResolver(Stub):
if not self._observers[uri_str]:
del self._observers[uri_str]
def getType(self, uri: Uri) -> Optional[str]:
def getType(self, uri) -> Optional[str]:
"""返回 URI 指向资源的 MIME 类型。"""
path = self._uri_to_file_path(uri)
if path:
mime, _ = mimetypes.guess_type(path)
return mime or "application/octet-stream"
scheme = getattr(uri, "_scheme", "") or ""
if isinstance(uri, str) and "://" in uri:
scheme = uri.split("://")[0]
if scheme == "content":
return "vnd.android.cursor.dir/vnd.android.content"
return None
def acquireContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]:
@@ -430,12 +547,40 @@ class ContentResolver(Stub):
return ContentProviderClient(uri)
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
return None
"""返回 URI 支持的流 MIME 类型列表。"""
mime = self.getType(uri)
if mime is None:
return None
if mime_type_filter and mime_type_filter.endswith("/*"):
prefix = mime_type_filter[:-1]
if mime.startswith(prefix):
return [mime]
return None
if mime == mime_type_filter:
return [mime]
return [mime] if mime else None
def openInputStream(self, uri: Uri) -> Any:
"""打开输入流。对 file:// URI 返回真实文件流。"""
path = self._uri_to_file_path(uri)
if path and os.path.isfile(path):
try:
return open(path, "rb")
except OSError:
return None
return None
def openOutputStream(self, uri: Uri) -> Any:
"""打开输出流。对 file:// URI 返回真实文件流。"""
path = self._uri_to_file_path(uri)
if path:
try:
d = os.path.dirname(path)
if d:
os.makedirs(d, exist_ok=True)
return open(path, "wb")
except OSError:
return None
return None
@@ -466,6 +611,7 @@ class Context(Stub):
INPUT_METHOD_SERVICE = "input_method"
CAMERA_SERVICE = "camera"
BATTERY_SERVICE = "battery"
CLIPBOARD_SERVICE = "clipboard"
# Mode 常量
MODE_PRIVATE = 0x0000
@@ -587,10 +733,16 @@ class Context(Stub):
return PackageManager()
def getResources(self):
return None
from .content_res import Resources, AssetManager
if not hasattr(self, "_resources") or self._resources is None:
self._resources = Resources(self.getAssets())
return self._resources
def getAssets(self):
return None
from .content_res import AssetManager
if not hasattr(self, "_assets") or self._assets is None:
self._assets = AssetManager()
return self._assets
def getMainLooper(self):
from .os import Looper
@@ -642,6 +794,8 @@ class Context(Stub):
if name == Context.BATTERY_SERVICE:
from .os import BatteryManager
return BatteryManager()
if name == Context.CLIPBOARD_SERVICE:
return ClipboardManager()
return self._services.get(name)
def startService(self, intent: Any) -> Optional[Any]:
@@ -659,15 +813,34 @@ class Context(Stub):
pass
def getString(self, res_id: int, *args) -> str:
"""返回资源字符串。优先委托给 Resources,否则返回合理默认值。"""
try:
res = self.getResources()
val = res.getString(res_id, *args)
if val is not None:
return val
except Exception:
pass
return str(res_id)
def getColor(self, color_id: int) -> int:
return 0
"""返回资源颜色。优先委托给 Resources,否则返回不透明黑色。"""
try:
res = self.getResources()
return res.getColor(color_id)
except Exception:
return 0xFF000000
def getDrawable(self, drawable_id: int):
return None
"""返回 Drawable。优先委托给 Resources。"""
try:
res = self.getResources()
return res.getDrawable(drawable_id)
except Exception:
return None
def getWallpaper(self):
"""返回壁纸 Drawable。桌面端返回 None(无系统壁纸)。"""
return None
def getWallpaperDesiredMinimumWidth(self) -> int:
@@ -786,18 +959,55 @@ class Context(Stub):
self._theme = resid
def getText(self, resId):
"""返回资源文本。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getText(resId)
if val is not None:
return val
except Exception:
pass
return str(resId)
def getQuantityString(self, resId, quantity, *formatArgs):
return str(resId)
"""返回复数字符串。优先委托给 Resources。"""
try:
res = self.getResources()
return res.getQuantityString(resId, quantity, *formatArgs)
except Exception:
return str(resId)
def getStringArray(self, resId):
"""返回字符串数组。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getStringArray(resId)
if val is not None:
return val
except Exception:
pass
return []
def getTextArray(self, resId):
"""返回文本数组。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getTextArray(resId)
if val is not None:
return val
except Exception:
pass
return []
def getIntArray(self, resId):
"""返回整型数组。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getIntArray(resId)
if val is not None:
return val
except Exception:
pass
return []
# ─── Context 派生 / 回调 (扩展) ───────────────────────────
@@ -1107,10 +1317,16 @@ class SharedPreferences(Stub):
return SharedPreferencesEditor(self)
def registerOnSharedPreferenceChangeListener(self, listener):
pass
if not hasattr(self, '_listeners'):
self._listeners = []
self._listeners.append(listener)
def unregisterOnSharedPreferenceChangeListener(self, listener):
pass
if hasattr(self, '_listeners'):
try:
self._listeners.remove(listener)
except ValueError:
pass
def getSharedPrefsFile(self):
return self._file_path
@@ -1177,6 +1393,12 @@ class SharedPreferencesEditor(Stub):
self._prefs._data.pop(key, None)
self._prefs._data.update(self._modified)
self._prefs._save()
# 触发监听器
sp = getattr(self, '_prefs', None)
if sp:
for key in self._modified:
for listener in getattr(sp, '_listeners', []):
listener.onSharedPreferenceChanged(sp, key)
self._modified.clear()
self._removed.clear()
self._clear_all = False
@@ -1365,29 +1587,55 @@ class ContentProvider(Stub):
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]:
"""默认实现:委托给关联 Context 的 ContentResolver。"""
if self._context is not None:
return self._context.getContentResolver().query(
uri, projection, selection, selection_args, sort_order)
return None
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
if self._context is not None:
return self._context.getContentResolver().insert(uri, values)
return None
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int:
if self._context is not None:
return self._context.getContentResolver().update(uri, values, selection, selection_args)
return 0
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int:
if self._context is not None:
return self._context.getContentResolver().delete(uri, selection, selection_args)
return 0
def getType(self, uri: Uri) -> Optional[str]:
"""返回 URI 的 MIME 类型。"""
path = ContentResolver._uri_to_file_path(uri)
if path:
mime, _ = mimetypes.guess_type(path)
return mime or "application/octet-stream"
return None
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
return None
mime = self.getType(uri)
if mime is None:
return None
return [mime]
def openFile(self, uri: Uri, mode: str) -> Any:
"""打开文件。对 file:// URI 返回真实文件对象。"""
path = ContentResolver._uri_to_file_path(uri)
if path and os.path.exists(path):
try:
fmode = "rb" if "r" in mode else "wb"
return open(path, fmode)
except OSError:
return None
return None
def openAssetFile(self, uri: Uri, mode: str) -> Any:
return None
return self.openFile(uri, mode)
def attachInfo(self, context: Context, info: Any):
self._context = context
@@ -1423,29 +1671,44 @@ class ContentProviderClient(Stub):
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]:
return None
"""委托给 ContentResolver 处理 file:// URI。"""
resolver = ContentResolver()
return resolver.query(uri, projection, selection, selection_args, sort_order)
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
return uri
resolver = ContentResolver()
return resolver.insert(uri, values)
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int:
return 0
resolver = ContentResolver()
return resolver.update(uri, values, selection, selection_args)
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int:
return 0
resolver = ContentResolver()
return resolver.delete(uri, selection, selection_args)
def applyBatch(self, operations: List[Any]) -> List[Any]:
return []
resolver = ContentResolver()
return resolver.applyBatch("", operations)
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
return len(values) if values else 0
resolver = ContentResolver()
return resolver.bulkInsert(uri, values)
def getType(self, uri: Uri) -> Optional[str]:
"""返回 URI 的 MIME 类型。"""
path = ContentResolver._uri_to_file_path(uri)
if path:
mime, _ = mimetypes.guess_type(path)
return mime or "application/octet-stream"
return None
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
return None
mime = self.getType(uri)
if mime is None:
return None
return [mime]
def call(self, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
return None
@@ -1457,13 +1720,21 @@ class ContentProviderClient(Stub):
pass
def openFile(self, uri: Uri, mode: str) -> Any:
"""打开文件。对 file:// URI 返回真实文件对象。"""
path = ContentResolver._uri_to_file_path(uri)
if path and os.path.exists(path):
try:
fmode = "rb" if "r" in mode else "wb"
return open(path, fmode)
except OSError:
return None
return None
def openAssetFile(self, uri: Uri, mode: str) -> Any:
return None
return self.openFile(uri, mode)
def openTypedAssetFile(self, uri: Uri, mime_type: str, opts: Optional[ContentValues]) -> Any:
return None
return self.openFile(uri, "r")
def getLocalContentProvider(self) -> Optional[ContentProvider]:
return None
@@ -1637,6 +1908,14 @@ class CursorLoader(Loader):
self._sort_order = sort_order
def loadInBackground(self):
"""在后台加载数据,委托给 ContentResolver.query。"""
if self._context is not None:
try:
resolver = self._context.getContentResolver()
return resolver.query(self._uri, self._projection, self._selection,
self._selection_args, self._sort_order)
except Exception:
pass
return None
@@ -1708,6 +1987,29 @@ class SyncResult(Stub):
return f"SyncResult({self.stats.numEntries} entries)"
# ═══════════════════════════════════════════════════════════════
# ClipboardManager
# ═══════════════════════════════════════════════════════════════
class ClipboardManager(Stub):
"""ClipboardManager — 剪贴板管理器"""
def getText(self):
if _USE_LINUX:
return LinuxClipboard.read()
return ""
def setText(self, text):
if _USE_LINUX:
return LinuxClipboard.write(text)
return False
def hasText(self):
if _USE_LINUX:
return len(LinuxClipboard.read()) > 0
return False
# ═══════════════════════════════════════════════════════════════
# 替换 _core.py 中的 _GlobalContext
# ═══════════════════════════════════════════════════════════════
@@ -1748,4 +2050,5 @@ __all__ = [
"DialogInterface",
"SyncAdapter",
"SyncResult",
"ClipboardManager",
]
+33 -10
View File
@@ -13,6 +13,9 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError
from ..java.lang import RuntimeException
from .app import Intent as _BaseIntent
from .app import PendingIntent as _BasePendingIntent
from .content import IntentFilter as _BaseIntentFilter
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
@@ -680,8 +683,8 @@ MIME_TYPE_ALL = "*/*"
# Intent — 完整扩展
# ═══════════════════════════════════════════════════════════════
class Intent(Stub):
"""Android Intent — 完整扩展 (桌面存根)"""
class Intent(_BaseIntent):
"""Android Intent — 完整扩展 (桌面存根,继承 app.Intent 的所有方法)"""
# ── ShortcutIconResource inner class ──
@@ -761,16 +764,36 @@ class Intent(Stub):
def getData(self) -> Optional[str]:
return self._data
def setData(self, data: str) -> "Intent":
def setData(self, data) -> "Intent":
self._data = data
self._type = None
# 从 Uri 提取 scheme
if data is not None:
if hasattr(data, 'getScheme'):
self._scheme = data.getScheme() or ""
elif isinstance(data, str):
from urllib.parse import urlparse
self._scheme = urlparse(data).scheme
return self
def getDataString(self) -> Optional[str]:
return self._data
if self._data is None:
return None
if isinstance(self._data, str):
return self._data
return str(self._data)
def getScheme(self) -> str:
return self._scheme
if self._scheme:
return self._scheme
# 从 data 动态提取
if self._data is not None:
if hasattr(self._data, 'getScheme'):
return self._data.getScheme() or ""
elif isinstance(self._data, str):
from urllib.parse import urlparse
return urlparse(self._data).scheme
return ""
# ── Type ──
@@ -1256,8 +1279,8 @@ class IntentReceiver(Stub):
# PendingIntent
# ═══════════════════════════════════════════════════════════════
class PendingIntent(Stub):
"""PendingIntent — 延迟 Intent"""
class PendingIntent(_BasePendingIntent):
"""PendingIntent — 延迟 Intent (继承 app.PendingIntent 的所有方法)"""
FLAG_ONE_SHOT = 1 << 30
FLAG_NO_CREATE = 1 << 29
@@ -1268,7 +1291,7 @@ class PendingIntent(Stub):
def __init__(self, intent: Optional[Intent] = None, requestCode: int = 0,
flags: int = 0):
super().__init__()
super().__init__(intent, requestCode, flags)
self._intent = intent
self._request_code = requestCode
self._flags = flags
@@ -1455,8 +1478,8 @@ class IntentService(Stub):
# IntentFilter
# ═══════════════════════════════════════════════════════════════
class IntentFilter(Stub):
"""IntentFilter — Intent 过滤器 (桌面存根)"""
class IntentFilter(_BaseIntentFilter):
"""IntentFilter — Intent 过滤器 (桌面存根,继承 content.IntentFilter 的所有方法)"""
class AuthorityEntry(Stub):
"""AuthorityEntry — 权限条目"""
+23
View File
@@ -36,6 +36,8 @@ class Resources(Stub):
self._drawables: Dict[str, Any] = {}
self._booleans: Dict[str, bool] = {}
self._integers: Dict[str, int] = {}
self._string_arrays: Dict[str, List[str]] = {}
self._int_arrays: Dict[str, List[int]] = {}
self._identifiers: Dict[str, int] = {}
self._next_id = 0x7f010000
self._configuration = Configuration()
@@ -125,6 +127,27 @@ class Resources(Stub):
return val
return 0
# ── Arrays ──
def getStringArray(self, id: Union[int, str]) -> List[str]:
if isinstance(id, str):
return self._string_arrays.get(id, [])
for key, val in self._string_arrays.items():
if self._identifiers.get(key) == id:
return val
return []
def getTextArray(self, id: Union[int, str]) -> List[str]:
return self.getStringArray(id)
def getIntArray(self, id: Union[int, str]) -> List[int]:
if isinstance(id, str):
return self._int_arrays.get(id, [])
for key, val in self._int_arrays.items():
if self._identifiers.get(key) == id:
return val
return []
# ── Layout / Animation / Xml ──
def getLayout(self, id: Union[int, str]) -> Any:
+19 -3
View File
@@ -331,21 +331,31 @@ class Cursor(Stub):
return self.moveToPosition(self._pos + offset)
def moveToPosition(self, position: int) -> bool:
if position < -1 or position >= self.getCount():
count = self.getCount()
if position >= count:
self._pos = count
return False
if position < -1:
self._pos = -1
return False
self._pos = position
return True
def isFirst(self) -> bool:
return self._pos == 0
return self._pos == 0 and self.getCount() > 0
def isLast(self) -> bool:
return self._pos == self.getCount() - 1
count = self.getCount()
return count > 0 and self._pos == count - 1
def isBeforeFirst(self) -> bool:
if self.getCount() == 0:
return True
return self._pos < 0
def isAfterLast(self) -> bool:
if self.getCount() == 0:
return True
return self._pos >= self.getCount()
def getColumnIndex(self, column_name: str) -> int:
@@ -370,6 +380,9 @@ class Cursor(Stub):
def getColumnNames(self) -> List[str]:
return []
def getColumnCount(self) -> int:
return len(self.getColumnNames())
def getType(self, column_index: int) -> int:
return self.FIELD_TYPE_NULL
@@ -759,6 +772,9 @@ class MatrixCursor(AbstractCursor):
def getLong(self, column_index: int) -> int:
return self.getInt(column_index)
def getShort(self, column_index: int) -> int:
return self.getInt(column_index)
def getFloat(self, column_index: int) -> float:
v = self.getString(column_index)
if v is None:
+203 -50
View File
@@ -181,9 +181,13 @@ class SQLiteProgram(SQLiteClosable):
def clearBindings(self):
self._bindings.clear()
def _ordered_bind_args(self) -> list:
"""按绑定索引升序返回参数列表(1-based 索引排序)。"""
return [self._bindings[k] for k in sorted(self._bindings.keys())]
def _bind_parameters(self, cursor: _sqlite3.Cursor):
for idx, val in sorted(self._bindings.items()):
cursor.execute(f"PRAGMA dummy={idx}")
for idx in sorted(self._bindings.keys()):
cursor.bind(idx, self._bindings[idx])
# ═══════════════════════════════════════════════════════════════
@@ -199,16 +203,18 @@ class SQLiteStatement(SQLiteProgram):
def execute(self):
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
conn.commit()
conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit()
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
def executeUpdateDelete(self) -> int:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
conn.commit()
cursor = conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit()
return cursor.rowcount
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
@@ -216,8 +222,9 @@ class SQLiteStatement(SQLiteProgram):
def executeInsert(self) -> int:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
conn.commit()
cursor = conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit()
return cursor.lastrowid
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
@@ -225,9 +232,10 @@ class SQLiteStatement(SQLiteProgram):
def simpleQueryForLong(self) -> int:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
cursor = conn.execute(self._sql, self._ordered_bind_args())
row = cursor.fetchone()
conn.commit()
if not self._db._in_transaction:
conn.commit()
if row:
return int(row[0]) if row[0] is not None else 0
raise SQLiteDoneException("no results")
@@ -237,15 +245,43 @@ class SQLiteStatement(SQLiteProgram):
def simpleQueryForString(self) -> Optional[str]:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
cursor = conn.execute(self._sql, self._ordered_bind_args())
row = cursor.fetchone()
conn.commit()
if not self._db._in_transaction:
conn.commit()
if row:
return str(row[0]) if row[0] is not None else None
raise SQLiteDoneException("no results")
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
def simpleQueryForBlobString(self) -> Optional[bytes]:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, self._ordered_bind_args())
row = cursor.fetchone()
if not self._db._in_transaction:
conn.commit()
if row:
v = row[0]
if v is None:
return None
if isinstance(v, bytes):
return v
if isinstance(v, str):
return v.encode("utf-8")
return bytes(str(v), "utf-8")
raise SQLiteDoneException("no results")
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
def execQuery(self) -> "SQLiteCursor":
"""执行查询 SQL 并返回 SQLiteCursor。"""
query = SQLiteQuery(self._db, self._sql)
for idx, val in self._bindings.items():
query.bind(idx, val)
return SQLiteCursor(self._db, query)
# ═══════════════════════════════════════════════════════════════
# SQLiteQuery
@@ -313,10 +349,17 @@ class SQLiteCursor(AbstractCursor):
def moveToPosition(self, position: int) -> bool:
self._load()
if position < -1 or position >= len(self._rows):
count = len(self._rows)
if position >= count:
self._pos = count
self._current_row = None
return False
if position < -1:
self._pos = -1
self._current_row = None
return False
self._pos = position
if 0 <= position < len(self._rows):
if 0 <= position < count:
self._current_row = self._rows[position]
else:
self._current_row = None
@@ -499,6 +542,39 @@ def _map_sqlite_error(msg: str, sql: str = "") -> SQLiteException:
return SQLiteException(msg)
def _extract_value_pairs(values: Any) -> List[Tuple[str, Any]]:
"""从 ContentValues / dict / 类似对象中提取 (列名, 值) 列表。
注意: 本实现中 ContentValues.valueSet() 返回的是键集合而非 (键, 值) 对,
因此需要通过 get() 取值。
"""
if values is None:
return []
if isinstance(values, dict):
return list(values.items())
# ContentValues 内部使用 _values dict
inner = getattr(values, "_values", None)
if isinstance(inner, dict):
return list(inner.items())
# ContentValues.valueSet() 在本实现中返回键集合
if hasattr(values, "valueSet") and hasattr(values, "get"):
try:
return [(str(k), values.get(k)) for k in values.valueSet()]
except Exception:
pass
if hasattr(values, "keySet") and hasattr(values, "get"):
try:
return [(str(k), values.get(k)) for k in values.keySet()]
except Exception:
pass
if hasattr(values, "items"):
try:
return [(str(k), v) for k, v in values.items()]
except Exception:
pass
return []
class SQLiteDatabase(SQLiteClosable):
"""SQLiteDatabase — 数据库操作类"""
@@ -536,10 +612,10 @@ class SQLiteDatabase(SQLiteClosable):
try:
db._conn = _sqlite3.connect(path)
db._conn.execute("PRAGMA journal_mode=WAL")
conn_cursor = db._conn.execute("PRAGMA schema_version")
conn_cursor = db._conn.execute("PRAGMA user_version")
row = conn_cursor.fetchone()
if row:
db._version = row[0]
db._version = int(row[0])
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e))
SQLiteDatabase._db_registry[path] = db
@@ -610,23 +686,37 @@ class SQLiteDatabase(SQLiteClosable):
self._ensure_open()
if values is None:
return -1
cols = []
vals = []
if hasattr(values, "valueSet"):
items = list(values.valueSet())
elif hasattr(values, "key_set"):
items = [(k, getattr(values, f"get{k[0].upper()}{k[1:]}", lambda: None)())
for k in values.key_set()]
elif isinstance(values, dict):
items = list(values.items())
else:
return -1
for k, v in items:
cols.append(str(k))
vals.append(v)
if len(cols) == 0:
items = _extract_value_pairs(values)
if not items:
cols = ["_id"]
vals = [None]
vals: List[Any] = [None]
else:
cols = [str(k) for k, _ in items]
vals = [v for _, v in items]
col_str = ", ".join(cols)
placeholders = ", ".join(["?"] * len(vals))
sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})"
try:
conn = self._acquire_connection()
cursor = conn.execute(sql, vals)
if not self._in_transaction:
conn.commit()
return cursor.lastrowid
except _sqlite3.DatabaseError:
return -1
def insertOrThrow(self, table: str, null_column_hack: Optional[str],
values: Any) -> int:
self._ensure_open()
if values is None:
raise SQLiteException("null values not allowed")
items = _extract_value_pairs(values)
if not items:
cols = ["_id"]
vals: List[Any] = [None]
else:
cols = [str(k) for k, _ in items]
vals = [v for _, v in items]
col_str = ", ".join(cols)
placeholders = ", ".join(["?"] * len(vals))
sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})"
@@ -639,23 +729,41 @@ class SQLiteDatabase(SQLiteClosable):
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), sql)
def replace(self, table: str, null_column_hack: Optional[str],
values: Any) -> int:
self._ensure_open()
if values is None:
return -1
items = _extract_value_pairs(values)
if not items:
cols = ["_id"]
vals: List[Any] = [None]
else:
cols = [str(k) for k, _ in items]
vals = [v for _, v in items]
col_str = ", ".join(cols)
placeholders = ", ".join(["?"] * len(vals))
sql = f"INSERT OR REPLACE INTO {table} ({col_str}) VALUES ({placeholders})"
try:
conn = self._acquire_connection()
cursor = conn.execute(sql, vals)
if not self._in_transaction:
conn.commit()
return cursor.lastrowid
except _sqlite3.DatabaseError:
return -1
def update(self, table: str, values: Any,
where_clause: Optional[str] = None,
where_args: Optional[List[Any]] = None) -> int:
self._ensure_open()
if values is None:
return 0
set_pairs = []
vals = []
if isinstance(values, dict):
items = list(values.items())
elif hasattr(values, "valueSet"):
items = list(values.valueSet())
else:
items = _extract_value_pairs(values)
if not items:
return 0
for k, v in items:
set_pairs.append(f"{k} = ?")
vals.append(v)
set_pairs = [f"{k} = ?" for k, _ in items]
vals: List[Any] = [v for _, v in items]
set_str = ", ".join(set_pairs)
sql = f"UPDATE {table} SET {set_str}"
if where_clause:
@@ -748,13 +856,24 @@ class SQLiteDatabase(SQLiteClosable):
return self._path
def getVersion(self) -> int:
if self._conn:
try:
cursor = self._conn.execute("PRAGMA user_version")
row = cursor.fetchone()
if row:
self._version = int(row[0])
return self._version
except _sqlite3.DatabaseError:
pass
return self._version
def setVersion(self, version: int):
self._version = version
self._version = int(version)
if self._conn:
try:
self._conn.execute(f"PRAGMA schema_version = {version}")
self._conn.execute(f"PRAGMA user_version = {int(version)}")
if not self._in_transaction:
self._conn.commit()
except _sqlite3.DatabaseError:
pass
@@ -784,20 +903,54 @@ class SQLiteDatabase(SQLiteClosable):
return self._enable_wal
def getMaximumSize(self) -> int:
if self._conn:
try:
page_size = self.getPageSize()
if page_size <= 0:
page_size = 4096
cursor = self._conn.execute("PRAGMA max_page_count")
row = cursor.fetchone()
if row:
self._max_size = int(row[0]) * page_size
return self._max_size
except _sqlite3.DatabaseError:
pass
return self._max_size
def getPageSize(self) -> int:
if self._conn:
try:
cursor = self._conn.execute("PRAGMA page_size")
row = cursor.fetchone()
if row:
self._page_size = int(row[0])
return self._page_size
except _sqlite3.DatabaseError:
pass
return self._page_size
def setMaximumSize(self, size: int) -> int:
self._max_size = size
return size
def setPageSize(self, size: int):
self._page_size = size
self._max_size = int(size)
if self._conn:
try:
self._conn.execute(f"PRAGMA page_size = {size}")
page_size = self.getPageSize()
if page_size <= 0:
page_size = 4096
max_pages = max(1, int(size) // page_size)
cursor = self._conn.execute(f"PRAGMA max_page_count = {max_pages}")
row = cursor.fetchone()
if row:
self._max_size = int(row[0]) * page_size
return self._max_size
except _sqlite3.DatabaseError:
pass
return self._max_size
def setPageSize(self, size: int):
self._page_size = int(size)
if self._conn:
try:
self._conn.execute(f"PRAGMA page_size = {int(size)}")
except _sqlite3.DatabaseError:
pass
+661 -40
View File
@@ -8,6 +8,7 @@ import math
import struct
import io
import os
import time
from enum import IntEnum
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
@@ -732,6 +733,42 @@ class Path(Stub):
pass
def op(self, other: "Path", op: int) -> bool:
"""对两个路径执行布尔运算。基于包围盒的近似实现。"""
if other is None:
return False
self_bounds = self.computeBounds()
other_bounds = other.computeBounds()
if op == Path.Op.UNION:
# 合并:将 other 的段追加到 self
self.addPath(other)
return True
if op == Path.Op.INTERSECT:
# 交集:仅当包围盒相交时保留相交区域
if not RectF.intersects(self_bounds, other_bounds):
self.reset()
return False
# 用相交包围盒作为近似结果
inter = RectF(self_bounds.left, self_bounds.top,
self_bounds.right, self_bounds.bottom)
inter.intersect(other_bounds.left, other_bounds.top,
other_bounds.right, other_bounds.bottom)
self.reset()
self.addRect(inter.left, inter.top, inter.right, inter.bottom)
return True
if op == Path.Op.DIFFERENCE:
# 差集:若包围盒不相交则 self 不变,否则标记(近似)
if not RectF.intersects(self_bounds, other_bounds):
return True
# 近似:无法精确计算,保留 self 不变
return True
if op == Path.Op.REVERSE_DIFFERENCE:
# 反向差集:用 other 替换 self
self.set(other)
return True
if op == Path.Op.XOR:
# 异或:合并两者
self.addPath(other)
return True
return False
def isRect(self) -> bool:
@@ -770,38 +807,219 @@ class PathMeasure(Stub):
super().__init__()
self._path = path
self._force_closed = forceClosed
self._contours: List[List[Tuple[float, float, float]]] = []
self._contour_lengths: List[float] = []
self._total_length: float = 0.0
if path is not None:
self._flatten()
def _flatten(self):
"""将路径展平为线段列表,计算累积长度。"""
self._contours = []
self._contour_lengths = []
self._total_length = 0.0
if not self._path:
return
segs = self._path._segments
cur_x = 0.0
cur_y = 0.0
start_x = 0.0
start_y = 0.0
contour: List[Tuple[float, float, float]] = []
contour_len = 0.0
for seg in segs:
t = seg.get("type")
if t == "move":
if contour:
self._contours.append(contour)
self._contour_lengths.append(contour_len)
self._total_length += contour_len
cur_x = seg.get("x", 0.0)
cur_y = seg.get("y", 0.0)
start_x = cur_x
start_y = cur_y
contour = [(cur_x, cur_y, 0.0)]
contour_len = 0.0
elif t == "line":
nx = seg.get("x", cur_x)
ny = seg.get("y", cur_y)
d = math.hypot(nx - cur_x, ny - cur_y)
contour_len += d
contour.append((nx, ny, contour_len))
cur_x = nx
cur_y = ny
elif t == "quad":
x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y)
x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y)
steps = 16
prev_x, prev_y = cur_x, cur_y
for i in range(1, steps + 1):
tt = i / steps
mt = 1 - tt
nx = mt * mt * cur_x + 2 * mt * tt * x1 + tt * tt * x2
ny = mt * mt * cur_y + 2 * mt * tt * y1 + tt * tt * y2
d = math.hypot(nx - prev_x, ny - prev_y)
contour_len += d
contour.append((nx, ny, contour_len))
prev_x, prev_y = nx, ny
cur_x = x2
cur_y = y2
elif t == "cubic":
x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y)
x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y)
x3, y3 = seg.get("x3", cur_x), seg.get("y3", cur_y)
steps = 24
prev_x, prev_y = cur_x, cur_y
for i in range(1, steps + 1):
tt = i / steps
mt = 1 - tt
nx = mt ** 3 * cur_x + 3 * mt ** 2 * tt * x1 + 3 * mt * tt ** 2 * x2 + tt ** 3 * x3
ny = mt ** 3 * cur_y + 3 * mt ** 2 * tt * y1 + 3 * mt * tt ** 2 * y2 + tt ** 3 * y3
d = math.hypot(nx - prev_x, ny - prev_y)
contour_len += d
contour.append((nx, ny, contour_len))
prev_x, prev_y = nx, ny
cur_x = x3
cur_y = y3
elif t == "close":
if contour:
d = math.hypot(start_x - cur_x, start_y - cur_y)
if d > 1e-10:
contour_len += d
contour.append((start_x, start_y, contour_len))
cur_x = start_x
cur_y = start_y
if contour:
if self._force_closed and segs and segs[-1].get("type") != "close":
d = math.hypot(start_x - cur_x, start_y - cur_y)
if d > 1e-10:
contour_len += d
contour.append((start_x, start_y, contour_len))
self._contours.append(contour)
self._contour_lengths.append(contour_len)
self._total_length += contour_len
def setPath(self, path: Path, forceClosed: bool):
self._path = path
self._force_closed = forceClosed
self._contours = []
self._contour_lengths = []
self._total_length = 0.0
if path is not None:
self._flatten()
def getLength(self) -> float:
if not self._path:
return 0.0
bounds = self._path.computeBounds()
return math.sqrt(bounds.width() ** 2 + bounds.height() ** 2)
"""返回当前轮廓的长度。"""
if self._contour_lengths:
return self._contour_lengths[0]
return 0.0
def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool:
if not self._path:
"""获取指定距离处的坐标和切线方向。"""
if not self._contours:
return False
contour = self._contours[0]
length = self._contour_lengths[0]
if length <= 0:
return False
# 钳制距离
d = max(0.0, min(distance, length))
# 二分查找
lo, hi = 0, len(contour) - 1
while lo < hi:
mid = (lo + hi) // 2
if contour[mid][2] < d:
lo = mid + 1
else:
hi = mid
idx = max(1, lo)
px, py, pd = contour[idx - 1]
nx, ny, nd = contour[idx]
seg_len = nd - pd
if seg_len < 1e-10:
t = 0.0
else:
t = (d - pd) / seg_len
x = px + (nx - px) * t
y = py + (ny - py) * t
dx = nx - px
dy = ny - py
dl = math.hypot(dx, dy)
if dl < 1e-10:
tx, ty = 1.0, 0.0
else:
tx, ty = dx / dl, dy / dl
if len(pos) >= 2:
pos[0] = 0.0
pos[1] = 0.0
pos[0] = x
pos[1] = y
if len(tan) >= 2:
tan[0] = 1.0
tan[1] = 0.0
tan[0] = tx
tan[1] = ty
return True
def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool:
return False
"""获取指定距离处的变换矩阵(平移+旋转)。"""
pos = [0.0, 0.0]
tan = [0.0, 0.0]
if not self.getPosTan(distance, pos, tan):
return False
angle = math.degrees(math.atan2(tan[1], tan[0]))
matrix.reset()
matrix.postRotate(angle)
matrix.postTranslate(pos[0], pos[1])
return True
def getSegment(self, startD: float, stopD: float, dst: Path, startWithMoveTo: bool) -> bool:
return False
"""提取路径中 [startD, stopD] 区间的子路径。"""
if not self._contours:
return False
contour = self._contours[0]
length = self._contour_lengths[0]
if length <= 0:
return False
startD = max(0.0, min(startD, length))
stopD = max(0.0, min(stopD, length))
if startD >= stopD:
return False
first = True
for i in range(1, len(contour)):
px, py, pd = contour[i - 1]
nx, ny, nd = contour[i]
if nd < startD:
continue
if pd > stopD:
break
seg_len = nd - pd
if seg_len < 1e-10:
continue
# 计算线段两端在 [startD, stopD] 内的参数
t0 = max(0.0, (startD - pd) / seg_len)
t1 = min(1.0, (stopD - pd) / seg_len)
x0 = px + (nx - px) * t0
y0 = py + (ny - py) * t0
x1 = px + (nx - px) * t1
y1 = py + (ny - py) * t1
if first and startWithMoveTo:
dst.moveTo(x0, y0)
else:
dst.lineTo(x0, y0)
dst.lineTo(x1, y1)
first = False
return True
def isClosed(self) -> bool:
return self._force_closed
if not self._path or not self._path._segments:
return self._force_closed
if self._force_closed:
return True
return any(s.get("type") == "close" for s in self._path._segments)
def nextContour(self) -> bool:
"""移动到下一个轮廓。"""
if len(self._contours) > 1:
self._contours.pop(0)
self._contour_lengths.pop(0)
return True
return False
@@ -1556,35 +1774,99 @@ class Camera(Stub):
self._location_x = 0.0
self._location_y = 0.0
self._location_z = -8.0
self._rot_x = 0.0
self._rot_y = 0.0
self._rot_z = 0.0
self._trans_x = 0.0
self._trans_y = 0.0
self._trans_z = 0.0
self._stack: List[Tuple[float, float, float, float, float, float, float, float, float]] = []
def save(self):
pass
"""保存当前变换状态到栈。"""
self._stack.append((self._rot_x, self._rot_y, self._rot_z,
self._trans_x, self._trans_y, self._trans_z,
self._location_x, self._location_y, self._location_z))
def restore(self):
pass
"""从栈恢复上次保存的变换状态。"""
if self._stack:
(self._rot_x, self._rot_y, self._rot_z,
self._trans_x, self._trans_y, self._trans_z,
self._location_x, self._location_y, self._location_z) = self._stack.pop()
def translate(self, x: float, y: float, z: float):
self._location_x += x
self._location_y += y
self._location_z += z
self._trans_x += x
self._trans_y += y
self._trans_z += z
def rotateX(self, deg: float):
pass
"""绕 X 轴旋转(累积角度)。"""
self._rot_x += deg
def rotateY(self, deg: float):
pass
"""绕 Y 轴旋转(累积角度)。"""
self._rot_y += deg
def rotateZ(self, deg: float):
pass
"""绕 Z 轴旋转(累积角度)。"""
self._rot_z += deg
def rotate(self, x: float, y: float, z: float):
pass
"""同时绕 X/Y/Z 轴旋转。"""
self._rot_x += x
self._rot_y += y
self._rot_z += z
def _compute_matrix(self) -> List[float]:
"""计算 4x4 变换矩阵并投影为 3x3 仿射矩阵。"""
rx = math.radians(self._rot_x)
ry = math.radians(self._rot_y)
rz = math.radians(self._rot_z)
cx, sx = math.cos(rx), math.sin(rx)
cy, sy = math.cos(ry), math.sin(ry)
cz, sz = math.cos(rz), math.sin(rz)
# 旋转矩阵 R = Rz * Ry * Rx
r00 = cz * cy
r01 = cz * sy * sx - sz * cx
r02 = cz * sy * cx + sz * sx
r10 = sz * cy
r11 = sz * sy * sx + cz * cx
r12 = sz * sy * cx - cz * sx
r20 = -sy
r21 = cy * sx
r22 = cy * cx
# 应用平移
tx = self._trans_x
ty = self._trans_y
tz = self._trans_z
# 相机位置(z 越负越远)
cam_z = self._location_z
# 透视投影:z' = z + cam_z,当 cam_z != 0 时做透视除法
# 简化为:将 3D 点投影到 2D
# 点 (x,y,z) -> (x*r00+y*r01+z*r02+tx, x*r10+y*r11+z*r12+ty, x*r20+y*r21+z*r22+tz)
# 透视:screen_x = x' * d / (d - z'), screen_y = y' * d / (d - z')
d = -cam_z # 相机到原点距离
if abs(d) < 1e-10:
# 正交投影
return [r00, r01, tx, r10, r11, ty, 0.0, 0.0, 1.0]
# 透视投影矩阵(近似):将 z 影响缩放到 x,y
# m[0]=r00, m[1]=r01, m[2]=tx, m[3]=r10, m[4]=r11, m[5]=ty
# 透视项:m[6]=r20/d, m[7]=r21/d, m[8]=1 - tz/d
return [r00, r01, tx,
r10, r11, ty,
r20 / d, r21 / d, 1.0 - tz / d]
def getMatrix(self, matrix: Matrix):
matrix.reset()
"""计算变换矩阵并写入给定的 Matrix。"""
matrix._values = self._compute_matrix()
def applyToCanvas(self, canvas: "Canvas"):
pass
"""将相机变换应用到 Canvas。"""
m = Matrix()
self.getMatrix(m)
if hasattr(canvas, "concat"):
canvas.concat(m)
def setLocation(self, x: float, y: float, z: float):
self._location_x = x
@@ -1592,7 +1874,17 @@ class Camera(Stub):
self._location_z = z
def dotWithNormal(self, dx: float, dy: float, dz: float) -> float:
return 0.0
"""计算向量与变换后 Z 轴法线的点积。"""
rx = math.radians(self._rot_x)
ry = math.radians(self._rot_y)
rz = math.radians(self._rot_z)
cy, sy = math.cos(ry), math.sin(ry)
cx, sx = math.cos(rx), math.sin(rx)
# 变换后的 Z 轴法线
nz_x = sy
nz_y = -cy * sx
nz_z = cy * cx
return dx * nz_x + dy * nz_y + dz * nz_z
# ═══════════════════════════════════════════════════════════════
@@ -1660,8 +1952,15 @@ class Picture(Stub):
self._canvas = None
def draw(self, canvas: "Canvas"):
"""回放录制的绘制命令到指定 Canvas。"""
for cmd in self._recorded:
pass
method = cmd.get("method")
args = cmd.get("args", [])
if method and hasattr(canvas, method):
try:
getattr(canvas, method)(*args)
except Exception:
pass
def getWidth(self) -> int:
return self._canvas.getWidth() if hasattr(self, "_canvas") and self._canvas else 0
@@ -1690,7 +1989,9 @@ class PictureDrawable(Stub):
self._picture = picture
def draw(self, canvas: "Canvas"):
pass
"""将 Picture 回放到指定 Canvas。"""
if self._picture is not None:
self._picture.draw(canvas)
# ═══════════════════════════════════════════════════════════════
@@ -1704,6 +2005,10 @@ class SurfaceTexture(Stub):
super().__init__()
self._tex_name = texName
self._timestamp = 0
self._buffer_width = 0
self._buffer_height = 0
self._attached = texName != 0
self._released = False
def getTimestamp(self) -> int:
return self._timestamp
@@ -1712,16 +2017,24 @@ class SurfaceTexture(Stub):
self._timestamp = int(time.time() * 1e9)
def release(self):
pass
"""释放 SurfaceTexture 资源。"""
self._released = True
self._attached = False
def setDefaultBufferSize(self, width: int, height: int):
pass
"""设置默认缓冲区大小。"""
self._buffer_width = width
self._buffer_height = height
def attachToGLContext(self, texName: int):
pass
"""将 SurfaceTexture 附加到 GL 纹理。"""
self._tex_name = texName
self._attached = True
self._released = False
def detachFromGLContext(self):
pass
"""从 GL 纹理分离 SurfaceTexture。"""
self._attached = False
# ═══════════════════════════════════════════════════════════════
@@ -1768,6 +2081,59 @@ class Region(Stub):
return self._rect.contains(x, y)
def op(self, *args) -> bool:
"""对区域执行布尔运算。支持 op(rect, op) / op(region, op) / op(left,top,right,bottom,op)。"""
if len(args) == 2:
operand, op_code = args
if isinstance(operand, Rect):
other = Rect(operand.left, operand.top, operand.right, operand.bottom)
elif isinstance(operand, Region):
other = Rect(operand._rect.left, operand._rect.top,
operand._rect.right, operand._rect.bottom)
else:
return False
elif len(args) == 5:
left, top, right, bottom, op_code = args
other = Rect(left, top, right, bottom)
else:
return False
self_r = self._rect
if op_code == Region.OP_INTERSECT:
nL = max(self_r.left, other.left)
nT = max(self_r.top, other.top)
nR = min(self_r.right, other.right)
nB = min(self_r.bottom, other.bottom)
if nL < nR and nT < nB:
self_r.set(nL, nT, nR, nB)
return True
self_r.setEmpty()
return False
if op_code == Region.OP_UNION:
if self_r.isEmpty():
self_r.set(other.left, other.top, other.right, other.bottom)
else:
self_r.left = min(self_r.left, other.left)
self_r.top = min(self_r.top, other.top)
self_r.right = max(self_r.right, other.right)
self_r.bottom = max(self_r.bottom, other.bottom)
return True
if op_code == Region.OP_REPLACE:
self_r.set(other.left, other.top, other.right, other.bottom)
return True
if op_code == Region.OP_DIFFERENCE:
# 差集:若不相交则不变;若完全包含则清空;否则近似为不变
if not Rect.intersects(self_r, other):
return True
if other.contains(self_r.left, self_r.top) and other.contains(self_r.right - 1, self_r.bottom - 1):
self_r.setEmpty()
return False
return True
if op_code == Region.OP_REVERSE_DIFFERENCE:
self_r.set(other.left, other.top, other.right, other.bottom)
return True
if op_code == Region.OP_XOR:
if not Rect.intersects(self_r, other):
self_r.union(other.left, other.top, other.right, other.bottom)
return True
return False
def isRect(self) -> bool:
@@ -1894,10 +2260,32 @@ class Paint(Stub):
def setTextSize(self, textSize: float):
self._text_size = textSize
self._font_cache = None
def getTextSize(self) -> float:
return self._text_size
def _get_font(self):
"""获取 PIL ImageFont 对象(带缓存)。"""
if not HAS_PIL:
return None
cache_key = (self._text_size, id(self._typeface) if self._typeface else None)
if getattr(self, "_font_cache_key", None) == cache_key and getattr(self, "_font_cache", None):
return self._font_cache
try:
from PIL import ImageFont
font_path = None
if self._typeface and hasattr(self._typeface, "_path") and self._typeface._path:
font_path = self._typeface._path
if font_path and os.path.exists(font_path):
self._font_cache = ImageFont.truetype(font_path, int(self._text_size))
else:
self._font_cache = ImageFont.load_default()
self._font_cache_key = cache_key
return self._font_cache
except Exception:
return None
def setTypeface(self, typeface: Optional[Typeface]):
self._typeface = typeface
@@ -2004,13 +2392,31 @@ class Paint(Stub):
self._shadow_layer = None
def measureText(self, text: str) -> float:
"""测量文本宽度。优先使用 PIL ImageFont 获取精确宽度。"""
font = self._get_font()
if font is not None:
try:
if hasattr(font, "getlength"):
return float(font.getlength(text))
bbox = font.getbbox(text)
return float(bbox[2] - bbox[0])
except Exception:
pass
return len(text) * self._text_size * 0.5
def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int:
"""在最大宽度内截断文本,返回可容纳的字符数。"""
font = self._get_font()
width = 0.0
count = 0
for i, ch in enumerate(text):
cw = self._text_size * 0.5
if font is not None:
try:
cw = font.getlength(ch) if hasattr(font, "getlength") else (font.getbbox(ch)[2])
except Exception:
cw = self._text_size * 0.5
else:
cw = self._text_size * 0.5
if width + cw > maxWidth:
break
width += cw
@@ -2019,9 +2425,16 @@ class Paint(Stub):
return count
def getTextWidths(self, text: str, widths: List[float]) -> int:
cw = self._text_size * 0.5
"""获取每个字符的宽度。"""
font = self._get_font()
for i, ch in enumerate(text):
widths[i] = cw
if font is not None:
try:
widths[i] = float(font.getlength(ch)) if hasattr(font, "getlength") else float(font.getbbox(ch)[2])
except Exception:
widths[i] = self._text_size * 0.5
else:
widths[i] = self._text_size * 0.5
return len(text)
def getTextPath(self, text: str, start: int, end: int, x: float, y: float, path: Path):
@@ -2030,7 +2443,22 @@ class Paint(Stub):
path.lineTo(x + (i - start + 1) * self._text_size * 0.3, y)
def getFontMetrics(self) -> Dict[str, float]:
"""返回字体度量信息。优先使用 PIL ImageFont 获取真实度量。"""
size = self._text_size
font = self._get_font()
if font is not None:
try:
if hasattr(font, "getmetrics"):
ascent, descent = font.getmetrics()
return {
"ascent": -float(ascent),
"descent": float(descent),
"top": -float(ascent) - 2.0,
"bottom": float(descent) + 2.0,
"leading": 0.0,
}
except Exception:
pass
return {
"ascent": -size * 0.8,
"descent": size * 0.2,
@@ -2049,6 +2477,82 @@ class Paint(Stub):
def descent(self) -> float:
return self._text_size * 0.2
def setStrokeCap(self, cap):
self._stroke_cap = cap
def getStrokeCap(self):
return getattr(self, '_stroke_cap', 0)
def setStrokeJoin(self, join):
self._stroke_join = join
def getStrokeJoin(self):
return getattr(self, '_stroke_join', 0)
def setStrokeMiter(self, miter):
self._stroke_miter = miter
def getStrokeMiter(self):
return getattr(self, '_stroke_miter', 4.0)
def getTextBounds(self, text, start, end, bounds):
"""返回文本边界矩形。优先使用 PIL ImageFont 获取真实边界。"""
font = self._get_font()
if font is not None:
try:
sub = text[start:end]
bbox = font.getbbox(sub)
bounds.left = int(bbox[0])
bounds.top = int(bbox[1])
bounds.right = int(bbox[2])
bounds.bottom = int(bbox[3])
return
except Exception:
pass
if hasattr(self, '_text_size'):
w = len(text[start:end]) * self._text_size * 0.5
h = self._text_size
else:
w = len(text[start:end]) * 12
h = 24
bounds.left = 0
bounds.top = -int(h * 0.8)
bounds.right = int(w)
bounds.bottom = int(h * 0.2)
def setBlendMode(self, blendmode):
self._blend_mode = blendmode
def getBlendMode(self):
return getattr(self, '_blend_mode', None)
def setLetterSpacing(self, letterSpacing):
self._letter_spacing = letterSpacing
def getLetterSpacing(self):
return getattr(self, '_letter_spacing', 0.0)
def setFontFeatureSettings(self, settings):
self._font_feature_settings = settings
def getFontFeatureSettings(self):
return getattr(self, '_font_feature_settings', None)
def setHinting(self, mode):
self._hinting = mode
def getHinting(self):
return getattr(self, '_hinting', 2) # HINTING_ON
def setFakeBoldText(self, fakeBoldText):
self._fake_bold_text = fakeBoldText
def isFakeBoldText(self):
return getattr(self, '_fake_bold_text', False)
def hasGlyph(self, string):
return len(string) > 0
# ═══════════════════════════════════════════════════════════════
# Bitmap
@@ -2249,13 +2753,30 @@ class Bitmap(Stub):
return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8)
def sameAs(self, other: "Bitmap") -> bool:
"""比较两个 Bitmap 的像素是否完全相同。"""
if not isinstance(other, Bitmap):
return False
if self._width != other._width or self._height != other._height:
return False
if self._config != other._config:
return False
if self._recycled or other._recycled:
return False
# 使用 PIL 比较像素
if HAS_PIL and hasattr(self, "_image") and hasattr(other, "_image"):
try:
from PIL import ImageChops
diff = ImageChops.difference(self._image, other._image)
return diff.getbbox() is None
except Exception:
return list(self._image.getdata()) == list(other._image.getdata())
# 回退到字节比较
if hasattr(self, "_pixels") and hasattr(other, "_pixels"):
return bytes(self._pixels) == bytes(other._pixels)
return True
def prepareToDraw(self):
"""提示将位图数据上传到 GPU 缓存。桌面端为空操作。"""
pass
def hasAlpha(self) -> bool:
@@ -2344,6 +2865,29 @@ class BitmapFactory(Stub):
@staticmethod
def decodeResource(res, resId: int, opts: Options = None) -> Optional[Bitmap]:
"""从资源解码位图。尝试通过 Resources 打开原始资源流。"""
# 尝试通过 Resources 获取资源路径或流
if res is not None:
try:
# 尝试 openRawResource
stream = res.openRawResource(resId)
if stream is not None:
return BitmapFactory.decodeStream(stream, None, opts)
except Exception:
pass
try:
# 尝试通过资源名查找文件
name = res.getResourceEntryName(resId) if hasattr(res, "getResourceEntryName") else None
if name and name != "unknown":
for ext in (".png", ".jpg", ".jpeg", ".webp", ".bmp"):
candidate = resolve_android_path("res/drawable/" + name + ext)
if os.path.exists(candidate):
return BitmapFactory.decodeFile(candidate, opts)
candidate = resolve_android_path("res/drawable-xxhdpi/" + name + ext)
if os.path.exists(candidate):
return BitmapFactory.decodeFile(candidate, opts)
except Exception:
pass
return Bitmap(100, 100, Bitmap.Config.ARGB_8888)
@staticmethod
@@ -2547,7 +3091,22 @@ class Canvas(Stub):
return Rect(r.left, r.top, r.right, r.bottom)
def quickReject(self, *args) -> bool:
return False
"""判断给定矩形是否完全在裁剪区域之外。"""
clip = self._clip_stack[-1]
if len(args) == 1:
r = args[0]
if isinstance(r, Rect):
left, top, right, bottom = r.left, r.top, r.right, r.bottom
elif isinstance(r, RectF):
left, top, right, bottom = r.left, r.top, r.right, r.bottom
else:
return False
elif len(args) == 4:
left, top, right, bottom = args
else:
return False
return (right <= clip.left or left >= clip.right or
bottom <= clip.top or top >= clip.bottom)
def drawColor(self, color: int):
if HAS_PIL and self._draw is not None:
@@ -2747,7 +3306,37 @@ class Canvas(Stub):
self._draw.text((x, y), text, fill=fill_color)
def drawTextOnPath(self, text: str, path: Path, hOffset: float, vOffset: float, paint: Paint):
pass
"""沿路径绘制文本。使用 PathMeasure 逐字符放置。"""
if not text or not HAS_PIL or self._draw is None:
return
pm = PathMeasure(path, False)
total = pm.getLength()
if total <= 0:
return
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
dist = hOffset
pos = [0.0, 0.0]
tan = [0.0, 0.0]
for ch in text:
cw = paint.measureText(ch)
if dist > total:
break
if pm.getPosTan(dist, pos, tan):
angle = math.degrees(math.atan2(tan[1], tan[0]))
# 使用 PIL 旋转绘制每个字符
try:
from PIL import Image as _PILImage
char_img = _PILImage.new("RGBA", (int(cw) + 4, int(paint._text_size) + 4), (0, 0, 0, 0))
from PIL import ImageDraw as _PILDraw
cd = _PILDraw.Draw(char_img)
cd.text((0, 0), ch, fill=fill_color)
rotated = char_img.rotate(-angle, expand=True, resample=_PILImage.BICUBIC)
self._image.paste(rotated, (int(pos[0] + vOffset * tan[1]), int(pos[1] - vOffset * tan[0])),
rotated)
except Exception:
self._draw.text((pos[0], pos[1]), ch, fill=fill_color)
dist += cw
def drawPosText(self, text: str, pos: List[float], paint: Paint):
if HAS_PIL and self._draw is not None:
@@ -2758,14 +3347,44 @@ class Canvas(Stub):
self._draw.text((pos[i * 2], pos[i * 2 + 1]), ch, fill=fill_color)
def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None):
pass
"""回放 Picture 中录制的绘制命令。"""
if picture is None:
return
recorded = getattr(picture, "_recorded", [])
if not recorded:
return
for cmd in recorded:
method = cmd.get("method")
args = cmd.get("args", [])
if method and hasattr(self, method):
try:
getattr(self, method)(*args)
except Exception:
pass
def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None):
pass
"""绘制 NinePatch(拉伸位图)到目标矩形。"""
if patch is None or patch._bitmap is None:
return
bitmap = patch._bitmap
if dst is None:
self.drawBitmap(bitmap, 0, 0, paint)
return
# 简单缩放绘制
if isinstance(dst, (Rect, RectF)):
dw = dst.right - dst.left
dh = dst.bottom - dst.top
else:
dw, dh = dst[2] - dst[0], dst[3] - dst[1]
scaled = Bitmap.createScaledBitmap(bitmap, int(dw), int(dh), True)
self.drawBitmap(scaled, dst.left if hasattr(dst, "left") else dst[0],
dst.top if hasattr(dst, "top") else dst[1], paint)
def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float,
inner: RectF, innerRx: float, innerRy: float, paint: Paint):
pass
"""绘制双层圆角矩形(外层和内层)。"""
self.drawRoundRect(outer, outerRx, outerRy, paint)
self.drawRoundRect(inner, innerRx, innerRy, paint)
def drawBitmapMesh(self, bitmap: Bitmap, meshWidth: int, meshHeight: int,
verts: List[float], vertOffset: int, colors: List[int],
@@ -2785,10 +3404,12 @@ class Canvas(Stub):
return False
def setDensity(self, density: int):
pass
"""设置画布密度(DPI)。"""
self._density = density
def getDensity(self) -> int:
return 0
"""返回画布密度(DPI),默认 0 表示未知。"""
return getattr(self, "_density", 0)
def toString(self) -> str:
return f"Canvas({self._width}x{self._height})"
+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
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxBluetooth, LinuxSensors, HAS_BLUETOOTHCTL, LinuxPower
_USE_LINUX = IS_LINUX
except ImportError:
try:
from .._linux_impl import IS_LINUX, LinuxBluetooth, LinuxSensors, HAS_BLUETOOTHCTL
_USE_LINUX = IS_LINUX
LinuxPower = None
except ImportError:
_USE_LINUX = False
LinuxPower = None
# ═══════════════════════════════════════════════════════════════
# Sensor
@@ -251,14 +264,89 @@ class SensorManager(Stub):
self._sensors[type_] = Sensor(type_)
def getDefaultSensor(self, type_: int) -> Optional[Sensor]:
if _USE_LINUX:
sensor = self._build_linux_sensor(type_)
if sensor is not None:
return sensor
return self._sensors.get(type_)
def _build_linux_sensor(self, type_: int) -> Optional[Sensor]:
"""根据 Linux 真实传感器数据构造 Sensor 对象"""
try:
if type_ == Sensor.TYPE_AMBIENT_TEMPERATURE:
temps = LinuxSensors.get_temperatures()
if temps:
t = temps[0]
return Sensor(Sensor.TYPE_AMBIENT_TEMPERATURE,
name=t.get("name", "Linux Temperature"),
vendor="Linux", resolution=0.01,
max_range=125.0, power=0.0, min_delay=1000)
return None
if type_ == Sensor.TYPE_LIGHT:
light = LinuxSensors.get_light_level()
if light is not None:
return Sensor(Sensor.TYPE_LIGHT, name="Linux Light Sensor",
vendor="Linux", resolution=0.001,
max_range=40000.0, power=0.0, min_delay=1000)
return None
except Exception:
return None
return None
def getSensorList(self, type_: int) -> List[Sensor]:
if _USE_LINUX:
return self._get_linux_sensor_list(type_)
if type_ == -1: # ALL
return list(self._sensors.values())
sensor = self._sensors.get(type_)
return [sensor] if sensor else []
def _get_linux_sensor_list(self, type_: int) -> List[Sensor]:
"""从 Linux 真实传感器数据生成传感器列表"""
sensors: List[Sensor] = []
try:
if type_ == -1 or type_ == Sensor.TYPE_AMBIENT_TEMPERATURE:
for t in LinuxSensors.get_temperatures():
sensors.append(Sensor(
Sensor.TYPE_AMBIENT_TEMPERATURE,
name=t.get("name", "Linux Temperature"),
vendor="Linux", resolution=0.01,
max_range=125.0, power=0.0, min_delay=1000))
if type_ == -1 or type_ == Sensor.TYPE_LIGHT:
light = LinuxSensors.get_light_level()
if light is not None:
sensors.append(Sensor(
Sensor.TYPE_LIGHT, name="Linux Light Sensor",
vendor="Linux", resolution=0.001,
max_range=40000.0, power=0.0, min_delay=1000))
# CPU 频率作为附加传感器 (映射到压力传感器类型以承载真实数据)
if type_ == -1 or type_ == Sensor.TYPE_PRESSURE:
for f in LinuxSensors.get_cpu_frequency():
sensors.append(Sensor(
Sensor.TYPE_PRESSURE,
name=f"CPU{f.get('cpu', 0)} Frequency",
vendor="Linux", resolution=1.0,
max_range=10000.0, power=0.0, min_delay=1000))
# 风扇转速作为附加传感器 (映射到相对湿度类型以承载真实数据)
if type_ == -1 or type_ == Sensor.TYPE_RELATIVE_HUMIDITY:
for fan in LinuxSensors.get_fan_speeds():
sensors.append(Sensor(
Sensor.TYPE_RELATIVE_HUMIDITY,
name=f"Fan {fan.get('name', '')}",
vendor="Linux", resolution=1.0,
max_range=10000.0, power=0.0, min_delay=1000))
except Exception:
pass
# 若未取到真实数据则回退到模拟传感器
if not sensors:
if type_ == -1:
sensors = list(self._sensors.values())
else:
sensor = self._sensors.get(type_)
if sensor:
sensors = [sensor]
return sensors
def getSensors(self) -> List[Sensor]:
return list(self._sensors.values())
@@ -297,18 +385,56 @@ class SensorManager(Stub):
@staticmethod
def getRotationMatrix(R: List[float], I: List[float],
gravity: List[float], geomagnetic: List[float]) -> bool:
"""根据重力和地磁向量计算旋转矩阵 R 和倾斜矩阵 I。
算法对齐 Android SensorManager.getRotationMatrix。"""
if len(R) < 9:
R.extend([0] * (9 - len(R)))
if len(R) >= 9:
R[0] = 1.0
R[1] = 0.0
R[2] = 0.0
R[3] = 0.0
R[4] = 1.0
R[5] = 0.0
R[6] = 0.0
R[7] = 0.0
R[8] = 1.0
R.extend([0.0] * (9 - len(R)))
gx, gy, gz = gravity[0], gravity[1], gravity[2]
mx, my, mz = geomagnetic[0], geomagnetic[1], geomagnetic[2]
# 归一化重力向量 -> H
g_len = math.sqrt(gx * gx + gy * gy + gz * gz)
if g_len < 1e-10:
return False
hx = gx / g_len
hy = gy / g_len
hz = gz / g_len
# 叉积 A = H x M (东方向)
ax = hy * mz - hz * my
ay = hz * mx - hx * mz
az = hx * my - hy * mx
a_len = math.sqrt(ax * ax + ay * ay + az * az)
if a_len < 1e-10:
return False
ax /= a_len
ay /= a_len
az /= a_len
# 重新计算 M = A x H
ex = ay * hz - az * hy
ey = az * hx - ax * hz
ez = ax * hy - ay * hx
# 填充旋转矩阵 R (列优先,Android 格式)
R[0] = ax
R[1] = ay
R[2] = az
R[3] = ex
R[4] = ey
R[5] = ez
R[6] = hx
R[7] = hy
R[8] = hz
# 填充倾斜矩阵 I
if I is not None and len(I) >= 9:
inv_h = 1.0 / g_len
inv_m = 1.0 / math.sqrt(mx * mx + my * my + mz * mz) if (mx * mx + my * my + mz * mz) > 0 else 0
I[0] = inv_h * hx
I[1] = inv_h * hy
I[2] = inv_h * hz
I[3] = inv_m * mx
I[4] = inv_m * my
I[5] = inv_m * mz
I[6] = inv_h * ex
I[7] = inv_h * ey
I[8] = inv_h * ez
return True
@staticmethod
@@ -317,17 +443,69 @@ class SensorManager(Stub):
@staticmethod
def getInclination(I: List[float]) -> float:
"""根据倾斜矩阵 I 计算地磁倾角(弧度)。"""
if len(I) >= 9:
# I[5] = cos(el), I[2] = sin(el)
# 倾角 = atan2(I[2], I[5])
return math.atan2(I[2], I[5])
return 0.0
@staticmethod
def remapCoordinateSystem(inR: List[float], X: int, Y: int,
outR: List[float]) -> bool:
if len(inR) >= 9 and len(outR) >= 9:
for i in range(9):
outR[i] = inR[i]
"""根据指定的 X/Y 轴重映射坐标系。"""
if len(inR) < 9 or len(outR) < 9:
return False
# Android 轴常量
AXIS_X = 1
AXIS_Y = 2
AXIS_Z = 3
AXIS_MINUS_X = -1
AXIS_MINUS_Y = -2
AXIS_MINUS_Z = -3
# 将轴常量映射到列索引和符号
axis_map = {
AXIS_X: (0, 1), AXIS_Y: (1, 1), AXIS_Z: (2, 1),
AXIS_MINUS_X: (0, -1), AXIS_MINUS_Y: (1, -1), AXIS_MINUS_Z: (2, -1),
}
if X not in axis_map or Y not in axis_map:
return False
x_col, x_sign = axis_map[X]
y_col, y_sign = axis_map[Y]
# Z = X x Y
z_col = 3 - x_col - y_col
z_sign = x_sign * y_sign
# 如果 X 和 Y 不正交
if abs(x_col - y_col) != 1 and not (x_col == 0 and y_col == 2) and not (x_col == 2 and y_col == 0):
z_sign = -z_sign
# 填充输出矩阵(列优先)
for row in range(3):
outR[row * 3 + 0] = inR[row * 3 + x_col] * x_sign
outR[row * 3 + 1] = inR[row * 3 + y_col] * y_sign
outR[row * 3 + 2] = inR[row * 3 + z_col] * z_sign
return True
def getDeviceId(self) -> str:
"""返回传感器设备 ID。尝试从 /sys 读取真实设备标识。"""
import os as _os
# 尝试从 /sys/class/sensor 读取
try:
base = "/sys/class/sensor"
if _os.path.isdir(base):
names = _os.listdir(base)
if names:
return names[0]
except (OSError, IOError):
pass
# 尝试从 /sys/bus/iio/devices 读取
try:
base = "/sys/bus/iio/devices"
if _os.path.isdir(base):
names = _os.listdir(base)
if names:
return names[0]
except (OSError, IOError):
pass
return "sensor_hub_0"
@@ -470,6 +648,29 @@ class BatteryManager(Stub):
super().__init__()
def getIntProperty(self, id: int) -> int:
"""返回电池整型属性。优先使用 LinuxPower 读取真实数据。"""
if _USE_LINUX and LinuxPower is not None:
try:
info = LinuxPower.get_battery_info() or {}
if id == BatteryManager.BATTERY_PROPERTY_CAPACITY:
level = info.get("level", -1)
return int(level) if isinstance(level, (int, float)) and level >= 0 else -1
if id == BatteryManager.BATTERY_PROPERTY_STATUS:
status_map = {
"charging": BatteryManager.BATTERY_STATUS_CHARGING,
"discharging": BatteryManager.BATTERY_STATUS_DISCHARGING,
"not charging": BatteryManager.BATTERY_STATUS_NOT_CHARGING,
"full": BatteryManager.BATTERY_STATUS_FULL,
"unknown": BatteryManager.BATTERY_STATUS_UNKNOWN,
}
return status_map.get(str(info.get("status", "unknown")).lower(),
BatteryManager.BATTERY_STATUS_UNKNOWN)
if id == BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER:
voltage = info.get("voltage", 0)
return int(voltage * 1000) if isinstance(voltage, (int, float)) else 0
except Exception:
pass
# 回退到默认值
id_values = {
BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER: 3500,
BatteryManager.BATTERY_PROPERTY_CAPACITY: 85,
@@ -478,7 +679,19 @@ class BatteryManager(Stub):
return id_values.get(id, 0)
def getLongProperty(self, id: int) -> int:
return 0
"""返回电池长整型属性。优先使用 LinuxPower 读取真实数据。"""
if _USE_LINUX and LinuxPower is not None:
try:
info = LinuxPower.get_battery_info() or {}
if id == BatteryManager.BATTERY_PROPERTY_CURRENT_NOW:
current = info.get("current_now", 0)
return int(current) if isinstance(current, (int, float)) else 0
if id == BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER:
voltage = info.get("voltage", 0)
return int(voltage * 3600000) if isinstance(voltage, (int, float)) else 0
except Exception:
pass
return self.getIntProperty(id)
# ═══════════════════════════════════════════════════════════════
+18 -10
View File
@@ -13,6 +13,10 @@ import random
from typing import Any, Callable, Dict, List, Optional, Tuple
from .._core import Stub, AndroidOnlyError
from .hardware import SensorManager as _BaseSensorManager
from .hardware import Sensor as _BaseSensor
from .hardware import SensorEvent as _BaseSensorEvent
from .hardware import SensorEventListener as _BaseSensorEventListener
__all__ = [
"Sensor", "SensorEvent", "SensorEventListener",
@@ -101,8 +105,8 @@ AXIS_MINUS_Z = 6
# Sensor (扩展)
# ═══════════════════════════════════════════════════════════════
class Sensor(Stub):
"""Sensor — 传感器 (完整版)"""
class Sensor(_BaseSensor):
"""Sensor — 传感器 (完整版,继承 hardware.Sensor 的所有方法)"""
TYPE_ACCELEROMETER = TYPE_ACCELEROMETER
TYPE_MAGNETIC_FIELD = TYPE_MAGNETIC_FIELD
@@ -153,7 +157,8 @@ class Sensor(Stub):
fifo_reserved_event_count: int = 0,
fifo_max_event_count: int = 0,
id_: int = 0):
super().__init__()
super().__init__(type_, name, vendor, version, resolution,
max_range, power, min_delay)
self._type = type_
self._name = name
self._vendor = vendor
@@ -248,14 +253,17 @@ class Sensor(Stub):
# SensorEvent
# ═══════════════════════════════════════════════════════════════
class SensorEvent(Stub):
"""SensorEvent — 传感器事件"""
class SensorEvent(_BaseSensorEvent):
"""SensorEvent — 传感器事件 (继承 hardware.SensorEvent 的所有方法)"""
def __init__(self, sensor: Optional[Sensor] = None,
values: List[float] = None,
accuracy: int = SENSOR_STATUS_ACCURACY_HIGH,
timestamp: int = 0):
super().__init__()
if sensor is not None:
super().__init__(sensor, accuracy)
else:
Stub.__init__(self)
self.sensor = sensor
self.values = values or [0.0, 0.0, 0.0]
self.accuracy = accuracy
@@ -290,8 +298,8 @@ class SensorEvent(Stub):
# SensorEventListener
# ═══════════════════════════════════════════════════════════════
class SensorEventListener(Stub):
"""SensorEventListener — 传感器事件监听器"""
class SensorEventListener(_BaseSensorEventListener):
"""SensorEventListener — 传感器事件监听器 (继承 hardware.SensorEventListener 的所有方法)"""
def onSensorChanged(self, event: SensorEvent):
pass
@@ -313,8 +321,8 @@ class SensorEventListener(Stub):
# SensorManager (扩展)
# ═══════════════════════════════════════════════════════════════
class SensorManager(Stub):
"""SensorManager — 传感器管理器 (完整版)"""
class SensorManager(_BaseSensorManager):
"""SensorManager — 传感器管理器 (完整版,继承 hardware.SensorManager)"""
AXIS_X = AXIS_X
AXIS_Y = AXIS_Y
+224 -13
View File
@@ -16,6 +16,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
try:
from .._linux_impl import IS_LINUX, LinuxLocation
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Location
@@ -614,6 +620,9 @@ class GnssStatus(Stub):
return 0.0
def getBasebandCn0DbHz(self, index: int) -> float:
"""返回指定卫星的基带载噪比(dB-Hz)。"""
if 0 <= index < len(self._satellites):
return self._satellites[index].get("baseband_cn0", 0.0)
return 0.0
def getCn0DbHz(self, index: int) -> float:
@@ -867,6 +876,42 @@ class Address(Stub):
def setFeatureName(self, name: str):
self._feature_name = name
def setAdminArea(self, area: str):
self._admin_area = area
def setSubAdminArea(self, area: str):
self._sub_admin_area = area
def setLocality(self, locality: str):
self._locality = locality
def setSubLocality(self, sub_locality: str):
self._sub_locality = sub_locality
def setPremises(self, premises: str):
self._premises = premises
def setThoroughfare(self, thoroughfare: str):
self._thoroughfare = thoroughfare
def setSubThoroughfare(self, sub_thoroughfare: str):
self._sub_thoroughfare = sub_thoroughfare
def setPostalCode(self, postal_code: str):
self._postal_code = postal_code
def setCountryName(self, country_name: str):
self._country_name = country_name
def setCountryCode(self, country_code: str):
self._country_code = country_code
def setPhone(self, phone: str):
self._phone = phone
def setUrl(self, url: str):
self._url = url
def setLatitude(self, latitude: float):
self._latitude = latitude
self._has_lat_lon = True
@@ -892,29 +937,144 @@ class Address(Stub):
class Geocoder(Stub):
"""Geocoder — 地理编码/逆地理编码"""
# Nominatim (OpenStreetMap) 免费 API
_NOMINATIM_URL = "https://nominatim.openstreetmap.org"
_USER_AGENT = "Android-pyApi/1.0"
def __init__(self, context: Any = None, locale: Any = None):
super().__init__()
self._context = context
self._locale = locale
def _get_locale_str(self) -> str:
if self._locale is None:
return "en"
if hasattr(self._locale, "getLanguage"):
lang = self._locale.getLanguage()
country = self._locale.getCountry()
return f"{lang}-{country}" if country else lang
return str(self._locale)
@staticmethod
def _http_get_json(url: str) -> Optional[Any]:
"""使用 urllib 发送 GET 请求并解析 JSON。"""
try:
import urllib.request
import json
req = urllib.request.Request(url, headers={
"User-Agent": Geocoder._USER_AGENT,
"Accept-Language": "en",
})
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
except Exception:
return None
def getFromLocation(self, latitude: float, longitude: float,
max_results: int = 1) -> List[Address]:
"""逆地理编码:根据经纬度获取地址。优先使用 Nominatim API。"""
results: List[Address] = []
# 尝试在线逆地理编码
try:
url = (f"{self._NOMINATIM_URL}/reverse?"
f"lat={latitude}&lon={longitude}&format=jsonv2"
f"&addressdetails=1&limit={max_results}")
data = self._http_get_json(url)
if data and isinstance(data, dict) and "address" in data:
addr = self._parse_nominatim_reverse(data)
results.append(addr)
except Exception:
pass
# 回退到默认地址
if not results:
addr = Address()
addr.setLatitude(latitude)
addr.setLongitude(longitude)
addr.setFeatureName("Unknown Location")
addr.setLocality("Unknown")
addr.setCountryName("Unknown")
results.append(addr)
return results[:max_results] if max_results > 0 else []
def _parse_nominatim_reverse(self, data: dict) -> Address:
"""解析 Nominatim 逆地理编码响应。"""
addr = Address()
addr.setLatitude(latitude)
addr.setLongitude(longitude)
addr.setFeatureName("Unknown Location")
addr.setLocality("Unknown")
addr.setCountryName("Unknown")
return [addr] if max_results > 0 else []
address = data.get("address", {})
lat = data.get("lat")
lon = data.get("lon")
if lat is not None:
addr.setLatitude(float(lat))
if lon is not None:
addr.setLongitude(float(lon))
addr.setFeatureName(address.get("road") or address.get("building")
or data.get("name") or "Unknown")
addr.setLocality(address.get("city") or address.get("town")
or address.get("village") or address.get("county") or "Unknown")
addr.setAdminArea(address.get("state") or "")
addr.setSubAdminArea(address.get("state_district") or "")
addr.setCountryName(address.get("country") or "Unknown")
addr.setCountryCode(address.get("country_code", "").upper())
addr.setPostalCode(address.get("postcode") or "")
addr.setAddressLine(0, data.get("display_name", ""))
return addr
def getFromLocationName(self, location_name: str, max_results: int = 1,
lower_left_lat: Optional[float] = None,
lower_left_lon: Optional[float] = None,
upper_right_lat: Optional[float] = None,
upper_right_lon: Optional[float] = None) -> List[Address]:
"""地理编码:根据地名获取经纬度。优先使用 Nominatim API。"""
results: List[Address] = []
# 尝试在线地理编码
try:
import urllib.parse
params = {
"q": location_name,
"format": "jsonv2",
"addressdetails": "1",
"limit": str(max_results),
}
# 添加边界框
if all(v is not None for v in (lower_left_lat, lower_left_lon,
upper_right_lat, upper_right_lon)):
params["viewbox"] = f"{lower_left_lon},{upper_right_lat},{upper_right_lon},{lower_left_lat}"
params["bounded"] = "1"
query = urllib.parse.urlencode(params)
url = f"{self._NOMINATIM_URL}/search?{query}"
data = self._http_get_json(url)
if data and isinstance(data, list):
for item in data[:max_results]:
addr = self._parse_nominatim_search(item)
results.append(addr)
except Exception:
pass
# 回退到默认地址
if not results:
addr = Address()
addr.setFeatureName(location_name)
addr.setLocality(location_name)
results.append(addr)
return results[:max_results] if max_results > 0 else []
def _parse_nominatim_search(self, item: dict) -> Address:
"""解析 Nominatim 搜索响应。"""
addr = Address()
addr.setFeatureName(location_name)
addr.setLocality(location_name)
return [addr] if max_results > 0 else []
lat = item.get("lat")
lon = item.get("lon")
if lat is not None:
addr.setLatitude(float(lat))
if lon is not None:
addr.setLongitude(float(lon))
address = item.get("address", {})
addr.setFeatureName(item.get("name") or address.get("road") or "Unknown")
addr.setLocality(address.get("city") or address.get("town")
or address.get("village") or address.get("county") or "Unknown")
addr.setAdminArea(address.get("state") or "")
addr.setCountryName(address.get("country") or "Unknown")
addr.setCountryCode(address.get("country_code", "").upper())
addr.setPostalCode(address.get("postcode") or "")
addr.setAddressLine(0, item.get("display_name", ""))
return addr
# ═══════════════════════════════════════════════════════════════
@@ -950,8 +1110,20 @@ class LocationManager(Stub):
self._last_known_locations: Dict[str, Location] = {}
self._enabled_providers = {self.GPS_PROVIDER, self.NETWORK_PROVIDER,
self.PASSIVE_PROVIDER, self.FUSED_PROVIDER}
self._test_providers: Dict[str, LocationProvider] = {}
self._test_locations: Dict[str, Location] = {}
self._proximity_alerts: List[Dict[str, Any]] = []
def getLastKnownLocation(self, provider: str) -> Optional[Location]:
if _USE_LINUX:
data = LinuxLocation.get_location()
loc = Location(provider or "network")
loc.setLatitude(data.get("latitude", 0.0))
loc.setLongitude(data.get("longitude", 0.0))
loc.setAccuracy(data.get("accuracy", 0))
loc.setTime(int(datetime.now().timestamp() * 1000))
loc.setProvider(provider or "network")
return loc
if provider in self._last_known_locations:
return self._last_known_locations[provider]
loc = Location(provider)
@@ -969,6 +1141,18 @@ class LocationManager(Stub):
provider = provider_or_request
else:
provider = self.GPS_PROVIDER
if _USE_LINUX and listener_or_callback is not None:
data = LinuxLocation.get_location()
loc = Location(provider or "network")
loc.setLatitude(data.get("latitude", 0.0))
loc.setLongitude(data.get("longitude", 0.0))
loc.setAccuracy(data.get("accuracy", 0))
loc.setTime(int(datetime.now().timestamp() * 1000))
loc.setProvider(provider or "network")
try:
listener_or_callback.onLocationChanged(loc)
except Exception:
pass
if listener_or_callback is not None:
if provider not in self._listeners:
self._listeners[provider] = []
@@ -984,9 +1168,13 @@ class LocationManager(Stub):
del self._listeners[provider]
def isProviderEnabled(self, provider: str) -> bool:
if _USE_LINUX:
return provider == "network"
return provider in self._enabled_providers
def getProviders(self, criteria: Optional[Criteria] = None, enabled_only: bool = False) -> List[str]:
if _USE_LINUX:
return ["network"]
providers = list(self._providers.keys())
if enabled_only:
providers = [p for p in providers if p in self._enabled_providers]
@@ -1005,19 +1193,42 @@ class LocationManager(Stub):
def addProximityAlert(self, latitude: float, longitude: float,
radius: float, expiration: int, intent: Any):
pass
"""添加接近警报。记录警报信息,在位置更新时检查是否进入/离开区域。"""
import time as _time
alert = {
"latitude": latitude,
"longitude": longitude,
"radius": radius,
"expiration": expiration,
"intent": intent,
"created_at": _time.time(),
"last_state": None, # None=未触发, True=在区域内, False=在区域外
}
self._proximity_alerts.append(alert)
def sendExtraCommand(self, provider: str, command: str, extras: Any) -> bool:
"""向位置提供者发送额外命令。桌面端无实际效果,返回成功。"""
return True
def clearTestProviderLocation(self, provider: str):
pass
"""清除测试提供者的位置数据。"""
self._test_locations.pop(provider, None)
def setTestProviderLocation(self, provider: str, location: Location):
pass
"""设置测试提供者的位置。"""
self._test_locations[provider] = location
# 通知该提供者的监听器
if provider in self._listeners:
for listener in self._listeners[provider]:
try:
listener.onLocationChanged(location)
except Exception:
pass
def removeTestProvider(self, provider: str):
pass
"""移除测试提供者。"""
self._test_providers.pop(provider, None)
self._test_locations.pop(provider, None)
def addTestProvider(self, name: str, requires_network: bool, requires_satellite: bool,
requires_cell: bool, has_monetary_cost: bool, supports_altitude: bool,
+53 -2
View File
@@ -20,6 +20,13 @@ from .._core import Stub, AndroidOnlyError, _app_data_dir, _sdcard_root
from ..java.lang import RuntimeException
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
@@ -517,14 +524,58 @@ class ConnectivityManager(Stub):
self._active_network: Optional[Any] = None
self._all_networks: List[Any] = []
def _get_linux_active_info(self):
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
try:
from .net import NetworkInfo
interfaces = LinuxNetwork.get_interfaces()
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
connected = any(
(iface.get("ifname") or iface.get("name", "")) != "lo"
and iface.get("operstate", "").lower() == "up"
for iface in interfaces
)
if not connected:
connected = LinuxNetwork.get_connectivity()
ip = LinuxNetwork.get_ip_address()
iface_name = ""
net_type = ConnectivityManager.TYPE_ETHERNET
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
state = iface.get("operstate", "")
if name and name != "lo" and state.lower() == "up":
iface_name = iface_name or name
if name and name.startswith(("wl", "wlan", "wlp")):
net_type = ConnectivityManager.TYPE_WIFI
iface_name = name
data = {
"connected": connected,
"ip": ip,
"interface": iface_name,
"type_name": "WIFI" if net_type == ConnectivityManager.TYPE_WIFI else "ETHERNET",
}
return NetworkInfo.from_linux_data(data, network_type=net_type)
except Exception:
return self._active_network_info
def getActiveNetworkInfo(self) -> Optional[Any]:
if _USE_LINUX:
return self._get_linux_active_info()
return self._active_network_info
def getNetworkInfo(self, networkType: int) -> Optional[Any]:
if _USE_LINUX:
info = self._get_linux_active_info()
if info is not None and info.getType() == networkType:
return info
return None
return None
def getNetworkInfo(self, network: Any) -> Optional[Any]:
return None
def getConnectivityStatus(self) -> bool:
"""返回当前网络连通性 (True=已连接)"""
if _USE_LINUX:
return LinuxNetwork.get_connectivity()
return False
def getAllNetworkInfo(self) -> List[Any]:
return self._all_networks
+513 -37
View File
@@ -4,8 +4,10 @@ Android Media 组件 — MediaPlayer, MediaRecorder, SoundPool, AudioManager 等
"""
import io
import json
import math
import os
import subprocess
import time
import struct
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
@@ -13,6 +15,120 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, resolve_android_path
from .graphics import PointF
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxAudio, HAS_AMIXER
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# ffprobe / ffmpeg 辅助函数
# ═══════════════════════════════════════════════════════════════
def _ffprobe_metadata(path: str) -> Optional[Dict[str, Any]]:
"""用 ffprobe 获取媒体文件元数据,返回解析后的 JSON 字典。
失败时返回 None。"""
try:
result = subprocess.run(
["ffprobe", "-v", "quiet", "-print_format", "json",
"-show_format", "-show_streams", path],
capture_output=True, text=True, timeout=30,
)
if result.returncode != 0:
return None
return json.loads(result.stdout)
except (FileNotFoundError, subprocess.SubprocessError,
json.JSONDecodeError, OSError):
return None
def _ffprobe_duration_ms(path: str) -> int:
"""用 ffprobe 获取时长(毫秒),失败返回 0。"""
data = _ffprobe_metadata(path)
if not data:
return 0
try:
fmt = data.get("format", {})
duration = fmt.get("duration")
if duration is not None:
return int(float(duration) * 1000)
# 回退:从流中找
for stream in data.get("streams", []):
d = stream.get("duration")
if d is not None:
return int(float(d) * 1000)
except (ValueError, TypeError):
pass
return 0
def _ffprobe_video_size(path: str) -> Tuple[int, int]:
"""用 ffprobe 获取视频分辨率 (width, height),失败返回 (0, 0)。"""
data = _ffprobe_metadata(path)
if not data:
return (0, 0)
try:
for stream in data.get("streams", []):
if stream.get("codec_type") == "video":
w = int(stream.get("width", 0))
h = int(stream.get("height", 0))
if w > 0 and h > 0:
return (w, h)
except (ValueError, TypeError):
pass
return (0, 0)
def _ffmpeg_extract_frame(path: str, time_us: int = -1,
width: int = 0, height: int = 0) -> Optional[Any]:
"""用 ffmpeg 提取指定时间点的视频帧,返回 PIL Image。
time_us < 0 表示提取第一帧。失败返回 None。"""
if not HAS_PIL:
return None
try:
from PIL import Image
cmd = ["ffmpeg", "-v", "quiet"]
if time_us is not None and time_us >= 0:
cmd += ["-ss", str(time_us / 1_000_000.0)]
cmd += ["-i", path, "-frames:v", "1", "-f", "image2pipe",
"-vcodec", "png", "-"]
if width > 0 and height > 0:
# 在解码后缩放
cmd = ["ffmpeg", "-v", "quiet"]
if time_us is not None and time_us >= 0:
cmd += ["-ss", str(time_us / 1_000_000.0)]
cmd += ["-i", path, "-frames:v", "1", "-vf",
f"scale={width}:{height}", "-f", "image2pipe",
"-vcodec", "png", "-"]
result = subprocess.run(cmd, capture_output=True, timeout=30)
if result.returncode != 0 or not result.stdout:
return None
return Image.open(io.BytesIO(result.stdout))
except (FileNotFoundError, subprocess.SubprocessError, OSError):
return None
except Exception:
return None
def _ffmpeg_extract_cover(path: str) -> Optional[bytes]:
"""用 ffmpeg 提取嵌入的封面图片,返回原始字节。
失败返回 None。"""
try:
# 尝试提取附加图片流(封面)
cmd = ["ffmpeg", "-v", "quiet", "-i", path,
"-map", "0:v", "-frames:v", "1", "-f", "image2pipe",
"-vcodec", "png", "-"]
result = subprocess.run(cmd, capture_output=True, timeout=30)
if result.returncode == 0 and result.stdout:
return result.stdout
return None
except (FileNotFoundError, subprocess.SubprocessError, OSError):
return None
except Exception:
return None
# ═══════════════════════════════════════════════════════════════
# AudioAttributes
@@ -236,6 +352,7 @@ class MediaPlayer(Stub):
def __init__(self):
super().__init__()
self._data_source = None
self._resolved_path = None
self._prepared = False
self._playing = False
self._looping = False
@@ -250,6 +367,8 @@ class MediaPlayer(Stub):
self._audio_attributes = None
self._display = None
self._surface = None
self._released = False
self._start_time = 0.0 # 用于模拟播放进度
# Listeners
self._on_prepared_listener = None
@@ -260,43 +379,126 @@ class MediaPlayer(Stub):
self._on_video_size_changed_listener = None
def setDataSource(self, path: str = None, uri=None, headers: Dict = None):
self._data_source = path or str(uri)
self._data_source = path or str(uri) if uri is not None else path
if self._data_source:
self._resolved_path = resolve_android_path(self._data_source)
def _load_media_info(self):
"""用 ffprobe 加载时长和分辨率信息(try/except 保护)。"""
if not self._resolved_path:
return
if not os.path.exists(self._resolved_path):
return
try:
duration = _ffprobe_duration_ms(self._resolved_path)
if duration > 0:
self._duration = duration
except Exception:
pass
try:
w, h = _ffprobe_video_size(self._resolved_path)
if w > 0 and h > 0:
self._video_width = w
self._video_height = h
if self._on_video_size_changed_listener:
self._on_video_size_changed_listener.onVideoSizeChanged(
self, w, h)
except Exception:
pass
def prepare(self):
if self._released:
return
if self._resolved_path and not os.path.exists(self._resolved_path):
if self._on_error_listener:
self._on_error_listener.onError(
self, MediaPlayer.MEDIA_ERROR_UNKNOWN, -1004)
raise IOError(f"数据源文件不存在: {self._resolved_path}")
self._load_media_info()
self._prepared = True
def prepareAsync(self):
if self._released:
return
if self._resolved_path and not os.path.exists(self._resolved_path):
if self._on_error_listener:
self._on_error_listener.onError(
self, MediaPlayer.MEDIA_ERROR_UNKNOWN, -1004)
return
self._load_media_info()
self._prepared = True
if self._on_prepared_listener:
self._on_prepared_listener.onPrepared(self)
def start(self):
if self._released:
return
self._playing = True
self._start_time = time.time()
def stop(self):
self._playing = False
def pause(self):
if self._playing:
# 记录已播放的进度
elapsed = int((time.time() - self._start_time) * 1000)
self._current_position = min(
self._current_position + elapsed, self._duration)
self._playing = False
def reset(self):
self._playing = False
self._prepared = False
self._current_position = 0
self._duration = 0
self._video_width = 0
self._video_height = 0
self._data_source = None
self._resolved_path = None
def release(self):
self._playing = False
self._prepared = False
self._released = True
self._data_source = None
self._resolved_path = None
self._on_prepared_listener = None
self._on_completion_listener = None
self._on_error_listener = None
self._on_info_listener = None
self._on_seek_complete_listener = None
self._on_video_size_changed_listener = None
def seekTo(self, msec: int):
self._current_position = max(0, min(msec, self._duration))
self._start_time = time.time()
if self._on_seek_complete_listener:
self._on_seek_complete_listener.onSeekComplete(self)
def getCurrentPosition(self) -> int:
if self._playing and self._start_time > 0:
elapsed = int((time.time() - self._start_time) * 1000)
pos = self._current_position + elapsed
if self._duration > 0 and pos >= self._duration:
pos = self._duration
if self._looping:
self._current_position = 0
self._start_time = time.time()
pos = 0
else:
self._playing = False
if self._on_completion_listener:
self._on_completion_listener.onCompletion(self)
return pos
return self._current_position
def getDuration(self) -> int:
if self._duration == 0 and self._resolved_path:
try:
self._duration = _ffprobe_duration_ms(self._resolved_path)
except Exception:
pass
return self._duration
def isPlaying(self) -> bool:
@@ -328,9 +530,25 @@ class MediaPlayer(Stub):
self._display = surface_holder
def getVideoWidth(self) -> int:
if self._video_width == 0 and self._resolved_path:
try:
w, h = _ffprobe_video_size(self._resolved_path)
if w > 0:
self._video_width = w
self._video_height = h
except Exception:
pass
return self._video_width
def getVideoHeight(self) -> int:
if self._video_height == 0 and self._resolved_path:
try:
w, h = _ffprobe_video_size(self._resolved_path)
if h > 0:
self._video_width = w
self._video_height = h
except Exception:
pass
return self._video_height
def setOnPreparedListener(self, listener: "MediaPlayer.OnPreparedListener"):
@@ -427,6 +645,15 @@ class MediaRecorder(Stub):
self._output_file = None
self._preview_display = None
self._recording = False
self._prepared = False
self._released = False
self._video_width = 0
self._video_height = 0
self._video_frame_rate = 0
self._video_encoding_bit_rate = 0
self._audio_encoding_bit_rate = 0
self._max_duration = 0
self._max_filesize = 0
def setAudioSource(self, audio_source: int):
self._audio_source = audio_source
@@ -446,13 +673,36 @@ class MediaRecorder(Stub):
def setOutputFile(self, path: str):
self._output_file = path
def setVideoSize(self, width: int, height: int):
self._video_width = width
self._video_height = height
def setVideoFrameRate(self, rate: int):
self._video_frame_rate = rate
def setVideoEncodingBitRate(self, bit_rate: int):
self._video_encoding_bit_rate = bit_rate
def setAudioEncodingBitRate(self, bit_rate: int):
self._audio_encoding_bit_rate = bit_rate
def setMaxDuration(self, max_duration_ms: int):
self._max_duration = max_duration_ms
def setMaxFileSize(self, max_filesize_bytes: int):
self._max_filesize = max_filesize_bytes
def setPreviewDisplay(self, surface_holder):
self._preview_display = surface_holder
def prepare(self):
pass
if self._released:
return
self._prepared = True
def start(self):
if self._released or not self._prepared:
return
self._recording = True
def stop(self):
@@ -460,11 +710,29 @@ class MediaRecorder(Stub):
def reset(self):
self._recording = False
self._prepared = False
self._audio_source = 0
self._video_source = 0
self._output_format = 0
self._audio_encoder = 0
self._video_encoder = 0
self._output_file = None
self._video_width = 0
self._video_height = 0
self._video_frame_rate = 0
self._video_encoding_bit_rate = 0
def release(self):
self._recording = False
self._prepared = False
self._released = True
self._output_file = None
self._preview_display = None
def getMaxAmplitude(self) -> int:
if self._recording:
import random
return random.randint(0, 32767)
return 0
@@ -596,15 +864,37 @@ class AudioManager(Stub):
self._parameters: Dict[str, str] = {}
def getStreamVolume(self, streamType: int) -> int:
if _USE_LINUX:
# STREAM_MUSIC 及其它流类型映射到 Linux 主音量
return LinuxAudio.get_volume()
return self._stream_volumes.get(streamType, 0)
def getStreamMaxVolume(self, streamType: int) -> int:
if _USE_LINUX:
return 100
return 15
def setStreamVolume(self, streamType: int, index: int, flags: int = 0):
if _USE_LINUX:
# STREAM_MUSIC 及其它流类型映射到 Linux 主音量
LinuxAudio.set_volume(index)
return
if streamType in self._stream_volumes:
self._stream_volumes[streamType] = max(0, min(15, index))
def isMicrophoneMute(self) -> bool:
"""麦克风是否静音"""
if _USE_LINUX:
return LinuxAudio.get_mute()
return False
def setMicrophoneMute(self, on: bool):
"""设置麦克风静音"""
if _USE_LINUX:
LinuxAudio.set_mute(on)
return
# 存根: 无操作
def adjustVolume(self, direction: int, flags: int = 0):
pass
@@ -996,53 +1286,221 @@ class MediaMetadataRetriever(Stub):
METADATA_KEY_BITRATE = "bitrate"
METADATA_KEY_LOCATION = "location"
# 提取帧选项
OPTION_CLOSEST = 0x01
OPTION_CLOSEST_SYNC = 0x02
OPTION_NEXT_SYNC = 0x03
OPTION_PREVIOUS_SYNC = 0x04
# ffprobe format_name 到 MIME 类型映射
_MIME_MAP = {
"mov,mp4,m4a,3gp,3g2,mj2": "video/mp4",
"matroska,webm": "video/x-matroska",
"avi": "video/x-msvideo",
"flv": "video/x-flv",
"wav": "audio/wav",
"mp3": "audio/mpeg",
"aac": "audio/aac",
"ogg": "audio/ogg",
"flac": "audio/flac",
"asf": "video/x-ms-asf",
"mpeg": "video/mpeg",
"webm": "video/webm",
}
def __init__(self):
super().__init__()
self._metadata: Dict[str, str] = {}
self._resolved_path: Optional[str] = None
self._ffprobe_data: Optional[Dict[str, Any]] = None
def setDataSource(self, path: str):
resolved = resolve_android_path(path)
if os.path.exists(resolved):
size = os.path.getsize(resolved)
self._metadata = {
MediaMetadataRetriever.METADATA_KEY_DURATION: "30000",
MediaMetadataRetriever.METADATA_KEY_TITLE: os.path.basename(resolved),
MediaMetadataRetriever.METADATA_KEY_MIMETYPE: "video/mp4",
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH: "1920",
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT: "1080",
MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO: "yes",
MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO: "yes",
MediaMetadataRetriever.METADATA_KEY_BITRATE: str(size * 8 // 30),
}
def setDataSource(self, path: str, headers: Optional[Dict[str, str]] = None):
"""设置数据源并用 ffprobe 加载元数据。"""
self._resolved_path = resolve_android_path(path)
self._metadata = {}
self._ffprobe_data = None
if not self._resolved_path or not os.path.exists(self._resolved_path):
return
# 用 ffprobe 获取真实元数据(try/except 保护)
try:
self._ffprobe_data = _ffprobe_metadata(self._resolved_path)
except Exception:
self._ffprobe_data = None
if self._ffprobe_data:
self._parse_ffprobe_metadata()
def setDataSource(self, path: str, headers: Dict[str, str]):
self.setDataSource(path)
def _parse_ffprobe_metadata(self):
"""将 ffprobe 输出解析为 Android 风格的元数据键值对。"""
if not self._ffprobe_data:
return
data = self._ffprobe_data
fmt = data.get("format", {})
tags = fmt.get("tags", {})
streams = data.get("streams", [])
# 找到视频流和音频流
video_stream = None
audio_stream = None
for s in streams:
ct = s.get("codec_type", "")
if ct == "video" and video_stream is None:
video_stream = s
elif ct == "audio" and audio_stream is None:
audio_stream = s
md = self._metadata
# 时长(毫秒)
duration = fmt.get("duration")
if duration is None and video_stream:
duration = video_stream.get("duration")
if duration is None and audio_stream:
duration = audio_stream.get("duration")
if duration is not None:
try:
md[MediaMetadataRetriever.METADATA_KEY_DURATION] = str(
int(float(duration) * 1000))
except (ValueError, TypeError):
pass
# 标题
title = tags.get("title") or tags.get("TITLE")
if title:
md[MediaMetadataRetriever.METADATA_KEY_TITLE] = title
# 艺术家
artist = tags.get("artist") or tags.get("ARTIST")
if artist:
md[MediaMetadataRetriever.METADATA_KEY_ARTIST] = artist
# 专辑
album = tags.get("album") or tags.get("ALBUM")
if album:
md[MediaMetadataRetriever.METADATA_KEY_ALBUM] = album
# 作曲家
composer = tags.get("composer") or tags.get("COMPOSER")
if composer:
md[MediaMetadataRetriever.METADATA_KEY_COMPOSER] = composer
# 作者
writer = tags.get("writer") or tags.get("WRITER")
if writer:
md[MediaMetadataRetriever.METADATA_KEY_WRITER] = writer
# 流派
genre = tags.get("genre") or tags.get("GENRE")
if genre:
md[MediaMetadataRetriever.METADATA_KEY_GENRE] = genre
# 日期 / 年份
date = tags.get("date") or tags.get("DATE") or tags.get("year")
if date:
md[MediaMetadataRetriever.METADATA_KEY_DATE] = str(date)
# 尝试提取年份
year_str = str(date)[:4]
if year_str.isdigit():
md[MediaMetadataRetriever.METADATA_KEY_YEAR] = year_str
# 音轨号
track = tags.get("track") or tags.get("TRACK")
if track:
md[MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER] = str(track)
# 位置
location = tags.get("location") or tags.get("LOCATION")
if location:
md[MediaMetadataRetriever.METADATA_KEY_LOCATION] = str(location)
# MIME 类型
format_name = fmt.get("format_name", "")
if format_name:
mime = MediaMetadataRetriever._MIME_MAP.get(
format_name, f"video/{format_name.split(',')[0]}")
md[MediaMetadataRetriever.METADATA_KEY_MIMETYPE] = mime
# 轨道数
if streams:
md[MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS] = str(len(streams))
# 比特率
bit_rate = fmt.get("bit_rate")
if bit_rate:
md[MediaMetadataRetriever.METADATA_KEY_BITRATE] = str(bit_rate)
# 视频信息
if video_stream:
w = video_stream.get("width", 0)
h = video_stream.get("height", 0)
if w:
md[MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH] = str(w)
if h:
md[MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT] = str(h)
md[MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO] = "yes"
else:
md[MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO] = "no"
# 音频信息
if audio_stream:
md[MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO] = "yes"
else:
md[MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO] = "no"
def extractMetadata(self, keyCode: str) -> Optional[str]:
return self._metadata.get(keyCode)
def getEmbeddedPicture(self) -> Optional[bytes]:
"""用 ffmpeg 提取嵌入的封面图片,返回原始字节数据。
失败返回 None。"""
if not self._resolved_path or not os.path.exists(self._resolved_path):
return None
try:
return _ffmpeg_extract_cover(self._resolved_path)
except Exception:
return None
def getFrameAtTime(self, timeUs: int = -1, option: int = 0) -> Optional["Bitmap"]:
if HAS_PIL:
from PIL import Image
from .graphics import Bitmap
img = Image.new("RGB", (1920, 1080), (64, 64, 64))
bm = Bitmap(1920, 1080, Bitmap.Config.ARGB_8888)
bm._image = img
return bm
"""用 ffmpeg 提取指定时间点的视频帧,返回 Bitmap(内含 PIL Image)。
timeUs < 0 表示提取第一帧。失败返回 None。"""
if not self._resolved_path or not os.path.exists(self._resolved_path):
return None
try:
img = _ffmpeg_extract_frame(self._resolved_path, timeUs)
if img is not None:
from .graphics import Bitmap
w, h = img.size
bm = Bitmap(w, h, Bitmap.Config.ARGB_8888)
# 确保 RGBA 模式
if img.mode != "RGBA":
img = img.convert("RGBA")
bm._image = img
return bm
except Exception:
pass
return None
def getScaledFrameAtTime(self, timeUs: int, option: int, width: int, height: int) -> Optional["Bitmap"]:
if HAS_PIL:
from PIL import Image
from .graphics import Bitmap
img = Image.new("RGB", (width, height), (64, 64, 64))
bm = Bitmap(width, height, Bitmap.Config.ARGB_8888)
bm._image = img
return bm
def getScaledFrameAtTime(self, timeUs: int, option: int,
width: int, height: int) -> Optional["Bitmap"]:
"""用 ffmpeg 提取并缩放指定时间点的视频帧。"""
if not self._resolved_path or not os.path.exists(self._resolved_path):
return None
try:
img = _ffmpeg_extract_frame(self._resolved_path, timeUs, width, height)
if img is not None:
from .graphics import Bitmap
bm = Bitmap(width, height, Bitmap.Config.ARGB_8888)
if img.mode != "RGBA":
img = img.convert("RGBA")
bm._image = img
return bm
except Exception:
pass
return None
def release(self):
self._metadata.clear()
self._resolved_path = None
self._ffprobe_data = None
# ═══════════════════════════════════════════════════════════════
@@ -1788,6 +2246,7 @@ class ExifInterface(Stub):
super().__init__()
self._filename = resolve_android_path(filename)
self._attributes: Dict[str, str] = {}
self._pil_image = None
self._load_exif()
def _load_exif(self):
@@ -1799,6 +2258,7 @@ class ExifInterface(Stub):
from PIL import ExifTags
try:
img = Image.open(self._filename)
self._pil_image = img
exif_data = img._getexif()
if exif_data:
for tag_id, value in exif_data.items():
@@ -1870,13 +2330,29 @@ class ExifInterface(Stub):
pass
def flipHorizontally(self):
pass
if hasattr(self, '_pil_image') and self._pil_image:
from PIL import Image
self._pil_image = self._pil_image.transpose(Image.FLIP_LEFT_RIGHT)
self._save_from_pil()
def flipVertically(self):
pass
if hasattr(self, '_pil_image') and self._pil_image:
from PIL import Image
self._pil_image = self._pil_image.transpose(Image.FLIP_TOP_BOTTOM)
self._save_from_pil()
def rotate(self, degree: int):
pass
def rotate(self, degrees):
if hasattr(self, '_pil_image') and self._pil_image:
from PIL import Image
self._pil_image = self._pil_image.rotate(degrees, expand=True)
self._save_from_pil()
# 更新方向属性
self.setAttribute("Orientation", "1") # Normal after rotation
def _save_from_pil(self):
"""从 PIL 图像保存回文件"""
if hasattr(self, '_pil_image') and self._pil_image and hasattr(self, '_filename'):
self._pil_image.save(self._filename)
# ═══════════════════════════════════════════════════════════════
+195 -33
View File
@@ -17,6 +17,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Uri (re-export from content)
@@ -379,6 +386,38 @@ class NetworkInfo(Stub):
}
return names.get(type_, "UNKNOWN")
@classmethod
def from_linux_data(cls, data: Optional[dict] = None,
network_type: int = 9) -> "NetworkInfo":
"""从 Linux 真实网络数据构造 NetworkInfo
data 字典可包含: ip, interface, connected, type_name 等
"""
if data is None:
data = {}
connected = bool(data.get("connected", False))
state = "CONNECTED" if connected else "DISCONNECTED"
detailed_state = "CONNECTED" if connected else "DISCONNECTED"
type_name = data.get("type_name") or cls._default_type_name_static(network_type)
extra_info = data.get("ip") or data.get("interface")
return cls(
type_=network_type,
state=state,
detailed_state=detailed_state,
type_name=type_name,
extra_info=extra_info,
is_available=True,
roaming=False,
)
@staticmethod
def _default_type_name_static(type_: int) -> str:
names = {
0: "MOBILE", 1: "WIFI", 6: "WIMAX", 7: "BLUETOOTH",
9: "ETHERNET", 17: "VPN",
}
return names.get(type_, "UNKNOWN")
def getType(self) -> int:
return self._type
@@ -1040,53 +1079,98 @@ class TrafficStats(Stub):
UNSUPPORTED = -1
TAG_SYSTEM_UPDATE = 0
_cached_stats = None
_cache_time = 0
@staticmethod
def getMobileTxBytes() -> int:
def _read_net_dev():
"""读取 /proc/net/dev 获取网络流量"""
try:
with open("/proc/net/dev", "r") as f:
lines = f.readlines()[2:] # 跳过前两行表头
total_rx = 0
total_tx = 0
for line in lines:
parts = line.split()
ifname = parts[0].rstrip(":")
if ifname == "lo":
continue
rx_bytes = int(parts[1])
tx_bytes = int(parts[9])
total_rx += rx_bytes
total_tx += tx_bytes
return {"rx_bytes": total_rx, "tx_bytes": total_tx,
"rx_packets": 0, "tx_packets": 0}
except Exception:
return {"rx_bytes": 0, "tx_bytes": 0, "rx_packets": 0, "tx_packets": 0}
@staticmethod
def getMobileRxBytes():
return 0 # 桌面无移动数据
@staticmethod
def getMobileTxBytes():
return 0
@staticmethod
def getMobileRxBytes() -> int:
def getMobileRxPackets():
return 0
@staticmethod
def getTotalTxBytes() -> int:
def getMobileTxPackets():
return 0
@staticmethod
def getTotalRxBytes() -> int:
return 0
def getTotalRxBytes():
return TrafficStats._read_net_dev()["rx_bytes"]
@staticmethod
def getMobileTxPackets() -> int:
return 0
def getTotalTxBytes():
return TrafficStats._read_net_dev()["tx_bytes"]
@staticmethod
def getMobileRxPackets() -> int:
return 0
def getTotalRxPackets():
return TrafficStats._read_net_dev()["rx_packets"]
@staticmethod
def getTotalTxPackets() -> int:
return 0
def getTotalTxPackets():
return TrafficStats._read_net_dev()["tx_packets"]
@staticmethod
def getTotalRxPackets() -> int:
return 0
def getUidRxBytes(uid):
try:
with open(f"/proc/uid_stat/{uid}/tcp_rcv", "r") as f:
return int(f.read().strip())
except Exception:
return 0
@staticmethod
def getUidTxBytes(uid: int) -> int:
return 0
def getUidTxBytes(uid):
try:
with open(f"/proc/uid_stat/{uid}/tcp_snd", "r") as f:
return int(f.read().strip())
except Exception:
return 0
@staticmethod
def getUidRxBytes(uid: int) -> int:
return 0
def getUidRxPackets(uid):
return TrafficStats.getUidRxBytes(uid)
@staticmethod
def getUidTxPackets(uid: int) -> int:
return 0
def getUidTxPackets(uid):
return TrafficStats.getUidTxBytes(uid)
@staticmethod
def getUidRxPackets(uid: int) -> int:
return 0
def getThreadStatsTag():
return getattr(TrafficStats, '_thread_tag', 0)
@staticmethod
def setThreadStatsTag(tag):
TrafficStats._thread_tag = tag
@staticmethod
def clearThreadStatsTag():
TrafficStats._thread_tag = 0
@staticmethod
def getUidTcpTxBytes(uid: int) -> int:
@@ -1104,18 +1188,6 @@ class TrafficStats(Stub):
def getUidUdpRxBytes(uid: int) -> int:
return 0
@staticmethod
def setThreadStatsTag(tag: int):
pass
@staticmethod
def clearThreadStatsTag():
pass
@staticmethod
def getThreadStatsTag() -> int:
return 0
@staticmethod
def setThreadStatsUid(uid: int):
pass
@@ -1230,22 +1302,112 @@ class ConnectivityManager(Stub):
self._network_caps: Dict[int, NetworkCapabilities] = {}
self._link_props: Dict[int, LinkProperties] = {}
def _get_linux_active_info(self) -> Optional[NetworkInfo]:
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
try:
interfaces = LinuxNetwork.get_interfaces()
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
connected = any(
(iface.get("ifname") or iface.get("name", "")) != "lo"
and iface.get("operstate", "").lower() == "up"
for iface in interfaces
)
# 若有活跃接口但 get_connectivity 为 False (外网不可达), 仍视为已连接
if not connected:
connected = LinuxNetwork.get_connectivity()
ip = LinuxNetwork.get_ip_address()
iface_name = ""
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
state = iface.get("operstate", "")
if name and name != "lo" and state.lower() == "up":
iface_name = name
break
# 判断网络类型 (无线接口 -> WIFI, 否则 ETHERNET)
net_type = self.TYPE_ETHERNET
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
if name and name.startswith(("wl", "wlan", "wlp")):
net_type = self.TYPE_WIFI
iface_name = iface_name or name
break
data = {
"connected": connected,
"ip": ip,
"interface": iface_name,
"type_name": "WIFI" if net_type == self.TYPE_WIFI else "ETHERNET",
}
return NetworkInfo.from_linux_data(data, network_type=net_type)
except Exception:
return self._active_network_info
def getActiveNetworkInfo(self) -> Optional[NetworkInfo]:
if _USE_LINUX:
return self._get_linux_active_info()
return self._active_network_info
def getNetworkInfo(self, network_type: int) -> Optional[NetworkInfo]:
if _USE_LINUX:
info = self._get_linux_active_info()
if info is not None and info.getType() == network_type:
return info
# 请求的类型与活跃网络不同时, 返回断开状态
return NetworkInfo(
type_=network_type, state="DISCONNECTED",
detailed_state="DISCONNECTED", is_available=False,
)
network = self._networks.get(network_type)
if network:
return NetworkInfo(type_=network_type, state="CONNECTED", is_available=True)
return self._active_network_info
def getConnectivityStatus(self) -> bool:
"""返回当前网络连通性 (True=已连接)"""
if _USE_LINUX:
return LinuxNetwork.get_connectivity()
return True
def getNetworkInfoForUid(self, network: Network, uid: int) -> Optional[NetworkInfo]:
return self._active_network_info
def getAllNetworks(self) -> List[Network]:
if _USE_LINUX:
networks: List[Network] = []
try:
interfaces = LinuxNetwork.get_interfaces()
idx = 0
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
if not name or name == "lo":
continue
idx += 1
net = Network(idx)
networks.append(net)
# 若无真实接口, 回退到内部缓存
if networks:
return networks
except Exception:
pass
return list(self._networks.values())
def getActiveNetwork(self) -> Optional[Network]:
if _USE_LINUX:
try:
interfaces = LinuxNetwork.get_interfaces()
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
state = iface.get("operstate", "")
if name and name != "lo" and state.lower() == "up":
net = Network(1)
self._networks[1] = net
return net
# 回退: 若有外网连通性, 仍返回一个活跃 Network
if LinuxNetwork.get_connectivity():
net = Network(1)
self._networks[1] = net
return net
except Exception:
pass
if self._networks:
nid = next(iter(self._networks))
return self._networks[nid]
+78 -12
View File
@@ -14,6 +14,25 @@ import time
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError
from .net import ConnectivityManager as _BaseConnectivityManager
from .net import DhcpInfo as _BaseDhcpInfo
from .net import LinkAddress as _BaseLinkAddress
from .net import LinkProperties as _BaseLinkProperties
from .net import Network as _BaseNetwork
from .net import NetworkCapabilities as _BaseNetworkCapabilities
from .net import NetworkInfo as _BaseNetworkInfo
from .net import NetworkRequest as _BaseNetworkRequest
from .net import NetworkSpecifier as _BaseNetworkSpecifier
from .net import ProxyInfo as _BaseProxyInfo
from .net import TrafficStats as _BaseTrafficStats
from .net import MatchAllNetworkSpecifier as _BaseMatchAllNetworkSpecifier
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
__all__ = [
"Network", "NetworkCapabilities", "NetworkRequest", "NetworkSpecifier",
@@ -30,7 +49,7 @@ __all__ = [
# Network
# ═══════════════════════════════════════════════════════════════
class Network(Stub):
class Network(_BaseNetwork):
"""Network — 网络"""
def __init__(self, netId: int = 0):
@@ -74,7 +93,7 @@ class Network(Stub):
# NetworkCapabilities
# ═══════════════════════════════════════════════════════════════
class NetworkCapabilities(Stub):
class NetworkCapabilities(_BaseNetworkCapabilities):
"""NetworkCapabilities — 网络能力"""
TRANSPORT_CELLULAR = 0
@@ -236,7 +255,7 @@ class NetworkCapabilities(Stub):
# NetworkSpecifier / MatchAllNetworkSpecifier
# ═══════════════════════════════════════════════════════════════
class NetworkSpecifier(Stub):
class NetworkSpecifier(_BaseNetworkSpecifier):
"""NetworkSpecifier — 网络指定器"""
def satisfiedBy(self, other: Optional["NetworkSpecifier"]) -> bool:
@@ -252,7 +271,7 @@ class NetworkSpecifier(Stub):
return id(self) & 0x7FFFFFFF
class MatchAllNetworkSpecifier(NetworkSpecifier):
class MatchAllNetworkSpecifier(_BaseMatchAllNetworkSpecifier, NetworkSpecifier):
"""MatchAllNetworkSpecifier — 匹配所有网络"""
def satisfiedBy(self, other: Optional["NetworkSpecifier"]) -> bool:
@@ -266,7 +285,7 @@ class MatchAllNetworkSpecifier(NetworkSpecifier):
# NetworkRequest
# ═══════════════════════════════════════════════════════════════
class NetworkRequest(Stub):
class NetworkRequest(_BaseNetworkRequest):
"""NetworkRequest — 网络请求"""
def __init__(self, capabilities: Optional[NetworkCapabilities] = None,
@@ -430,7 +449,7 @@ class NetworkAgent(Stub):
# NetworkInfo (扩展)
# ═══════════════════════════════════════════════════════════════
class NetworkInfo(Stub):
class NetworkInfo(_BaseNetworkInfo):
"""NetworkInfo — 网络信息 (完整版)"""
TYPE_NONE = -1
@@ -558,7 +577,7 @@ class NetworkInfo(Stub):
# LinkAddress
# ═══════════════════════════════════════════════════════════════
class LinkAddress(Stub):
class LinkAddress(_BaseLinkAddress):
"""LinkAddress — 链路地址"""
def __init__(self, address: str = "0.0.0.0", prefixLength: int = 24,
@@ -600,7 +619,7 @@ class LinkAddress(Stub):
# LinkProperties
# ═══════════════════════════════════════════════════════════════
class LinkProperties(Stub):
class LinkProperties(_BaseLinkProperties):
"""LinkProperties — 链路属性"""
def __init__(self):
@@ -874,7 +893,7 @@ class MobileLinkQualityInfo(LinkQualityInfo):
# TrafficStats
# ═══════════════════════════════════════════════════════════════
class TrafficStats(Stub):
class TrafficStats(_BaseTrafficStats):
"""TrafficStats — 流量统计"""
UID_REMOVED = -4
@@ -969,7 +988,7 @@ class TrafficStats(Stub):
# ProxyInfo
# ═══════════════════════════════════════════════════════════════
class ProxyInfo(Stub):
class ProxyInfo(_BaseProxyInfo):
"""ProxyInfo — 代理信息"""
def __init__(self, host: str = "", port: int = 0,
@@ -1012,7 +1031,7 @@ class ProxyInfo(Stub):
# DhcpInfo / DhcpInfoInternal
# ═══════════════════════════════════════════════════════════════
class DhcpInfo(Stub):
class DhcpInfo(_BaseDhcpInfo):
"""DhcpInfo — DHCP 信息"""
def __init__(self):
@@ -1245,7 +1264,7 @@ class ConnectivityDiagnosticsManager(Stub):
# ConnectivityManager (扩展)
# ═══════════════════════════════════════════════════════════════
class ConnectivityManager(Stub):
class ConnectivityManager(_BaseConnectivityManager):
"""ConnectivityManager — 连接管理器 (扩展版)"""
TYPE_NONE = -1
@@ -1291,17 +1310,64 @@ class ConnectivityManager(Stub):
def getActiveNetwork(self) -> Optional[Network]:
return self._active_network
def _get_linux_active_info(self):
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
try:
from .net import NetworkInfo
interfaces = LinuxNetwork.get_interfaces()
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
connected = any(
(iface.get("ifname") or iface.get("name", "")) != "lo"
and iface.get("operstate", "").lower() == "up"
for iface in interfaces
)
if not connected:
connected = LinuxNetwork.get_connectivity()
ip = LinuxNetwork.get_ip_address()
iface_name = ""
net_type = ConnectivityManager.TYPE_ETHERNET
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
state = iface.get("operstate", "")
if name and name != "lo" and state.lower() == "up":
iface_name = iface_name or name
if name and name.startswith(("wl", "wlan", "wlp")):
net_type = ConnectivityManager.TYPE_WIFI
iface_name = name
data = {
"connected": connected,
"ip": ip,
"interface": iface_name,
"type_name": "WIFI" if net_type == ConnectivityManager.TYPE_WIFI else "ETHERNET",
}
return NetworkInfo.from_linux_data(data, network_type=net_type)
except Exception:
return None
def getActiveNetworkInfo(self) -> Optional[NetworkInfo]:
if _USE_LINUX:
return self._get_linux_active_info()
if self._active_network:
return self._network_infos.get(self._active_network.getNetId())
return None
def getNetworkInfo(self, networkType: int) -> Optional[NetworkInfo]:
if _USE_LINUX:
info = self._get_linux_active_info()
if info is not None and info.getType() == networkType:
return info
return None
for info in self._network_infos.values():
if info.getType() == networkType:
return info
return None
def getConnectivityStatus(self) -> bool:
"""返回当前网络连通性 (True=已连接)"""
if _USE_LINUX:
return LinuxNetwork.get_connectivity()
return self._active_network is not None
def getNetworkInfoForNetwork(self, network: Network) -> Optional[NetworkInfo]:
return self._network_infos.get(network.getNetId())
+116
View File
@@ -16,6 +16,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# SupplicantState
@@ -620,6 +627,8 @@ class WifiManager(Stub):
self._p2p_channels: List[int] = []
def getConnectionInfo(self) -> Optional[WifiInfo]:
if _USE_LINUX:
return self._get_linux_connection_info()
if self._connection_info is None:
info = WifiInfo()
info._ssid = '"mock_wifi"'
@@ -632,20 +641,127 @@ class WifiManager(Stub):
self._connection_info = info
return self._connection_info
def getWifiInfo(self) -> Optional[WifiInfo]:
"""getWifiInfo — getConnectionInfo 的别名"""
return self.getConnectionInfo()
def _get_linux_connection_info(self) -> WifiInfo:
"""从 LinuxNetwork.get_wifi_info() 构造 WifiInfo"""
info = WifiInfo()
try:
data = LinuxNetwork.get_wifi_info() or {}
ssid = data.get("ssid", "")
info._ssid = f'"{ssid}"' if ssid else "<unknown ssid>"
info._bssid = data.get("bssid", "00:00:00:00:00:00")
# signal 通常是 0-100, 转换为 RSSI (dBm, 约 -100..0)
signal = data.get("signal", 0)
if isinstance(signal, (int, float)) and signal > 0:
info._rssi = int(-100 + (signal / 100.0) * 60)
else:
info._rssi = int(signal) if signal else WifiInfo.INVALID_RSSI
iface = data.get("interface", "")
if iface:
info._frequency = 2412 # 默认 2.4GHz
info._link_speed = data.get("link_speed", 0) or 0
info._network_id = 0
info._supplicant_state = SupplicantState(SupplicantState.COMPLETED)
except Exception:
pass
return info
def getScanResults(self) -> List[ScanResult]:
if _USE_LINUX:
results = self._linux_scan_results()
if results is not None:
return results
return list(self._scan_results)
def _linux_scan_results(self) -> Optional[List[ScanResult]]:
"""通过 nmcli 获取 WiFi 扫描结果 (真实实现)"""
import subprocess
import re
try:
proc = subprocess.run(
["nmcli", "-t", "-f", "ssid,bssid,signal,freq,security",
"dev", "wifi", "list"],
capture_output=True, text=True, timeout=15,
)
if proc.returncode != 0:
return None
results: List[ScanResult] = []
seen: Set[str] = set()
for line in proc.stdout.strip().split("\n"):
if not line:
continue
# nmcli -t 模式中值内的 : 被转义为 \:, 按非转义 : 分割
parts = re.split(r'(?<!\\):', line)
parts = [p.replace('\\:', ':') for p in parts]
if len(parts) < 5:
continue
ssid = parts[0]
bssid = parts[1]
if not bssid or bssid in seen:
continue
seen.add(bssid)
sr = ScanResult()
sr.SSID = ssid
sr.BSSID = bssid
try:
sr.level = int(parts[2]) if parts[2] else 0
except ValueError:
sr.level = 0
try:
sr.frequency = int(float(parts[3])) if parts[3] else 0
except ValueError:
sr.frequency = 0
sr.capabilities = parts[4] if len(parts) > 4 else ""
results.append(sr)
return results
except Exception:
return None
def startScan(self) -> bool:
return True
def setWifiEnabled(self, enabled: bool) -> bool:
if _USE_LINUX:
import subprocess
import shutil
try:
cmd = ["nmcli", "radio", "wifi", "on" if enabled else "off"]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
if proc.returncode != 0 and shutil.which("rfkill"):
# 回退到 rfkill
rfkill_cmd = (["rfkill", "unblock", "wifi"] if enabled
else ["rfkill", "block", "wifi"])
subprocess.run(rfkill_cmd, capture_output=True, timeout=5)
except Exception:
pass
self._wifi_enabled = enabled
self._wifi_state = self.WIFI_STATE_ENABLED if enabled else self.WIFI_STATE_DISABLED
return True
def isWifiEnabled(self) -> bool:
if _USE_LINUX:
return self._linux_has_wireless_interface()
return self._wifi_enabled
def _linux_has_wireless_interface(self) -> bool:
"""检查是否存在无线网络接口"""
try:
for iface in LinuxNetwork.get_interfaces():
name = iface.get("ifname") or iface.get("name", "")
if name and name.startswith(("wl", "wlan", "wlp")):
return True
# 回退: 检查 /sys/class/net 下的无线接口
import os as _os
for name in _os.listdir("/sys/class/net/") if _os.path.isdir("/sys/class/net") else []:
if name.startswith(("wl", "wlan", "wlp")):
return True
except Exception:
pass
return False
def getWifiState(self) -> int:
return self._wifi_state
+551 -40
View File
@@ -20,6 +20,13 @@ from .._core import (
HAS_PSUTIL,
)
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxPower, LinuxProcess, HAS_PSUTIL as _HAS_PSUTIL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Build
@@ -108,16 +115,95 @@ class Build(Stub):
@staticmethod
def getRadioVersion() -> str:
return ""
"""返回基带/无线电版本。桌面端无无线电硬件,返回空字符串。"""
return Build.RADIO
@staticmethod
def getSerial() -> str:
return "unknown"
"""返回设备序列号。尝试从 /sys 或 /proc 读取真实硬件序列号。"""
# 尝试从 /sys/class/android_usb 读取
try:
for name in _os.listdir("/sys/class/android_usb"):
serial_path = f"/sys/class/android_usb/{name}/iSerial"
if _os.path.exists(serial_path):
with open(serial_path, "r") as f:
serial = f.read().strip()
if serial:
return serial
except (OSError, IOError):
pass
# 尝试从 /proc/cpuinfo 读取 Serial 字段
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if line.lower().startswith("serial"):
parts = line.split(":", 1)
if len(parts) == 2:
serial = parts[1].strip()
if serial and serial != "0" * len(serial):
return serial
except (OSError, IOError):
pass
# 尝试从 /sys/class/dmi/id/product_serial 读取
try:
path = "/sys/class/dmi/id/product_serial"
if _os.path.exists(path):
with open(path, "r") as f:
serial = f.read().strip()
if serial:
return serial
except (OSError, IOError):
pass
return Build.SERIAL
@staticmethod
def getFingerprint() -> str:
return Build.FINGERPRINT
@classmethod
def _populate_from_linux(cls):
"""从 LinuxProcess.get_system_info() 填充真实设备信息"""
try:
info = LinuxProcess.get_system_info() or {}
import platform as _platform
import socket as _socket
hostname = info.get("hostname") or _socket.gethostname()
system = info.get("platform") or _platform.system()
arch = info.get("arch") or _platform.machine()
kernel = info.get("kernel") or _platform.release()
cls.MODEL = hostname
cls.MANUFACTURER = "Linux"
cls.BRAND = system
cls.DEVICE = arch
cls.BOARD = arch
cls.HARDWARE = arch
cls.HOST = hostname
cls.PRODUCT = hostname
cls.DISPLAY = kernel
cls.ID = kernel
# FINGERPRINT: brand/device/device:version/id/build:user/release-keys
cls.FINGERPRINT = (
f"{system}/{hostname}/{arch}:Linux/{kernel}/{hostname}:user/release-keys"
)
# CPU ABI 用架构映射
_abi_map = {
"x86_64": "x86_64", "amd64": "x86_64",
"aarch64": "arm64-v8a", "arm64": "arm64-v8a",
"x86": "x86", "i386": "x86", "i686": "x86",
"armv7l": "armeabi-v7a", "arm": "armeabi-v7a",
}
cls.CPU_ABI = _abi_map.get(arch, arch)
cls.SUPPORTED_ABIS = [cls.CPU_ABI]
cls.SUPPORTED_64_BIT_ABIS = [cls.CPU_ABI] if "64" in cls.CPU_ABI else []
cls.SUPPORTED_32_BIT_ABIS = [cls.CPU_ABI] if "64" not in cls.CPU_ABI else []
except Exception:
pass
# 在 Linux 上用真实系统信息填充 Build
if _USE_LINUX:
Build._populate_from_linux()
# ═══════════════════════════════════════════════════════════════
# SystemClock
@@ -150,7 +236,17 @@ class SystemClock(Stub):
@staticmethod
def setCurrentTimeMillis(millis: int) -> bool:
return False
"""尝试设置系统时间。需要 root 权限,通常在桌面端会失败。"""
try:
import subprocess
seconds = millis / 1000.0
# 尝试使用 date 命令设置时间
result = subprocess.run(
["date", "-s", f"@{int(seconds)}"],
capture_output=True, timeout=5)
return result.returncode == 0
except Exception:
return False
# ═══════════════════════════════════════════════════════════════
@@ -217,6 +313,20 @@ class Environment(Stub):
@staticmethod
def isExternalStorageRemovable(path: Optional[str] = None) -> bool:
"""判断外部存储是否可移除。通过检查 /proc/mounts 判断。"""
try:
check_path = path or _sdcard_root
with open("/proc/mounts", "r") as f:
for line in f:
parts = line.split()
if len(parts) >= 3 and check_path in parts[1]:
# 检查挂载选项中是否包含 SD 卡相关设备
device = parts[0]
if "/sd" in device or "/mmc" in device or "/usb" in device:
return True
return False
except (OSError, IOError):
pass
return False
@staticmethod
@@ -258,27 +368,42 @@ class Process(Stub):
@staticmethod
def myUid() -> int:
return 10001
"""返回当前进程的真实 UID。"""
return _os.getuid()
@staticmethod
def myPid() -> int:
return _os.getpid()
@staticmethod
def killProcess(pid: int):
pass
def killProcess(pid):
import signal
try:
_os.kill(pid, signal.SIGKILL)
except (OSError, ProcessLookupError):
pass
@staticmethod
def sendSignal(pid: int, signal: int):
pass
def sendSignal(pid, signal_num):
try:
_os.kill(pid, signal_num)
except (OSError, ProcessLookupError):
pass
@staticmethod
def setThreadPriority(priority: int):
pass
@staticmethod
def setThreadPriority(tid: int, priority: int):
pass
def setThreadPriority(*args):
"""设置线程优先级。支持 setThreadPriority(priority) 和 setThreadPriority(tid, priority)。"""
if len(args) == 1:
tid = 0
priority = args[0]
elif len(args) == 2:
tid, priority = args
else:
return
try:
_os.setpriority(_os.PRIO_PROCESS, tid, priority)
except (OSError, PermissionError):
pass
@staticmethod
def getElapsedCpuTime() -> int:
@@ -945,7 +1070,20 @@ class Looper(Stub):
return self._message_queue
def dump(self, pw: Any, prefix: str):
pass
"""将 DropBox 条目转储到给定的 writer。"""
try:
if hasattr(pw, "write"):
pw.write(f"{prefix}DropBoxManager entries:\n")
for entry in self._entries:
pw.write(f"{prefix} [{entry.getTag()}] @ {entry.getTimeMillis()}: "
f"{entry.getText(1024)}\n")
elif hasattr(pw, "println"):
pw.println(f"{prefix}DropBoxManager entries:")
for entry in self._entries:
pw.println(f"{prefix} [{entry.getTag()}] @ {entry.getTimeMillis()}: "
f"{entry.getText(1024)}")
except Exception:
pass
def toString(self) -> str:
return f"Looper({self._thread.name})"
@@ -1299,9 +1437,34 @@ class PowerManager(Stub):
return PowerManagerWakeLock(level_and_flags, tag)
def isInteractive(self) -> bool:
if _USE_LINUX:
return self.isScreenOn()
return self._is_interactive
def isScreenOn(self) -> bool:
"""屏幕是否点亮 — 在 Linux 上检查 DISPLAY 环境变量"""
if _USE_LINUX:
import os as _os_mod
display = _os_mod.environ.get("DISPLAY", "")
if display:
return True
# WAYLAND_DISPLAY 作为回退
wayland = _os_mod.environ.get("WAYLAND_DISPLAY", "")
return bool(wayland)
return self._is_interactive
def isPowerSaveMode(self) -> bool:
"""检查是否处于省电模式。读取 /sys 中的电源相关状态。"""
try:
# 检查 CPU 调速器是否为 powersave
gov_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"
if _os.path.exists(gov_path):
with open(gov_path, "r") as f:
governor = f.read().strip()
if governor == "powersave":
return True
except (OSError, IOError):
pass
return self._is_power_save
def isDeviceIdleMode(self) -> bool:
@@ -1314,16 +1477,29 @@ class PowerManager(Stub):
return False
def reboot(self, reason: Optional[str] = None):
pass
"""重启设备。桌面端尝试执行 reboot 命令(通常需要 root)。"""
try:
import subprocess
cmd = ["reboot"]
if reason:
cmd.append(reason)
subprocess.run(cmd, capture_output=True, timeout=5)
except Exception:
sys.stderr.write(f"[PowerManager] reboot: {reason or ''}\n")
def goToSleep(self, time_ms: int):
pass
"""使设备进入睡眠。桌面端无实际效果。"""
self._is_interactive = False
sys.stderr.write(f"[PowerManager] goToSleep at {time_ms}\n")
def wakeUp(self, time_ms: int):
pass
"""唤醒设备。桌面端无实际效果。"""
self._is_interactive = True
sys.stderr.write(f"[PowerManager] wakeUp at {time_ms}\n")
def userActivity(self, when: int, no_change_lights: bool = False):
pass
"""报告用户活动事件。桌面端无实际效果。"""
self._is_interactive = True
class PowerManagerWakeLock(Stub):
@@ -1373,20 +1549,101 @@ class Vibrator(Stub):
def __init__(self):
super().__init__()
self._ff_device: Optional[str] = None
self._vibrating = False
def vibrate(self, milliseconds: int):
sys.stderr.write(f"[Vibrator] vibrate: {milliseconds}ms\n")
@staticmethod
def _find_ff_device() -> Optional[str]:
"""查找 Linux 力反馈设备。"""
try:
for name in _os.listdir("/dev/input"):
if name.startswith("event"):
path = f"/dev/input/{name}"
if _os.access(path, _os.W_OK):
return path
except (OSError, IOError):
pass
# 检查 /sys/class/leds/vibrator
try:
if _os.path.exists("/sys/class/leds/vibrator"):
return "/sys/class/leds/vibrator"
except (OSError, IOError):
pass
return None
def vibrate(self, pattern: List[int], repeat: int):
sys.stderr.write(f"[Vibrator] vibrate pattern: {pattern}, repeat={repeat}\n")
def vibrate(self, *args):
"""振动。支持 vibrate(milliseconds) 和 vibrate(pattern, repeat)。"""
if len(args) == 1 and isinstance(args[0], (int, float)):
milliseconds = int(args[0])
self._vibrating = True
if self._ff_device is None:
self._ff_device = self._find_ff_device()
if self._ff_device and self._ff_device.startswith("/sys/class/leds"):
try:
with open(_os.path.join(self._ff_device, "brightness"), "w") as f:
f.write("1")
import threading
def _stop():
_time.sleep(milliseconds / 1000.0)
try:
with open(_os.path.join(self._ff_device, "brightness"), "w") as f:
f.write("0")
except (OSError, IOError):
pass
self._vibrating = False
threading.Thread(target=_stop, daemon=True).start()
return
except (OSError, IOError):
pass
sys.stderr.write(f"[Vibrator] vibrate: {milliseconds}ms\n")
elif len(args) == 2 and isinstance(args[0], list):
pattern = args[0]
repeat = args[1]
if not pattern:
return
self._vibrating = True
import threading
def _run_pattern():
idx = 0
while True:
duration = pattern[idx % len(pattern)]
if idx % 2 == 0:
_time.sleep(duration / 1000.0)
else:
self.vibrate(duration)
_time.sleep(duration / 1000.0)
idx += 1
if repeat < 0 or idx >= len(pattern):
if repeat < 0:
continue
break
self._vibrating = False
threading.Thread(target=_run_pattern, daemon=True).start()
sys.stderr.write(f"[Vibrator] vibrate pattern: {pattern}, repeat={repeat}\n")
def cancel(self):
"""取消振动。"""
self._vibrating = False
if self._ff_device and self._ff_device.startswith("/sys/class/leds"):
try:
with open(_os.path.join(self._ff_device, "brightness"), "w") as f:
f.write("0")
except (OSError, IOError):
pass
sys.stderr.write("[Vibrator] cancel\n")
def hasVibrator(self) -> bool:
return True
"""检查是否有振动器硬件。"""
return self._find_ff_device() is not None
def hasAmplitudeControl(self) -> bool:
"""检查是否支持振幅控制。"""
try:
if _os.path.exists("/sys/class/leds/vibrator/max_brightness"):
with open("/sys/class/leds/vibrator/max_brightness", "r") as f:
return int(f.read().strip()) > 1
except (OSError, IOError, ValueError):
pass
return False
def areEffectsSupported(self, effect_ids: List[int]) -> List[int]:
@@ -1399,20 +1656,41 @@ class Vibrator(Stub):
class VibratorEffect(Stub):
"""VibratorEffect — 振动效果"""
EFFECT_CLICK = 0
EFFECT_DOUBLE_CLICK = 1
EFFECT_TICK = 2
EFFECT_THUD = 3
EFFECT_POP = 4
EFFECT_HEAVY_CLICK = 5
def __init__(self, effect_id: int = 0):
super().__init__()
self._effect_id = effect_id
self._timings: Optional[List[int]] = None
self._amplitudes: Optional[List[int]] = None
self._repeat = -1
@staticmethod
def createOneShot(milliseconds: int, amplitude: int) -> "VibratorEffect":
return VibratorEffect(0)
"""创建单次振动效果。"""
effect = VibratorEffect(0)
effect._timings = [0, milliseconds]
effect._amplitudes = [0, amplitude]
effect._repeat = -1
return effect
@staticmethod
def createWaveform(pattern: List[int], repeat: int) -> "VibratorEffect":
return VibratorEffect(0)
"""创建波形振动效果。"""
effect = VibratorEffect(0)
effect._timings = list(pattern)
effect._amplitudes = [255 if i % 2 == 1 else 0 for i in range(len(pattern))]
effect._repeat = repeat
return effect
@staticmethod
def createPredefined(effect_id: int) -> "VibratorEffect":
"""创建预定义振动效果。"""
return VibratorEffect(effect_id)
@@ -1478,8 +1756,58 @@ class BatteryManager(Stub):
self._current_now = -500000
self._current_avg = -450000
self._energy_counter = 15000000
if _USE_LINUX:
self._refresh_from_linux()
_LINUX_STATUS_MAP = {
"charging": 2, # BATTERY_STATUS_CHARGING
"discharging": 3, # BATTERY_STATUS_DISCHARGING
"not charging": 4, # BATTERY_STATUS_NOT_CHARGING
"full": 5, # BATTERY_STATUS_FULL
"unknown": 1, # BATTERY_STATUS_UNKNOWN
}
def _get_linux_battery_info(self) -> dict:
"""从 LinuxPower 获取真实电池信息"""
try:
return LinuxPower.get_battery_info() or {}
except Exception:
return {}
def _refresh_from_linux(self):
"""用 Linux 真实电池信息刷新内部状态"""
info = self._get_linux_battery_info()
if not info:
return
self._present = bool(info.get("present", False))
level = info.get("level", -1)
if self._present and isinstance(level, (int, float)) and level >= 0:
self._level = int(level)
self._capacity = int(level)
else:
# 无电池或电量未知
self._level = -1
self._capacity = -1
status_str = str(info.get("status", "unknown")).lower()
self._status = self._LINUX_STATUS_MAP.get(status_str, self.BATTERY_STATUS_UNKNOWN)
voltage = info.get("voltage", 0)
if isinstance(voltage, (int, float)):
# LinuxPower 返回伏特, Android 期望毫伏
self._voltage = int(voltage * 1000)
temperature = info.get("temperature", 0)
if isinstance(temperature, (int, float)):
# LinuxPower 返回摄氏度, Android 期望十分之一度
self._temperature = int(temperature * 10)
self._technology = info.get("technology", "") or "Li-ion"
# 充电状态决定 plugged
if self._status == self.BATTERY_STATUS_CHARGING:
self._plugged = self.BATTERY_PLUGGED_AC
else:
self._plugged = 0
def getIntProperty(self, prop_id: int) -> int:
if _USE_LINUX:
self._refresh_from_linux()
if prop_id == self.BATTERY_PROPERTY_CAPACITY:
return self._capacity
if prop_id == self.BATTERY_PROPERTY_STATUS:
@@ -1498,9 +1826,57 @@ class BatteryManager(Stub):
return self.getIntProperty(prop_id)
def isCharging(self) -> bool:
if _USE_LINUX:
self._refresh_from_linux()
return self._status == self.BATTERY_STATUS_CHARGING
def isPresent(self) -> bool:
"""电池是否存在"""
if _USE_LINUX:
self._refresh_from_linux()
return self._present
def getLevel(self) -> int:
"""电池电量 (0-100, -1 表示未知)"""
if _USE_LINUX:
self._refresh_from_linux()
return self._level
def getVoltage(self) -> int:
"""电池电压 (毫伏)"""
if _USE_LINUX:
self._refresh_from_linux()
return self._voltage
def getTemperature(self) -> int:
"""电池温度 (十分之一摄氏度)"""
if _USE_LINUX:
self._refresh_from_linux()
return self._temperature
def getTechnology(self) -> str:
"""电池技术"""
if _USE_LINUX:
self._refresh_from_linux()
return self._technology
def getPlugType(self) -> int:
"""充电类型"""
if _USE_LINUX:
self._refresh_from_linux()
return self._plugged
def getStatus(self) -> int:
"""电池状态"""
if _USE_LINUX:
self._refresh_from_linux()
return self._status
def computeChargeTimeRemaining(self) -> int:
if _USE_LINUX:
# 无电池或已充满时返回 -1
if not self._present or self._status == self.BATTERY_STATUS_FULL:
return -1
return 1800000 # 30 minutes
def toString(self) -> str:
@@ -1582,18 +1958,38 @@ class TokenWatcher(Stub):
def __init__(self, handler: Handler, tag: str):
super().__init__()
self._handler = handler
self._tag = tag
self._tokens: Dict[int, str] = {}
def acquire(self, token: Any, tag: str):
pass
"""获取令牌。"""
self._tokens[id(token)] = tag
if len(self._tokens) == 1:
self.acquired()
def release(self, token: Any):
pass
"""释放令牌。"""
self._tokens.pop(id(token), None)
if not self._tokens:
self.released()
def isAcquired(self) -> bool:
return False
"""检查是否有令牌被持有。"""
return len(self._tokens) > 0
def acquired(self):
"""当第一个令牌被获取时调用。子类可重写。"""
pass
def released(self):
"""当所有令牌被释放时调用。子类可重写。"""
pass
def dump(self):
pass
"""转储令牌信息。"""
for tid, tag in self._tokens.items():
sys.stderr.write(f"[TokenWatcher] {self._tag}: token={tid}, tag={tag}\n")
class WorkSource(Stub):
@@ -1744,42 +2140,157 @@ class FileObserver(Stub):
super().__init__()
self._path = path
self._watching = False
self._thread = None
self._snapshot: Dict[str, float] = {}
def startWatching(self):
"""开始监听文件变化。使用轮询方式检测文件系统事件。"""
if self._watching:
return
self._watching = True
self._snapshot = self._take_snapshot()
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
self._thread.start()
def _take_snapshot(self) -> Dict[str, float]:
"""获取当前目录的文件快照(文件名 -> 修改时间)。"""
snapshot: Dict[str, float] = {}
try:
if _os.path.isdir(self._path):
for name in _os.listdir(self._path):
full = _os.path.join(self._path, name)
try:
snapshot[name] = _os.path.getmtime(full)
except OSError:
pass
elif _os.path.exists(self._path):
try:
snapshot[self._path] = _os.path.getmtime(self._path)
except OSError:
pass
except OSError:
pass
return snapshot
def _poll_loop(self):
"""轮询检测文件变化并触发事件。"""
while self._watching:
_time.sleep(0.5)
try:
current = self._take_snapshot()
# 检测新建和修改
for name, mtime in current.items():
if name not in self._snapshot:
self.onEvent(FileObserver.CREATE, name)
elif mtime != self._snapshot[name]:
self.onEvent(FileObserver.MODIFY, name)
# 检测删除
for name in list(self._snapshot.keys()):
if name not in current:
self.onEvent(FileObserver.DELETE, name)
self._snapshot = current
except Exception:
pass
def stopWatching(self):
"""停止监听。"""
self._watching = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=1.0)
self._thread = None
def onEvent(self, event: int, path: Optional[str]):
"""文件事件回调,子类可重写。"""
pass
def finalize(self):
pass
"""清理资源。"""
self.stopWatching()
class FileUtils(Stub):
"""FileUtils — 文件工具"""
@staticmethod
def readFileToString(file: Any) -> str:
return ""
def readFileToString(path):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return ""
@staticmethod
def stringToFile(file: Any, string: str):
pass
def stringToFile(path, content):
try:
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return True
except Exception:
return False
@staticmethod
def copyFile(src: Any, dest: Any) -> bool:
return True
def copyFile(src, dst):
try:
import shutil
shutil.copy2(src, dst)
return True
except Exception:
return False
@staticmethod
def deleteDirectory(dir_: Any) -> bool:
return True
def deleteDirectory(path):
try:
import shutil
shutil.rmtree(path)
return True
except Exception:
return False
@staticmethod
def deleteContents(path):
try:
import shutil
for entry in _os.listdir(path):
full = _os.path.join(path, entry)
if _os.path.isdir(full):
shutil.rmtree(full)
else:
_os.remove(full)
return True
except Exception:
return False
@staticmethod
def deleteOlderFiles(dir_: Any, min_count: int, min_age_ms: int):
pass
"""删除目录中较旧的文件,但保留至少 min_count 个文件。
min_age_ms 为文件的最小年龄(毫秒),低于此年龄的文件不会被删除。"""
try:
if not _os.path.isdir(dir_):
return
entries = []
for name in _os.listdir(dir_):
full = _os.path.join(dir_, name)
if _os.path.isfile(full):
try:
mtime = _os.path.getmtime(full)
entries.append((full, mtime))
except OSError:
pass
# 按修改时间排序(最新在前)
entries.sort(key=lambda x: x[1], reverse=True)
now = _time.time()
min_age_s = min_age_ms / 1000.0
# 保留最新的 min_count 个文件,删除超过 min_age 的旧文件
for i, (path, mtime) in enumerate(entries):
if i < min_count:
continue
if (now - mtime) >= min_age_s:
try:
_os.remove(path)
except OSError:
pass
except Exception:
pass
class PatternMatcher(Stub):
+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 ..java.lang import RuntimeException
from .os import BatteryManager as _BaseBatteryManager
from .os import PowerManager as _BasePowerManager
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxPower, LinuxProcess, HAS_PSUTIL as _HAS_PSUTIL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
@@ -421,8 +430,8 @@ class BatteryStatsManager(Stub):
# BatteryManager (扩展)
# ═══════════════════════════════════════════════════════════════
class BatteryManager(Stub):
"""BatteryManager (扩展) — 电池管理器"""
class BatteryManager(_BaseBatteryManager):
"""BatteryManager (扩展) — 电池管理器 (继承 os.BatteryManager 的所有方法)"""
EXTRA_STATUS = "status"
EXTRA_HEALTH = "health"
@@ -505,6 +514,54 @@ class BatteryManager(Stub):
self._discharge_time_remaining = -1
self._is_capacity_estimated = False
self._capacity_percentage = 100.0
if _USE_LINUX:
self._refresh_from_linux()
_LINUX_STATUS_MAP = {
"charging": BATTERY_STATUS_CHARGING,
"discharging": BATTERY_STATUS_DISCHARGING,
"not charging": BATTERY_STATUS_NOT_CHARGING,
"full": BATTERY_STATUS_FULL,
"unknown": BATTERY_STATUS_UNKNOWN,
}
def _get_linux_battery_info(self) -> dict:
"""从 LinuxPower 获取真实电池信息"""
try:
return LinuxPower.get_battery_info() or {}
except Exception:
return {}
def _refresh_from_linux(self):
"""用 Linux 真实电池信息刷新内部状态"""
info = self._get_linux_battery_info()
if not info:
return
self._present = bool(info.get("present", False))
level = info.get("level", -1)
if self._present and isinstance(level, (int, float)) and level >= 0:
self._battery_level = int(level)
self._capacity_percentage = float(level)
# BATTERY_PROPERTY_CAPACITY 在 Android 中返回百分比 (0-100)
self._capacity = int(level)
else:
# 无电池或电量未知
self._battery_level = -1
self._capacity_percentage = 0.0
self._capacity = -1
status_str = str(info.get("status", "unknown")).lower()
self._status = self._LINUX_STATUS_MAP.get(status_str, BATTERY_STATUS_UNKNOWN)
voltage = info.get("voltage", 0)
if isinstance(voltage, (int, float)):
self._voltage = int(voltage * 1000) # V -> mV
temperature = info.get("temperature", 0)
if isinstance(temperature, (int, float)):
self._temperature = temperature # 摄氏度
self._technology = info.get("technology", "") or "Li-ion"
if self._status == BATTERY_STATUS_CHARGING:
self._plugged = BATTERY_PLUGGED_AC
else:
self._plugged = 0
@staticmethod
def getInstance(context: Any) -> "BatteryManager":
@@ -513,6 +570,8 @@ class BatteryManager(Stub):
return BatteryManager._instance
def getIntProperty(self, id: int) -> int:
if _USE_LINUX:
self._refresh_from_linux()
properties = {
BATTERY_PROPERTY_CAPACITY: self._capacity,
BATTERY_PROPERTY_CHARGE_COUNTER: self._charge_counter,
@@ -521,6 +580,9 @@ class BatteryManager(Stub):
BATTERY_PROPERTY_ENERGY_COUNTER: self._energy_counter,
BATTERY_PROPERTY_CHARGE_TIME_REMAINING: self._charge_time_remaining,
BATTERY_PROPERTY_DISCHARGE_TIME_REMAINING: self._discharge_time_remaining,
BATTERY_PROPERTY_STATUS: self._status,
BATTERY_PROPERTY_TEMPERATURE: int(self._temperature),
BATTERY_PROPERTY_VOLTAGE: self._voltage,
}
return properties.get(id, 0)
@@ -528,9 +590,13 @@ class BatteryManager(Stub):
return self.getIntProperty(id)
def isCharging(self) -> bool:
if _USE_LINUX:
self._refresh_from_linux()
return self._status == BATTERY_STATUS_CHARGING or self._status == BATTERY_STATUS_FULL
def getBatteryLevel(self) -> int:
if _USE_LINUX:
self._refresh_from_linux()
return self._battery_level
def getInt(intent: Any, key: str, defaultValue: int) -> int:
@@ -617,8 +683,8 @@ class BatterySaverController(Stub):
# PowerManager (扩展)
# ═══════════════════════════════════════════════════════════════
class PowerManager(Stub):
"""PowerManager — 电源管理器 (扩展)"""
class PowerManager(_BasePowerManager):
"""PowerManager — 电源管理器 (扩展,继承 os.PowerManager 的所有方法)"""
PARTIAL_WAKE_LOCK = PARTIAL_WAKE_LOCK
FULL_WAKE_LOCK = FULL_WAKE_LOCK
@@ -687,9 +753,11 @@ class PowerManager(Stub):
self._shutdown_requested = False
self._thermal_status = THERMAL_STATUS_NONE
self._power_save_state = PowerSaveState()
self._current_wake_up_reason = WAKE_REASON_UNKNOWN
self._current_wake_up_reason = PowerManager.WAKE_REASON_UNKNOWN
def isInteractive(self) -> bool:
if _USE_LINUX:
return self.isScreenOn()
return self._is_interactive
def isPowerSaveMode(self) -> bool:
@@ -705,6 +773,13 @@ class PowerManager(Stub):
return True
def isScreenOn(self) -> bool:
if _USE_LINUX:
import os as _os_mod
display = _os_mod.environ.get("DISPLAY", "")
if display:
return True
wayland = _os_mod.environ.get("WAYLAND_DISPLAY", "")
return bool(wayland)
return self._is_screen_on
def isWakeLockLevelSupported(self, level: int) -> bool:
+138
View File
@@ -108,6 +108,25 @@ class _SettingsBase(Stub):
data = _SettingsBase._load(name)
return data.get(key, default_value)
@staticmethod
def putBoolean(name: str, key: str, value: bool) -> bool:
data = _SettingsBase._load(name)
data[key] = bool(value)
_SettingsBase._save(name)
return True
@staticmethod
def getBoolean(name: str, key: str, default_value: bool = False) -> bool:
data = _SettingsBase._load(name)
val = data.get(key, default_value)
if isinstance(val, bool):
return val
if isinstance(val, str):
return val.lower() in ("true", "1", "yes")
if isinstance(val, (int, float)):
return bool(val)
return default_value
class SettingsSystem(_SettingsBase):
_NAME = "settings_system"
@@ -144,6 +163,14 @@ class SettingsSystem(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsSystem._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsSystem._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsSystem._NAME, key, default_value)
class SettingsSecure(_SettingsBase):
_NAME = "settings_secure"
@@ -180,6 +207,14 @@ class SettingsSecure(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsSecure._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsSecure._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsSecure._NAME, key, default_value)
class SettingsGlobal(_SettingsBase):
_NAME = "settings_global"
@@ -216,6 +251,14 @@ class SettingsGlobal(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsGlobal._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsGlobal._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsGlobal._NAME, key, default_value)
class Settings(Stub):
"""Settings — 系统设置"""
@@ -236,6 +279,10 @@ class Settings(Stub):
class MediaStore(Stub):
"""MediaStore — 媒体存储"""
VOLUME_INTERNAL = "internal"
VOLUME_EXTERNAL = "external"
VOLUME_EXTERNAL_PRIMARY = "external_primary"
class MediaColumns(Stub):
_ID = 0
DATA = 1
@@ -259,16 +306,35 @@ class MediaStore(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/images/media"
INTERNAL_CONTENT_URI = "content://media/internal/images/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/images/media"
class Thumbnails(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/images/thumbnails"
class Video(Stub):
class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/video/media"
INTERNAL_CONTENT_URI = "content://media/internal/video/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/video/media"
class Audio(Stub):
class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/audio/media"
INTERNAL_CONTENT_URI = "content://media/internal/audio/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/audio/media"
@staticmethod
def getMediaUri(volume_name: str) -> str:
"""返回指定卷的媒体 URI。"""
return f"content://media/{volume_name}"
@staticmethod
def getVersion(context: Any, uri: str) -> str:
@@ -337,22 +403,94 @@ class DocumentsContract(Stub):
class ContactsContract(Stub):
"""ContactsContract — 联系人合约"""
AUTHORITY = "com.android.contacts"
class Contacts(Stub):
CONTENT_URI = "content://com.android.contacts/contacts"
CONTENT_FILTER_URI = "content://com.android.contacts/contacts/filter"
CONTENT_TYPE = "vnd.android.cursor.dir/contact"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact"
DISPLAY_NAME = "display_name"
PHOTO_URI = "photo_uri"
PHOTO_ID = "photo_id"
_ID = "_id"
HAS_PHONE_NUMBER = "has_phone_number"
LOOKUP_KEY = "lookup"
STARRED = "starred"
class CommonDataKinds(Stub):
class Email(Stub):
CONTENT_URI = "content://com.android.contacts/data/emails"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/email_v2"
ADDRESS = "data1"
DISPLAY_NAME = "display_name"
TYPE = "data2"
TYPE_HOME = 1
TYPE_WORK = 2
TYPE_OTHER = 3
TYPE_MOBILE = 4
class Phone(Stub):
CONTENT_URI = "content://com.android.contacts/data/phones"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone_v2"
NUMBER = "data1"
DISPLAY_NAME = "display_name"
TYPE = "data2"
TYPE_HOME = 1
TYPE_MOBILE = 2
TYPE_WORK = 3
TYPE_FAX_WORK = 4
TYPE_FAX_HOME = 5
TYPE_PAGER = 6
TYPE_OTHER = 7
TYPE_CALLBACK = 8
TYPE_CAR = 9
TYPE_COMPANY_MAIN = 10
TYPE_ISDN = 11
TYPE_MAIN = 12
TYPE_OTHER_FAX = 13
TYPE_RADIO = 14
TYPE_TELEX = 15
TYPE_TTY_TDD = 16
TYPE_WORK_MOBILE = 17
TYPE_WORK_PAGER = 18
TYPE_ASSISTANT = 19
class StructuredName(Stub):
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/name"
DISPLAY_NAME = "data1"
GIVEN_NAME = "data2"
FAMILY_NAME = "data3"
PREFIX = "data4"
MIDDLE_NAME = "data5"
SUFFIX = "data6"
class Organization(Stub):
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/organization"
COMPANY = "data1"
TYPE = "data2"
TITLE = "data4"
TYPE_WORK = 1
TYPE_OTHER = 2
class Data(Stub):
CONTENT_URI = "content://com.android.contacts/data"
CONTENT_TYPE = "vnd.android.cursor.dir/data"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/data"
MIMETYPE = "mimetype"
_ID = "_id"
DISPLAY_NAME = "display_name"
RAW_CONTACT_ID = "raw_contact_id"
CONTACT_ID = "contact_id"
class RawContacts(Stub):
CONTENT_URI = "content://com.android.contacts/raw_contacts"
CONTENT_TYPE = "vnd.android.cursor.dir/raw_contact"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/raw_contact"
_ID = "_id"
CONTACT_ID = "contact_id"
ACCOUNT_NAME = "account_name"
ACCOUNT_TYPE = "account_type"
class ContactsContractContacts(ContactsContract.Contacts):
+37 -37
View File
@@ -80,7 +80,7 @@ class OsConstants(Stub):
SIGTTIN = 21
SIGTTOU = 22
# errno 常量
# errno 常量 (使用 getattr 处理 Windows 上不存在的常量)
EPERM = errno.EPERM
ENOENT = errno.ENOENT
ESRCH = errno.ESRCH
@@ -95,7 +95,7 @@ class OsConstants(Stub):
ENOMEM = errno.ENOMEM
EACCES = errno.EACCES
EFAULT = errno.EFAULT
ENOTBLK = errno.ENOTBLK
ENOTBLK = getattr(errno, 'ENOTBLK', 15) # Windows 不支持
EBUSY = errno.EBUSY
EEXIST = errno.EEXIST
EXDEV = errno.EXDEV
@@ -106,7 +106,7 @@ class OsConstants(Stub):
ENFILE = errno.ENFILE
EMFILE = errno.EMFILE
ENOTTY = errno.ENOTTY
ETXTBSY = errno.ETXTBSY
ETXTBSY = getattr(errno, 'ETXTBSY', 139)
EFBIG = errno.EFBIG
ENOSPC = errno.ENOSPC
ESPIPE = errno.ESPIPE
@@ -123,16 +123,16 @@ class OsConstants(Stub):
ELOOP = errno.ELOOP
ENOMSG = errno.ENOMSG
EIDRM = errno.EIDRM
ENOSTR = errno.ENOSTR
ENODATA = errno.ENODATA
ETIME = errno.ETIME
ENOSR = errno.ENOSR
EREMOTE = errno.EREMOTE
ENOLINK = errno.ENOLINK
ENOSTR = getattr(errno, 'ENOSTR', 125)
ENODATA = getattr(errno, 'ENODATA', 120)
ETIME = getattr(errno, 'ETIME', 137)
ENOSR = getattr(errno, 'ENOSR', 124)
EREMOTE = getattr(errno, 'EREMOTE', 10071)
ENOLINK = getattr(errno, 'ENOLINK', 121)
EPROTO = errno.EPROTO
EMULTIHOP = errno.EMULTIHOP
EBADMSG = errno.EBADMSG
EOVERFLOW = errno.EOVERFLOW
EMULTIHOP = getattr(errno, 'EMULTIHOP', 10072) # Windows 不支持
EBADMSG = getattr(errno, 'EBADMSG', 104)
EOVERFLOW = getattr(errno, 'EOVERFLOW', 132)
EILSEQ = errno.EILSEQ
ENOTSOCK = errno.ENOTSOCK
EDESTADDRREQ = errno.EDESTADDRREQ
@@ -140,10 +140,10 @@ class OsConstants(Stub):
EPROTOTYPE = errno.EPROTOTYPE
ENOPROTOOPT = errno.ENOPROTOOPT
EPROTONOSUPPORT = errno.EPROTONOSUPPORT
ESOCKTNOSUPPORT = errno.ESOCKTNOSUPPORT
EOPNOTSUPP = errno.EOPNOTSUPP
EPFNOSUPPORT = errno.EPFNOSUPPORT
EAFNOSUPPORT = errno.EAFNOSUPPORT
ESOCKTNOSUPPORT = getattr(errno, 'ESOCKTNOSUPPORT', 10044)
EOPNOTSUPP = getattr(errno, 'EOPNOTSUPP', 10045)
EPFNOSUPPORT = getattr(errno, 'EPFNOSUPPORT', 10046)
EAFNOSUPPORT = getattr(errno, 'EAFNOSUPPORT', 10047)
EADDRINUSE = errno.EADDRINUSE
EADDRNOTAVAIL = errno.EADDRNOTAVAIL
ENETDOWN = errno.ENETDOWN
@@ -154,32 +154,32 @@ class OsConstants(Stub):
ENOBUFS = errno.ENOBUFS
EISCONN = errno.EISCONN
ENOTCONN = errno.ENOTCONN
ESHUTDOWN = errno.ESHUTDOWN
ETOOMANYREFS = errno.ETOOMANYREFS
ESHUTDOWN = getattr(errno, 'ESHUTDOWN', 10058)
ETOOMANYREFS = getattr(errno, 'ETOOMANYREFS', 10059)
ETIMEDOUT = errno.ETIMEDOUT
ECONNREFUSED = errno.ECONNREFUSED
EHOSTDOWN = errno.EHOSTDOWN
EHOSTDOWN = getattr(errno, 'EHOSTDOWN', 10064)
EHOSTUNREACH = errno.EHOSTUNREACH
EALREADY = errno.EALREADY
EINPROGRESS = errno.EINPROGRESS
ESTALE = errno.ESTALE
EUCLEAN = errno.EUCLEAN
ENOTNAM = errno.ENOTNAM
ENAVAIL = errno.ENAVAIL
EISNAM = errno.EISNAM
EREMOTEIO = errno.EREMOTEIO
EDQUOT = errno.EDQUOT
ENOMEDIUM = errno.ENOMEDIUM
EMEDIUMTYPE = errno.EMEDIUMTYPE
ECANCELED = errno.ECANCELED
ENOKEY = errno.ENOKEY
EKEYEXPIRED = errno.EKEYEXPIRED
EKEYREVOKED = errno.EKEYREVOKED
EKEYREJECTED = errno.EKEYREJECTED
EOWNERDEAD = errno.EOWNERDEAD
ENOTRECOVERABLE = errno.ENOTRECOVERABLE
ERFKILL = errno.ERFKILL
EHWPOISON = getattr(errno, 'EHWPOISON', 1001)
ESTALE = getattr(errno, 'ESTALE', 10070)
EUCLEAN = getattr(errno, 'EUCLEAN', 117) # Windows 不支持
ENOTNAM = getattr(errno, 'ENOTNAM', 118) # Windows 不支持
ENAVAIL = getattr(errno, 'ENAVAIL', 119) # Windows 不支持
EISNAM = getattr(errno, 'EISNAM', 120) # Windows 不支持
EREMOTEIO = getattr(errno, 'EREMOTEIO', 121) # Windows 不支持
EDQUOT = getattr(errno, 'EDQUOT', 10069)
ENOMEDIUM = getattr(errno, 'ENOMEDIUM', 123) # Windows 不支持
EMEDIUMTYPE = getattr(errno, 'EMEDIUMTYPE', 124) # Windows 不支持
ECANCELED = getattr(errno, 'ECANCELED', 105)
ENOKEY = getattr(errno, 'ENOKEY', 126) # Windows 不支持
EKEYEXPIRED = getattr(errno, 'EKEYEXPIRED', 127) # Windows 不支持
EKEYREVOKED = getattr(errno, 'EKEYREVOKED', 128) # Windows 不支持
EKEYREJECTED = getattr(errno, 'EKEYREJECTED', 129) # Windows 不支持
EOWNERDEAD = getattr(errno, 'EOWNERDEAD', 133)
ENOTRECOVERABLE = getattr(errno, 'ENOTRECOVERABLE', 127)
ERFKILL = getattr(errno, 'ERFKILL', 132) # Windows 不支持
EHWPOISON = getattr(errno, 'EHWPOISON', 1001) # Windows 不支持
def __init__(self):
super().__init__()
+35 -2
View File
@@ -14,6 +14,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, LinuxProcess, LinuxLocation
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# CellIdentity 基类集合
@@ -553,6 +559,9 @@ class TelephonyManager(Stub):
self._context = context
def getNetworkOperatorName(self) -> str:
if _USE_LINUX:
loc = LinuxLocation.get_location()
return loc.get("isp", "") or ""
return "MockOperator"
def getNetworkOperator(self) -> str:
@@ -562,9 +571,15 @@ class TelephonyManager(Stub):
return "us"
def getNetworkType(self) -> int:
if _USE_LINUX:
if LinuxNetwork.get_connectivity():
return self.NETWORK_TYPE_IWLAN
return self.NETWORK_TYPE_UNKNOWN
return self.NETWORK_TYPE_LTE
def getPhoneType(self) -> int:
if _USE_LINUX:
return self.PHONE_TYPE_NONE
return self.PHONE_TYPE_GSM
def getDataNetworkType(self) -> int:
@@ -574,6 +589,8 @@ class TelephonyManager(Stub):
return self.NETWORK_TYPE_LTE
def getSimOperator(self) -> str:
if _USE_LINUX:
return ""
return "310260"
def getSimOperatorName(self) -> str:
@@ -583,18 +600,29 @@ class TelephonyManager(Stub):
return "us"
def getSimSerialNumber(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "89014103211118510720"
def getSubscriberId(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "310260123456789"
def getDeviceId(self) -> Optional[str]:
if _USE_LINUX:
return LinuxNetwork.get_mac_address() or None
return "000000000000000"
def getDeviceSoftwareVersion(self) -> Optional[str]:
if _USE_LINUX:
info = LinuxProcess.get_system_info()
return info.get("kernel", "")
return "00.00.00.00"
def getLine1Number(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "15551234567"
def getGroupIdLevel1(self) -> Optional[str]:
@@ -619,6 +647,9 @@ class TelephonyManager(Stub):
return self.SIM_STATE_READY
def getCellLocation(self) -> Any:
# 桌面无基站,Linux 下返回 None
if _USE_LINUX:
return None
return None
def getAllCellInfo(self) -> Optional[List[CellInfo]]:
@@ -630,8 +661,10 @@ class TelephonyManager(Stub):
def getMmsUAProfUrl(self) -> str:
return "http://www.example.com/uaprof.xml"
def listen(self, listener: PhoneStateListener, events: int):
pass
def listen(self, listener, events):
if not hasattr(self, '_listeners'):
self._listeners = []
self._listeners.append((listener, events))
def getDataState(self) -> int:
return self.DATA_CONNECTED
+80 -19
View File
@@ -7,6 +7,7 @@ import html as _html
import re as _re
import sys
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
from urllib.parse import quote_plus as _quote_plus, unquote_plus as _unquote_plus
from .._core import (
Stub, AndroidOnlyError, android_context,
@@ -176,7 +177,9 @@ class TextUtils(Stub):
@staticmethod
def split(text: str, delimiter: str) -> List[str]:
return text.split(delimiter)
if text is None or len(text) == 0:
return []
return _re.split(delimiter, text)
@staticmethod
def htmlEncode(s: str) -> str:
@@ -186,6 +189,48 @@ class TextUtils(Stub):
def htmlDecode(s: str) -> str:
return _html.unescape(s)
@staticmethod
def concat(*texts: Any) -> str:
parts = []
for t in texts:
if t is not None:
parts.append(str(t))
return "".join(parts)
@staticmethod
def indexOf(s: Any, needle: Any, start: int = 0) -> int:
if s is None:
return -1
s = str(s)
needle = str(needle)
if start < 0:
start = 0
return s.find(needle, start)
@staticmethod
def lastIndexOf(s: Any, needle: Any, last: Optional[int] = None) -> int:
if s is None:
return -1
s = str(s)
needle = str(needle)
if last is None:
return s.rfind(needle)
if last < 0:
return -1
return s.rfind(needle, 0, last + 1)
@staticmethod
def urlEncode(s: str) -> str:
if s is None:
return ""
return _quote_plus(str(s))
@staticmethod
def urlDecode(s: str) -> str:
if s is None:
return ""
return _unquote_plus(str(s))
@staticmethod
def getReverse(s: str, start: int, end: int) -> str:
return s[start:end][::-1]
@@ -227,8 +272,13 @@ class TextUtils(Stub):
return text[:max_chars] + ellipsis
@staticmethod
def isDigitsOnly(s: str) -> bool:
return s.isdigit() if s else False
def isDigitsOnly(s: Any) -> bool:
if s is None:
return False
s = str(s)
if len(s) == 0:
return False
return s.isdigit()
@staticmethod
def isGraphical(s: str) -> bool:
@@ -293,12 +343,12 @@ class Spanned(Stub):
def __init__(self, source: str = ""):
super().__init__()
self._source = source
self._spans: Dict[Tuple[type, int], Tuple[int, int, Any]] = {} # (type, id) -> (start, end, span_obj)
self._spans: Dict[Tuple[type, int], Tuple[int, int, int, Any]] = {} # (type, id) -> (start, end, flags, span_obj)
def setSpan(self, span_obj: Any, start: int, end: int, flags: int):
span_id = id(span_obj)
span_type = type(span_obj)
self._spans[(span_type, span_id)] = (start, end, span_obj)
self._spans[(span_type, span_id)] = (start, end, flags, span_obj)
def removeSpan(self, span_obj: Any):
span_id = id(span_obj)
@@ -306,12 +356,13 @@ class Spanned(Stub):
self._spans.pop((span_type, span_id), None)
def getSpans(self, start: int, end: int, span_type: Optional[type] = None) -> List[Any]:
result = []
for (st, sid), (s, e, obj) in self._spans.items():
collected = []
for (st, sid), (s, e, flags, obj) in self._spans.items():
if span_type is None or st == span_type:
if s < end and e > start:
result.append(obj)
return result
collected.append((s, obj))
collected.sort(key=lambda t: t[0])
return [obj for _, obj in collected]
def getSpanStart(self, span_obj: Any) -> int:
span_id = id(span_obj)
@@ -329,10 +380,18 @@ class Spanned(Stub):
span_id = id(span_obj)
span_type = type(span_obj)
data = self._spans.get((span_type, span_id))
return 0 if data is None else 0
return data[2] if data else 0
def nextSpanTransition(self, start: int, end: int, span_type: Optional[type] = None) -> int:
return end
result = end
for (st, sid), (s, e, flags, obj) in self._spans.items():
if span_type is not None and st != span_type:
continue
if start < s < result:
result = s
if start < e < result:
result = e
return result
def toString(self) -> str:
return self._source
@@ -435,15 +494,17 @@ class SpannableStringBuilder(Editable):
else:
super().__init__(str(source))
def append(self, text: Any) -> "SpannableStringBuilder":
super().append(text)
return self
def append(self, text: Any, start: int, end: int) -> "SpannableStringBuilder":
if isinstance(text, str):
self._source += text[start:end]
def append(self, text: Any, start: Optional[int] = None,
end: Optional[int] = None) -> "SpannableStringBuilder":
if text is None:
return self
if start is None and end is None:
self._source += str(text)
else:
self._source += str(text)[start:end]
s = 0 if start is None else start
text_str = str(text)
e = len(text_str) if end is None else end
self._source += text_str[s:e]
return self
def insert(self, where: int, text: Any) -> "SpannableStringBuilder":
+74 -18
View File
@@ -5,6 +5,7 @@ Android Util 组件 — Log, SparseArray, LruCache, DisplayMetrics 等
import math
import re as _re
import struct
import sys
from datetime import datetime
from typing import Any, Callable, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union
@@ -51,19 +52,44 @@ class Log(Stub):
return 0
@staticmethod
def w(tag: str, msg: str) -> int:
def w(tag: str, msg: str = None, tr: Exception = None) -> int:
if Log._min_log_level <= Log.WARN:
# 支持 w(tag, Throwable) 调用形式
if msg is None and tr is not None:
msg = Log.getStackTraceString(tr)
elif tr is not None:
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
sys.stderr.write(f"W/{tag}: {msg}\n")
return 0
@staticmethod
def e(tag: str, msg: str) -> int:
def e(tag: str, msg: str = None, tr: Exception = None) -> int:
if Log._min_log_level <= Log.ERROR:
# 支持 e(tag, Throwable) 调用形式
if msg is None and tr is not None:
msg = Log.getStackTraceString(tr)
elif tr is not None:
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
sys.stderr.write(f"E/{tag}: {msg}\n")
return 0
@staticmethod
def wtf(tag: str, msg: str) -> int:
def wtf(tag: str, msg=None, tr: Exception = None) -> int:
"""What a Terrible Failure。支持以下调用形式:
- wtf(tag, msg)
- wtf(tag, Throwable)
- wtf(tag, msg, Throwable)
"""
# 如果 msg 是异常对象,则当作 tr 处理
if isinstance(msg, BaseException):
tr = msg
msg = None
if msg is None and tr is not None:
msg = Log.getStackTraceString(tr)
elif tr is not None:
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
if msg is None:
msg = ""
sys.stderr.write(f"WTF/{tag}: {msg}\n")
return 0
@@ -88,17 +114,16 @@ class Log(Stub):
@staticmethod
def getStackTraceString(tr: Exception) -> str:
import traceback
if tr is None:
return ""
if isinstance(tr, str):
return tr
return "".join(traceback.format_exception(type(tr), tr, tr.__traceback__))
@staticmethod
def setMinLogLevel(level: int):
Log._min_log_level = level
@staticmethod
def wtf(tag: str, tr: Exception) -> int:
sys.stderr.write(f"WTF/{tag}: {Log.getStackTraceString(tr)}\n")
return 0
# ═══════════════════════════════════════════════════════════════
# SparseArray
@@ -518,6 +543,23 @@ class TypedValue(Stub):
COMPLEX_UNIT_FRACTION = 6
COMPLEX_UNIT_FRACTION_PARENT = 7
COMPLEX_UNIT_SHIFT = 0
COMPLEX_UNIT_MASK = 0xF
COMPLEX_RADIX_SHIFT = 4
COMPLEX_RADIX_MASK = 0x3
COMPLEX_MANTISSA_SHIFT = 8
COMPLEX_MANTISSA_MASK = 0x00ffffff
# 复数值的基数乘数(与 AOSP TypedValue.RADIX_MULTS 一致)
_RADIX_MULTS = (
0.00390625, # 1/256, radix 0
3.0517578e-5, # 1/32768, radix 1
1.1920929e-7, # 1/8388608, radix 2
4.6566129e-10, # 1/2147483648, radix 3
)
TYPE_NULL = 0x00
TYPE_REFERENCE = 0x01
TYPE_ATTRIBUTE = 0x02
@@ -536,6 +578,9 @@ class TypedValue(Stub):
TYPE_LAST_COLOR_INT = 0x1F
TYPE_LAST_INT = 0x1F
DATA_NULL_UNDEFINED = 0
DATA_NULL_EMPTY = 1
def __init__(self):
super().__init__()
self.type = self.TYPE_NULL
@@ -564,18 +609,27 @@ class TypedValue(Stub):
@staticmethod
def complexToDimension(data: int, metrics: DisplayMetrics) -> float:
unit = data & 0xF
value = (data >> 4) & 0xFFFFFF
# Sign-extend
if value & 0x800000:
value = value - 0x1000000
return TypedValue.applyDimension(unit, float(value), metrics)
"""将复数维度值转换为像素值。
复数值格式:bits 0-3=unit, bits 4-5=radix, bits 8-31=mantissa(有符号24位)"""
unit = (data >> TypedValue.COMPLEX_UNIT_SHIFT) & TypedValue.COMPLEX_UNIT_MASK
radix = (data >> TypedValue.COMPLEX_RADIX_SHIFT) & TypedValue.COMPLEX_RADIX_MASK
mantissa = (data >> TypedValue.COMPLEX_MANTISSA_SHIFT) & TypedValue.COMPLEX_MANTISSA_MASK
# 24 位有符号数符号扩展
if mantissa & 0x800000:
mantissa = mantissa - 0x1000000
value = mantissa * TypedValue._RADIX_MULTS[radix]
return TypedValue.applyDimension(unit, value, metrics)
@staticmethod
def complexToFloat(data: int) -> float:
value = (data & 0xFFFFFF00) >> 8
exp = (data & 0xFF) - 150
return float(value) * (10.0 ** exp)
"""将复数值转换为浮点数(与 AOSP complexToFloat 一致)。"""
radix = (data >> TypedValue.COMPLEX_RADIX_SHIFT) & TypedValue.COMPLEX_RADIX_MASK
# mantissa 保持在原始位位置(左移 8 位),与 AOSP 一致
mantissa = data & (TypedValue.COMPLEX_MANTISSA_MASK << TypedValue.COMPLEX_MANTISSA_SHIFT)
# 处理符号(Java int 是有符号的,bit 31 为符号位)
if mantissa & 0x80000000:
mantissa = mantissa - 0x100000000
return float(mantissa) * TypedValue._RADIX_MULTS[radix]
@staticmethod
def complexToDimensionPixelOffset(data: int, metrics: DisplayMetrics) -> int:
@@ -589,7 +643,9 @@ class TypedValue(Stub):
return int(val - 0.5)
def getFloat(self) -> float:
return float(self.data)
"""将 data 字段按 IEEE 754 单精度浮点数解读(与 Float.intBitsToFloat 一致)。"""
bits = self.data & 0xFFFFFFFF
return struct.unpack('f', struct.pack('I', bits))[0]
def toString(self) -> str:
return f"TypedValue(type={self.type}, data={self.data})"
+250 -11
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
try:
from .._linux_impl import IS_LINUX, LinuxDisplay, LinuxUIAutomation, HAS_XDOTOOL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Gravity
@@ -363,6 +369,12 @@ class View(Stub):
def performClick(self) -> bool:
return self.callOnClick()
def screenshot(self, path=None):
"""截屏"""
if _USE_LINUX:
return LinuxDisplay.screenshot(path)
return None
def performLongClick(self) -> bool:
if self._on_long_click_listener:
self._on_long_click_listener(self)
@@ -926,6 +938,106 @@ class View(Stub):
def onApplyWindowInsets(self, insets):
return insets
def setOnCreateContextMenuListener(self, listener):
self._on_create_context_menu_listener = listener
return self
def getBaseline(self):
return -1
def setFocusableInTouchMode(self, focusable):
self._focusable_in_touch_mode = focusable
def isFocusableInTouchMode(self):
return getattr(self, '_focusable_in_touch_mode', False)
def setRotationX(self, rotationX):
self._rotation_x = rotationX
def getRotationX(self):
return getattr(self, '_rotation_x', 0.0)
def setRotationY(self, rotationY):
self._rotation_y = rotationY
def getRotationY(self):
return getattr(self, '_rotation_y', 0.0)
def setCameraDistance(self, distance):
self._camera_distance = distance
def getCameraDistance(self):
return getattr(self, '_camera_distance', 0.0)
def setForeground(self, foreground):
self._foreground = foreground
def getForeground(self):
return getattr(self, '_foreground', None)
def setBackgroundTintList(self, tint):
self._background_tint = tint
def setBackgroundTintMode(self, tintMode):
self._background_tint_mode = tintMode
def setScrollbarFadingEnabled(self, fadeScrollbars):
self._fade_scrollbars = fadeScrollbars
def isScrollbarFadingEnabled(self):
return getattr(self, '_fade_scrollbars', True)
def setVerticalScrollBarEnabled(self, enabled):
self._vertical_scrollbar = enabled
def isVerticalScrollBarEnabled(self):
return getattr(self, '_vertical_scrollbar', True)
def setHorizontalScrollBarEnabled(self, enabled):
self._horizontal_scrollbar = enabled
def isHorizontalScrollBarEnabled(self):
return getattr(self, '_horizontal_scrollbar', True)
def setLabelFor(self, id):
self._label_for = id
def getLabelFor(self):
return getattr(self, '_label_for', 0)
def cancelLongPress(self):
pass
def setPaddingRelative(self, start, top, end, bottom):
self._padding_start = start
self._padding_top = top
self._padding_end = end
self._padding_bottom = bottom
def setSoundEffectsEnabled(self, soundEffectsEnabled):
self._sound_effects = soundEffectsEnabled
def isSoundEffectsEnabled(self):
return getattr(self, '_sound_effects', True)
def setHapticFeedbackEnabled(self, hapticFeedbackEnabled):
self._haptic_feedback = hapticFeedbackEnabled
def isHapticFeedbackEnabled(self):
return getattr(self, '_haptic_feedback', True)
def refreshDrawableState(self):
pass
def getDrawableState(self):
return []
def requestRectangleOnScreen(self, rectangle):
return False
def setAllCaps(self, allCaps):
self._all_caps = allCaps
class MeasureSpec:
"""MeasureSpec — 测量规格包装"""
@@ -1130,8 +1242,13 @@ class ViewGroup(View, ViewParent, ViewManager):
# ─── child management ──────────────────────────────────────
def addView(self, child: View, index: int = -1,
def addView(self, child: View, index: Optional[int] = -1,
params: Optional["ViewGroup.LayoutParams"] = None):
"""addView — 支持 addView(child), addView(child, params), addView(child, index), addView(child, index, params)"""
# 智能 参数分派:第二个参数可能是 index(int) 或 params(LayoutParams)
if isinstance(index, ViewGroup.LayoutParams):
params = index
index = -1
if params is None:
params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -1143,9 +1260,6 @@ class ViewGroup(View, ViewParent, ViewManager):
child.setLayoutParams(params)
child.onAttachedToWindow()
def addView(self, child: View, params: Optional["ViewGroup.LayoutParams"] = None):
self.addView(child, -1, params)
def removeView(self, view: View):
if view in self._children:
self._children.remove(view)
@@ -1248,6 +1362,32 @@ class ViewGroup(View, ViewParent, ViewManager):
def isMotionEventSplittingEnabled(self) -> bool:
return True
# ─── layout transition ────────────────────────────────────
def setLayoutTransition(self, transition: Any):
self._layout_transition = transition
def getLayoutTransition(self) -> Any:
return getattr(self, '_layout_transition', None)
# ─── focus / touch blocking ───────────────────────────────
def setTouchscreenBlocksFocus(self, blocks: bool):
self._touchscreen_blocks_focus = blocks
def isTouchscreenBlocksFocus(self) -> bool:
return getattr(self, '_touchscreen_blocks_focus', False)
# ─── child visible rect ───────────────────────────────────
def getChildVisibleRect(self, child: View, r: Any, offset: Any) -> bool:
if r is not None and hasattr(r, 'set'):
r.set(child.getLeft(), child.getTop(),
child.getRight(), child.getBottom())
if offset is not None and hasattr(offset, 'set'):
offset.set(0, 0)
return True
def setTransitionGroup(self, is_transition_group: bool):
pass
@@ -1697,6 +1837,75 @@ class MotionEvent(Stub):
def getHistoricalSize(self, pos: int, pointer: int = 0) -> float:
return 0.5
# ─── historical coordinates ───────────────────────────────
def getHistorySize(self) -> int:
return len(getattr(self, '_history', []))
def getHistoricalX(self, pointer_index: int = 0, pos: int = 0) -> float:
history = getattr(self, '_history', [])
if 0 <= pos < len(history):
return history[pos].get('x', self.getX(pointer_index))
return self.getX(pointer_index)
def getHistoricalY(self, pointer_index: int = 0, pos: int = 0) -> float:
history = getattr(self, '_history', [])
if 0 <= pos < len(history):
return history[pos].get('y', self.getY(pointer_index))
return self.getY(pointer_index)
def getHistoricalPressure(self, pointer_index: int = 0, pos: int = 0) -> float:
history = getattr(self, '_history', [])
if 0 <= pos < len(history):
return history[pos].get('pressure', 1.0)
return 1.0
def getHistoricalEventTime(self, pos: int = 0) -> int:
history = getattr(self, '_history', [])
if 0 <= pos < len(history):
return history[pos].get('event_time', self._event_time)
return self._event_time
# ─── edge flags ───────────────────────────────────────────
def getEdgeFlags(self) -> int:
return getattr(self, '_edge_flags', 0)
def setEdgeFlags(self, edge_flags: int):
self._edge_flags = edge_flags
# ─── action mutation ──────────────────────────────────────
def setAction(self, action: int):
self._action = action
# ─── batch ────────────────────────────────────────────────
def addBatch(self, event_time: int, x: float, y: float,
pressure: float = 1.0, size: float = 0.5,
meta_state: int = 0):
if not hasattr(self, '_history'):
self._history = []
self._history.append({
'event_time': self._event_time,
'x': self._x,
'y': self._y,
'pressure': self.getPressure(0),
'size': self.getSize(0),
'meta_state': self._meta_state,
})
self._x = x
self._y = y
self._raw_x = x
self._raw_y = y
self._event_time = event_time
self._meta_state = meta_state
if self._pointers:
self._pointers[0]['x'] = x
self._pointers[0]['y'] = y
self._pointers[0]['pressure'] = pressure
self._pointers[0]['size'] = size
# ═══════════════════════════════════════════════════════════════
# KeyEvent
@@ -1981,6 +2190,28 @@ class KeyEvent(Stub):
meta_state=event._meta_state,
)
def getCharacters(self):
return getattr(self, '_characters', None)
def isSystem(self):
return False
def isPrintingKey(self):
return False
def isModifierKey(self, keyCode):
return keyCode in (KEYCODE_SHIFT_LEFT, KEYCODE_SHIFT_RIGHT, KEYCODE_ALT_LEFT, KEYCODE_ALT_RIGHT, KEYCODE_CTRL_LEFT, KEYCODE_CTRL_RIGHT, KEYCODE_META_LEFT, KEYCODE_META_RIGHT)
def startTracking(self):
self._tracking = True
def isTracking(self):
return getattr(self, '_tracking', False)
@staticmethod
def keyCodeToString(keyCode):
return f"KEYCODE_{keyCode}"
# ═══════════════════════════════════════════════════════════════
# GestureDetector
@@ -2688,14 +2919,18 @@ class SurfaceHolder(Stub):
def setFormat(self, format: int):
self._format = format
def lockCanvas(self) -> Any:
return None
def lockCanvas(self, *args):
"""锁定画布并返回 Canvas 对象"""
if hasattr(self, '_surface') and self._surface:
from .graphics import Canvas, Bitmap
if hasattr(self._surface, '_bitmap') and self._surface._bitmap:
return Canvas(self._surface._bitmap)
from .graphics import Canvas, Bitmap
return Canvas(Bitmap(100, 100))
def lockCanvas(self, dirty: Any) -> Any:
return None
def unlockCanvasAndPost(self, canvas: Any):
pass
def unlockCanvasAndPost(self, canvas):
"""解锁画布并提交"""
pass # 桌面环境无需实际提交
# ═══════════════════════════════════════════════════════════════
@@ -3260,9 +3495,13 @@ class Display(Stub):
return self._display_id
def getWidth(self) -> int:
if _USE_LINUX:
return LinuxDisplay.get_screen_size()[0]
return self._width
def getHeight(self) -> int:
if _USE_LINUX:
return LinuxDisplay.get_screen_size()[1]
return self._height
def getRotation(self) -> int:
+65 -42
View File
@@ -13,14 +13,33 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError
from ..java.lang import RuntimeException
from .view import KeyEvent as _BaseKeyEvent
from .view import ViewGroup as _BaseViewGroup
from .view import View as _BaseView
from .view import ViewOutlineProvider as _BaseViewOutlineProvider
from .view import ViewPropertyAnimator as _BaseViewPropertyAnimator
from .view import ViewTreeObserver as _BaseViewTreeObserver
from .view import ViewConfiguration as _BaseViewConfiguration
from .view import ViewParent as _BaseViewParent
from .view import WindowInsets as _BaseWindowInsets
from .view import Display as _BaseDisplay
from .view import DragEvent as _BaseDragEvent
from .view import GestureDetector as _BaseGestureDetector
from .view import ScaleGestureDetector as _BaseScaleGestureDetector
from .view import SoundEffectConstants as _BaseSoundEffectConstants
from .view import Surface as _BaseSurface
from .view import SurfaceView as _BaseSurfaceView
from .view import TextureView as _BaseTextureView
from .view import VelocityTracker as _BaseVelocityTracker
from .view import WindowInsetsController as _BaseWindowInsetsController
# ═══════════════════════════════════════════════════════════════
# ViewOutlineProvider
# ═══════════════════════════════════════════════════════════════
class ViewOutlineProvider(Stub):
"""ViewOutlineProvider — 视图轮廓提供者"""
class ViewOutlineProvider(_BaseViewOutlineProvider):
"""ViewOutlineProvider — 视图轮廓提供者 (继承 view.ViewOutlineProvider 的所有方法)"""
BACKGROUND = None
BOUNDS = None
@@ -99,8 +118,8 @@ class Outline(Stub):
# ViewPropertyAnimator
# ═══════════════════════════════════════════════════════════════
class ViewPropertyAnimator(Stub):
"""ViewPropertyAnimator — 视图属性动画器"""
class ViewPropertyAnimator(_BaseViewPropertyAnimator):
"""ViewPropertyAnimator — 视图属性动画器 (继承 view.ViewPropertyAnimator 的所有方法)"""
def __init__(self, view: Any = None):
super().__init__()
@@ -381,8 +400,8 @@ class ViewStructure(Stub):
# ViewTreeObserver
# ═══════════════════════════════════════════════════════════════
class ViewTreeObserver(Stub):
"""ViewTreeObserver — 视图树观察者"""
class ViewTreeObserver(_BaseViewTreeObserver):
"""ViewTreeObserver — 视图树观察者 (继承 view.ViewTreeObserver 的所有方法)"""
def __init__(self):
super().__init__()
@@ -529,8 +548,8 @@ class ViewDebug(Stub):
# ViewConfiguration
# ═══════════════════════════════════════════════════════════════
class ViewConfiguration(Stub):
"""ViewConfiguration — 视图配置"""
class ViewConfiguration(_BaseViewConfiguration):
"""ViewConfiguration — 视图配置 (继承 view.ViewConfiguration 的所有方法)"""
def __init__(self):
super().__init__()
@@ -615,8 +634,8 @@ class ViewConfiguration(Stub):
# ViewParent (扩展)
# ═══════════════════════════════════════════════════════════════
class ViewParent(Stub):
"""ViewParent — 视图父容器接口扩展"""
class ViewParent(_BaseViewParent):
"""ViewParent — 视图父容器接口扩展 (继承 view.ViewParent 的所有方法)"""
def requestLayout(self) -> None:
pass
@@ -699,8 +718,8 @@ class ViewParent(Stub):
# ViewGroup (扩展)
# ═══════════════════════════════════════════════════════════════
class ViewGroup(Stub):
"""ViewGroup — 视图组扩展"""
class ViewGroup(_BaseViewGroup):
"""ViewGroup — 视图组扩展 (继承 view.ViewGroup 的所有方法)"""
CLIP_TO_PADDING_MASK = 0
FOCUS_AFTER = 1
@@ -781,15 +800,19 @@ class ViewGroup(Stub):
f"margins=({self.leftMargin},{self.topMargin},"
f"{self.rightMargin},{self.bottomMargin}))")
def __init__(self):
super().__init__()
self._children: List[Any] = []
self._clip_children = True
self._clip_to_padding = True
def __init__(self, context=None):
super().__init__(context)
if not hasattr(self, '_children'):
self._children: List[Any] = []
if not hasattr(self, '_clip_children'):
self._clip_children = True
if not hasattr(self, '_clip_to_padding'):
self._clip_to_padding = True
self._layout_mode = self.LAYOUT_MODE_CLIP_BOUNDS
self._persistent_caching = self.PERSISTENT_NO_CACHE
self._touch_mode = False
self._motion_event_splitting = True
if not hasattr(self, '_motion_event_splitting'):
self._motion_event_splitting = True
self._transient_child: Optional[Any] = None
self._add_stateset_focus = False
self._child_drawable_state: Optional[Any] = None
@@ -947,8 +970,8 @@ class ViewOverlay(Stub):
# WindowInsets / DisplayCutout / Display
# ═══════════════════════════════════════════════════════════════
class WindowInsets(Stub):
"""WindowInsets — 窗口插入区域"""
class WindowInsets(_BaseWindowInsets):
"""WindowInsets — 窗口插入区域 (继承 view.WindowInsets 的所有方法)"""
def __init__(self):
super().__init__()
@@ -1080,8 +1103,8 @@ class DisplayCutout(Stub):
return f"DisplayCutout(safe={self._safe_insets})"
class Display(Stub):
"""Display — 屏幕显示信息"""
class Display(_BaseDisplay):
"""Display — 屏幕显示信息 (继承 view.Display 的所有方法)"""
DEFAULT_DISPLAY = 0
FLAG_PRESENTATION = 1
@@ -1423,8 +1446,8 @@ WINDOW_INSETS_ANIMATION_TYPE_TRANSLATION = 2
WINDOW_INSETS_ANIMATION_TYPE_ALPHA_AND_TRANSLATION = 3
class DragEvent(Stub):
"""DragEvent — 拖放事件"""
class DragEvent(_BaseDragEvent):
"""DragEvent — 拖放事件 (继承 view.DragEvent 的所有方法)"""
ACTION_DRAG_STARTED = DRAG_EVENT_ACTION_DRAG_STARTED
ACTION_DRAG_ENTERED = DRAG_EVENT_ACTION_DRAG_ENTERED
@@ -1467,8 +1490,8 @@ class DragEvent(Stub):
return f"DragEvent(action={actions.get(self._action, 'UNKNOWN')})"
class GestureDetector(Stub):
"""GestureDetector — 手势检测器"""
class GestureDetector(_BaseGestureDetector):
"""GestureDetector — 手势检测器 (继承 view.GestureDetector 的所有方法)"""
class SimpleOnGestureListener(Stub):
"""GestureDetector.SimpleOnGestureListener — 简单手势监听器"""
@@ -1559,8 +1582,8 @@ class GestureDetector(Stub):
return f"GestureDetector(listener={'yes' if self._listener else 'no'})"
class KeyEvent(Stub):
"""KeyEvent — 按键事件"""
class KeyEvent(_BaseKeyEvent):
"""KeyEvent — 按键事件 (扩展版,继承 view.KeyEvent 的所有常量)"""
ACTION_DOWN = 0
ACTION_UP = 1
@@ -2083,8 +2106,8 @@ class ContextMenu(Menu):
# ScaleGestureDetector / SoundEffectConstants / Surface / etc.
# ═══════════════════════════════════════════════════════════════
class ScaleGestureDetector(Stub):
"""ScaleGestureDetector — 缩放手势检测器"""
class ScaleGestureDetector(_BaseScaleGestureDetector):
"""ScaleGestureDetector — 缩放手势检测器 (继承 view.ScaleGestureDetector 的所有方法)"""
class SimpleOnScaleGestureListener(Stub):
def onScale(self, detector: "ScaleGestureDetector") -> bool:
@@ -2164,8 +2187,8 @@ class ScaleGestureDetector(Stub):
f"inProgress={self._in_progress})")
class SoundEffectConstants(Stub):
"""SoundEffectConstants — 音效常量"""
class SoundEffectConstants(_BaseSoundEffectConstants):
"""SoundEffectConstants — 音效常量 (继承 view.SoundEffectConstants 的所有方法)"""
CLICK = 0
NAVIGATION_UP = 1
@@ -2182,8 +2205,8 @@ class SoundEffectConstants(Stub):
return SoundEffectConstants.NAVIGATION_UP
class Surface(Stub):
"""Surface — 表面"""
class Surface(_BaseSurface):
"""Surface — 表面 (继承 view.Surface 的所有方法)"""
ROTATION_0 = 0
ROTATION_90 = 1
@@ -2256,8 +2279,8 @@ class SurfaceControl(Stub):
return f"SurfaceControl(layer={self._layer}, visible={self._visible})"
class SurfaceView(Stub):
"""SurfaceView — 表面视图扩展"""
class SurfaceView(_BaseSurfaceView):
"""SurfaceView — 表面视图扩展 (继承 view.SurfaceView 的所有方法)"""
def __init__(self, context: Any = None):
super().__init__()
@@ -2284,8 +2307,8 @@ class SurfaceView(Stub):
return "SurfaceView()"
class TextureView(Stub):
"""TextureView — 纹理视图"""
class TextureView(_BaseTextureView):
"""TextureView — 纹理视图扩展 (继承 view.TextureView 的所有方法)"""
def __init__(self, context: Any = None):
super().__init__()
@@ -2328,8 +2351,8 @@ class TouchDelegate(Stub):
return f"TouchDelegate(delegate={self._delegate_view})"
class VelocityTracker(Stub):
"""VelocityTracker — 速度追踪器"""
class VelocityTracker(_BaseVelocityTracker):
"""VelocityTracker — 速度追踪器 (继承 view.VelocityTracker 的所有方法)"""
def __init__(self, strategy: str = ""):
super().__init__()
@@ -2445,8 +2468,8 @@ class WindowInsetsAnimation(Stub):
f"fraction={self._interpolated_fraction})")
class WindowInsetsController(Stub):
"""WindowInsetsController — 窗口插入控制器"""
class WindowInsetsController(_BaseWindowInsetsController):
"""WindowInsetsController — 窗口插入控制器 (继承 view.WindowInsetsController 的所有方法)"""
APPEARANCE_OPAQUE_STATUS_BARS = 1
APPEARANCE_OPAQUE_NAVIGATION_BARS = 2
+144 -30
View File
@@ -14,6 +14,12 @@ from .view import (
ViewGroup as VG,
)
try:
from .._linux_impl import IS_LINUX, LinuxNotify, LinuxClipboard
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# TextView
@@ -359,6 +365,39 @@ class TextView(View):
def setPadding(self, left: int, top: int, right: int, bottom: int):
super().setPadding(left, top, right, bottom)
def setPaddingRelative(self, start: int, top: int, end: int, bottom: int):
self._padding_left = start
self._padding_top = top
self._padding_right = end
self._padding_bottom = bottom
# ─── text color getter ───────────────────────────────────
def getTextColor(self) -> int:
return self._text_color
# ─── selection action mode ───────────────────────────────
def setCustomSelectionActionModeCallback(self, callback: Any):
self._custom_selection_action_mode_callback = callback
def getCustomSelectionActionModeCallback(self) -> Any:
return getattr(self, '_custom_selection_action_mode_callback', None)
# ─── break / justification strategy ──────────────────────
def setBreakStrategy(self, break_strategy: int):
self._break_strategy = break_strategy
def getBreakStrategy(self) -> int:
return getattr(self, '_break_strategy', 0)
def setJustificationMode(self, justification_mode: int):
self._justification_mode = justification_mode
def getJustificationMode(self) -> int:
return getattr(self, '_justification_mode', 0)
def onMeasure(self, width_measure_spec: int, height_measure_spec: int):
from .view import MeasureSpec
w = MeasureSpec.getSize(width_measure_spec)
@@ -450,6 +489,12 @@ class CheckBox(Button):
def toggle(self):
self._checked = not self._checked
def setOnCheckedChangeListener(self, listener: Optional[Callable]):
self._on_checked_change_listener = listener
def getOnCheckedChangeListener(self) -> Optional[Callable]:
return getattr(self, '_on_checked_change_listener', None)
def performClick(self) -> bool:
self.toggle()
return super().performClick()
@@ -601,6 +646,22 @@ class Switch(Button):
def getTextOff(self) -> str:
return self._text_off
# ─── checked change listener ─────────────────────────────
def setOnCheckedChangeListener(self, listener: Optional[Callable]):
self._on_checked_change_listener = listener
def getOnCheckedChangeListener(self) -> Optional[Callable]:
return getattr(self, '_on_checked_change_listener', None)
# ─── switch text appearance ──────────────────────────────
def setSwitchTextAppearance(self, context: Any, resid: int):
self._switch_text_appearance = resid
def getSwitchTextAppearance(self) -> int:
return getattr(self, '_switch_text_appearance', 0)
def performClick(self) -> bool:
self.toggle()
return super().performClick()
@@ -688,6 +749,17 @@ class ImageView(View):
def getCropToPadding(self) -> bool:
return self._crop_to_padding
# ─── color filter ─────────────────────────────────────────
def setColorFilter(self, color_filter: Any):
self._color_filter = color_filter
def getColorFilter(self) -> Any:
return getattr(self, '_color_filter', None)
def clearColorFilter(self):
self._color_filter = None
def setBaseline(self, baseline: int):
self._baseline = baseline
@@ -1110,6 +1182,10 @@ class ListView(AdapterView):
def getCheckItemIds(self) -> List[int]:
return list(self._checked_id_positions.keys())
def clearChoices(self):
self._checked_item_positions.clear()
self._checked_id_positions.clear()
class GridView(AdapterView):
"""GridView — 网格视图"""
@@ -1254,28 +1330,48 @@ class RecyclerView(ViewGroup):
return False
def notifyDataSetChanged(self):
pass
for observer in getattr(self, '_observers', []):
observer.onChanged()
def notifyItemChanged(self, position: int):
pass
def notifyItemChanged(self, position, payload=None):
for observer in getattr(self, '_observers', []):
observer.onItemRangeChanged(position, 1, payload)
def notifyItemInserted(self, position: int):
pass
def notifyItemInserted(self, position):
for observer in getattr(self, '_observers', []):
observer.onItemRangeInserted(position, 1)
def notifyItemRemoved(self, position: int):
pass
def notifyItemRemoved(self, position):
for observer in getattr(self, '_observers', []):
observer.onItemRangeRemoved(position, 1)
def notifyItemMoved(self, from_position: int, to_position: int):
pass
def notifyItemMoved(self, fromPosition, toPosition):
for observer in getattr(self, '_observers', []):
observer.onItemRangeMoved(fromPosition, toPosition, 1)
def notifyItemRangeChanged(self, position_start: int, item_count: int):
pass
def notifyItemRangeChanged(self, positionStart, itemCount, payload=None):
for observer in getattr(self, '_observers', []):
observer.onItemRangeChanged(positionStart, itemCount, payload)
def notifyItemRangeInserted(self, position_start: int, item_count: int):
pass
def notifyItemRangeInserted(self, positionStart, itemCount):
for observer in getattr(self, '_observers', []):
observer.onItemRangeInserted(positionStart, itemCount)
def notifyItemRangeRemoved(self, position_start: int, item_count: int):
pass
def notifyItemRangeRemoved(self, positionStart, itemCount):
for observer in getattr(self, '_observers', []):
observer.onItemRangeRemoved(positionStart, itemCount)
def registerAdapterDataObserver(self, observer):
if not hasattr(self, '_observers'):
self._observers = []
self._observers.append(observer)
def unregisterAdapterDataObserver(self, observer):
if hasattr(self, '_observers'):
try:
self._observers.remove(observer)
except ValueError:
pass
def getStateRestorationPolicy(self) -> int:
return 0
@@ -2062,11 +2158,13 @@ class ScrollView(FrameLayout):
def arrowScroll(self, direction: int) -> bool:
return True
def smoothScrollTo(self, x: int, y: int):
self._scroll_position = y
def smoothScrollTo(self, x, y):
self._scroll_x = x
self._scroll_y = y
def smoothScrollBy(self, dx: int, dy: int):
self._scroll_position += dy
def smoothScrollBy(self, dx, dy):
self._scroll_x = getattr(self, '_scroll_x', 0) + dx
self._scroll_y = getattr(self, '_scroll_y', 0) + dy
def pageScroll(self, direction: int) -> bool:
return True
@@ -2100,11 +2198,13 @@ class HorizontalScrollView(FrameLayout):
def arrowScroll(self, direction: int) -> bool:
return True
def smoothScrollTo(self, x: int, y: int):
pass
def smoothScrollTo(self, x, y):
self._scroll_x = x
self._scroll_y = y
def smoothScrollBy(self, dx: int, dy: int):
pass
def smoothScrollBy(self, dx, dy):
self._scroll_x = getattr(self, '_scroll_x', 0) + dx
self._scroll_y = getattr(self, '_scroll_y', 0) + dy
def pageScroll(self, direction: int) -> bool:
return True
@@ -2789,16 +2889,24 @@ class BaseAdapter(Stub):
return self.getView(position, convert_view, parent)
def notifyDataSetChanged(self):
pass
for observer in getattr(self, '_observers', []):
observer.onChanged()
def notifyDataSetInvalidated(self):
pass
for observer in getattr(self, '_observers', []):
observer.onInvalidated()
def registerDataSetObserver(self, observer: Any):
pass
def registerDataSetObserver(self, observer):
if not hasattr(self, '_observers'):
self._observers = []
self._observers.append(observer)
def unregisterDataSetObserver(self, observer: Any):
pass
def unregisterDataSetObserver(self, observer):
if hasattr(self, '_observers'):
try:
self._observers.remove(observer)
except ValueError:
pass
class ArrayAdapter(BaseAdapter):
@@ -3589,9 +3697,15 @@ class Toast(Stub):
return toast
def show(self):
pass
if _USE_LINUX:
LinuxNotify.send("Toast", self._text)
else:
print(f"[Toast] {self._text}")
def cancel(self):
if _USE_LINUX:
# 桌面通知自动消失,无需操作
return
pass
def setGravity(self, gravity: int, x_offset: int, y_offset: int):
+68 -34
View File
@@ -11,6 +11,35 @@ import threading
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError
from .widget import AdapterView as _BaseAdapterView
from .widget import AutoCompleteTextView as _BaseAutoCompleteTextView
from .widget import ProgressBar as _BaseProgressBar
from .widget import Chronometer as _BaseChronometer
from .widget import TableLayout as _BaseTableLayout
from .widget import TableRow as _BaseTableRow
from .widget import LinearLayout as _BaseLinearLayout
from .widget import FrameLayout as _BaseFrameLayout
from .widget import RelativeLayout as _BaseRelativeLayout
from .widget import NumberPicker as _BaseNumberPicker
from .widget import SearchView as _BaseSearchView
from .widget import TabHost as _BaseTabHost
from .widget import TabWidget as _BaseTabWidget
from .widget import ViewAnimator as _BaseViewAnimator
from .widget import ImageButton as _BaseImageButton
from .widget import ZoomControls as _BaseZoomControls
from .widget import DigitalClock as _BaseDigitalClock
from .widget import AnalogClock as _BaseAnalogClock
from .widget import ToggleButton as _BaseToggleButton
from .widget import RadioButton as _BaseRadioButton
from .widget import SeekBar as _BaseSeekBar
from .widget import RatingBar as _BaseRatingBar
from .widget import ViewSwitcher as _BaseViewSwitcher
from .widget import TextSwitcher as _BaseTextSwitcher
from .widget import ImageSwitcher as _BaseImageSwitcher
from .widget import BaseAdapter as _BaseBaseAdapter
from .widget import ArrayAdapter as _BaseArrayAdapter
from .widget import SimpleAdapter as _BaseSimpleAdapter
from .widget import CursorAdapter as _BaseCursorAdapter
from ..java.lang import RuntimeException
@@ -135,7 +164,7 @@ class Adapter(Stub):
return f"Adapter(count={self.getCount()})"
class AdapterView(Stub):
class AdapterView(_BaseAdapterView):
"""AdapterView — 适配器视图"""
ITEM_VIEW_TYPE_IGNORE = -1
@@ -326,7 +355,7 @@ class ThemedSpinnerAdapter(Stub):
# AutoCompleteTextView
# ═══════════════════════════════════════════════════════════════
class AutoCompleteTextView(Stub):
class AutoCompleteTextView(_BaseAutoCompleteTextView):
"""AutoCompleteTextView — 自动完成文本视图"""
THRESHOLD_DEFAULT = 2
@@ -429,7 +458,7 @@ class AutoCompleteTextView(Stub):
# ProgressBar / SeekBar / AbsSeekBar / RatingBar
# ═══════════════════════════════════════════════════════════════
class ProgressBar(Stub):
class ProgressBar(_BaseProgressBar):
"""ProgressBar — 进度条扩展"""
STYLE_HORIZONTAL = 0x00000001
@@ -568,11 +597,11 @@ class AbsSeekBar(ProgressBar):
self._on_seek_bar_change = listener
class SeekBar(AbsSeekBar):
class SeekBar(_BaseSeekBar, AbsSeekBar):
"""SeekBar — 拖动条"""
def __init__(self, context: Any = None):
super().__init__(context)
AbsSeekBar.__init__(self, context)
self._on_seek_bar_change: Optional[Callable] = None
def setOnSeekBarChangeListener(self, listener: Optional[Callable]) -> None:
@@ -585,12 +614,12 @@ class SeekBar(AbsSeekBar):
return f"SeekBar(progress={self.getProgress()})"
class RatingBar(ProgressBar):
class RatingBar(_BaseRatingBar, ProgressBar):
"""RatingBar — 评分条"""
def __init__(self, context: Any = None, num_stars: int = 5,
step_size: float = 0.5):
super().__init__(context, ProgressBar.STYLE_HORIZONTAL)
ProgressBar.__init__(self, context, ProgressBar.STYLE_HORIZONTAL)
self._num_stars = num_stars
self._step_size = step_size
self._rating: float = 0.0
@@ -638,7 +667,7 @@ class RatingBar(ProgressBar):
# Chronometer
# ═══════════════════════════════════════════════════════════════
class Chronometer(Stub):
class Chronometer(_BaseChronometer):
"""Chronometer — 计时器"""
FORMAT_ELAPSED_TIME = 1
@@ -734,11 +763,12 @@ class CompoundButton(Stub):
return f"CompoundButton(checked={self._checked})"
class ToggleButton(CompoundButton):
class ToggleButton(_BaseToggleButton, CompoundButton):
"""ToggleButton — 开关按钮"""
def __init__(self, context: Any = None):
super().__init__(context)
CompoundButton.__init__(self, context)
self._text_on: str = "ON"
self._text_off: str = "OFF"
@@ -758,11 +788,12 @@ class ToggleButton(CompoundButton):
return f"ToggleButton(checked={self.isChecked()})"
class RadioButton(CompoundButton):
class RadioButton(_BaseRadioButton, CompoundButton):
"""RadioButton — 单选按钮"""
def __init__(self, context: Any = None):
super().__init__(context)
CompoundButton.__init__(self, context)
def toggle(self) -> None:
if not self.isChecked():
@@ -827,7 +858,7 @@ class RadioGroup(Stub):
# TableLayout / TableRow
# ═══════════════════════════════════════════════════════════════
class TableLayout(Stub):
class TableLayout(_BaseTableLayout):
"""TableLayout — 表格布局"""
def __init__(self, context: Any = None):
@@ -863,7 +894,7 @@ class TableLayout(Stub):
return f"TableLayout(rows={len(self._rows)})"
class TableRow(Stub):
class TableRow(_BaseTableRow):
"""TableRow — 表格行"""
class LayoutParams(Stub):
@@ -1029,7 +1060,7 @@ class GridLayout(Stub):
# LinearLayout / FrameLayout / RelativeLayout
# ═══════════════════════════════════════════════════════════════
class LinearLayout(Stub):
class LinearLayout(_BaseLinearLayout):
"""LinearLayout — 线性布局"""
HORIZONTAL = 0
@@ -1120,7 +1151,7 @@ class LinearLayout(Stub):
return f"LinearLayout(orientation={'H' if self._orientation == 0 else 'V'})"
class FrameLayout(Stub):
class FrameLayout(_BaseFrameLayout):
"""FrameLayout — 帧布局"""
class LayoutParams(Stub):
@@ -1177,7 +1208,7 @@ class FrameLayout(Stub):
return "FrameLayout()"
class RelativeLayout(Stub):
class RelativeLayout(_BaseRelativeLayout):
"""RelativeLayout — 相对布局"""
TRUE = -1
@@ -1284,7 +1315,7 @@ class RelativeLayout(Stub):
# NumberPicker / SearchView / TabHost / TabWidget
# ═══════════════════════════════════════════════════════════════
class NumberPicker(Stub):
class NumberPicker(_BaseNumberPicker):
"""NumberPicker — 数字选择器"""
def __init__(self, context: Any = None):
@@ -1344,7 +1375,7 @@ class NumberPicker(Stub):
return f"NumberPicker(value={self._value}, range=[{self._min_value}-{self._max_value}])"
class SearchView(Stub):
class SearchView(_BaseSearchView):
"""SearchView — 搜索视图扩展"""
ICONIFIED = 0
@@ -1425,7 +1456,7 @@ class SearchView(Stub):
return f"SearchView(query={self._query})"
class TabHost(Stub):
class TabHost(_BaseTabHost):
"""TabHost — 标签宿主"""
class TabSpec(Stub):
@@ -1493,7 +1524,7 @@ class TabHost(Stub):
return f"TabHost(tabs={len(self._tabs)}, current={self._current_tab})"
class TabWidget(Stub):
class TabWidget(_BaseTabWidget):
"""TabWidget — 标签小部件"""
def __init__(self, context: Any = None):
@@ -1529,7 +1560,7 @@ class TabWidget(Stub):
# TextSwitcher / ViewSwitcher / ViewAnimator / ImageSwitcher
# ═══════════════════════════════════════════════════════════════
class ViewAnimator(Stub):
class ViewAnimator(_BaseViewAnimator):
"""ViewAnimator — 视图动画器"""
def __init__(self, context: Any = None):
@@ -1595,7 +1626,7 @@ class ViewAnimator(Stub):
return f"ViewAnimator(children={len(self._children)}, displayed={self._displayed_child})"
class ViewSwitcher(ViewAnimator):
class ViewSwitcher(_BaseViewSwitcher, ViewAnimator):
"""ViewSwitcher — 视图切换器"""
class ViewFactory(Stub):
@@ -1605,6 +1636,7 @@ class ViewSwitcher(ViewAnimator):
def __init__(self, context: Any = None):
super().__init__(context)
ViewAnimator.__init__(self, context)
self._factory: Optional[ViewSwitcher.ViewFactory] = None
def setFactory(self, factory: ViewFactory) -> None:
@@ -1623,11 +1655,12 @@ class ViewSwitcher(ViewAnimator):
return f"ViewSwitcher()"
class TextSwitcher(ViewSwitcher):
class TextSwitcher(_BaseTextSwitcher, ViewSwitcher):
"""TextSwitcher — 文本切换器"""
def __init__(self, context: Any = None):
super().__init__(context)
ViewSwitcher.__init__(self, context)
self._factory = None
def setCurrentText(self, text: str) -> None:
@@ -1640,11 +1673,12 @@ class TextSwitcher(ViewSwitcher):
return "TextSwitcher()"
class ImageSwitcher(ViewSwitcher):
class ImageSwitcher(_BaseImageSwitcher, ViewSwitcher):
"""ImageSwitcher — 图片切换器"""
def __init__(self, context: Any = None):
super().__init__(context)
ViewSwitcher.__init__(self, context)
def setImageResource(self, res_id: int) -> None:
pass
@@ -1659,7 +1693,7 @@ class ImageSwitcher(ViewSwitcher):
return "ImageSwitcher()"
class ImageButton(Stub):
class ImageButton(_BaseImageButton):
"""ImageButton — 图片按钮"""
def __init__(self, context: Any = None):
@@ -1711,7 +1745,7 @@ class ZoomButton(ImageButton):
return f"ZoomButton(zoomIn={self._zoom_in})"
class ZoomControls(Stub):
class ZoomControls(_BaseZoomControls):
"""ZoomControls — 缩放控件"""
def __init__(self, context: Any = None):
@@ -1757,7 +1791,7 @@ class ZoomControls(Stub):
# DigitalClock / TextClock / AnalogClock
# ═══════════════════════════════════════════════════════════════
class DigitalClock(Stub):
class DigitalClock(_BaseDigitalClock):
"""DigitalClock — 数字时钟"""
def __init__(self, context: Any = None):
@@ -1825,7 +1859,7 @@ class TextClock(Stub):
return f"TextClock(time={self.getText()})"
class AnalogClock(Stub):
class AnalogClock(_BaseAnalogClock):
"""AnalogClock — 模拟时钟"""
def __init__(self, context: Any = None):
@@ -1882,7 +1916,7 @@ class QuickContactBadge(Stub):
# 各种 Adapter 实现
# ═══════════════════════════════════════════════════════════════
class BaseAdapter(Adapter):
class BaseAdapter(_BaseBaseAdapter, Adapter):
"""BaseAdapter — 基础适配器"""
def __init__(self):
@@ -1923,12 +1957,12 @@ class BaseAdapter(Adapter):
return self.getCount() == 0
class ArrayAdapter(BaseAdapter):
class ArrayAdapter(_BaseArrayAdapter, BaseAdapter):
"""ArrayAdapter — 数组适配器"""
def __init__(self, context: Any = None, resource: int = 0,
objects: Optional[List[Any]] = None):
super().__init__()
BaseAdapter.__init__(self)
self._context = context
self._resource = resource
self._drop_down_resource: int = resource
@@ -1990,7 +2024,7 @@ class ArrayAdapter(BaseAdapter):
return f"ArrayAdapter(items={len(self._objects)})"
class SimpleAdapter(BaseAdapter):
class SimpleAdapter(_BaseSimpleAdapter, BaseAdapter):
"""SimpleAdapter — 简单适配器"""
def __init__(self, context: Any = None,
@@ -1998,7 +2032,7 @@ class SimpleAdapter(BaseAdapter):
resource: int = 0,
from_keys: Optional[List[str]] = None,
to_ids: Optional[List[int]] = None):
super().__init__()
BaseAdapter.__init__(self)
self._context = context
self._data: List[Dict[str, Any]] = data or []
self._resource = resource
@@ -2031,7 +2065,7 @@ class SimpleAdapter(BaseAdapter):
return f"SimpleAdapter(items={len(self._data)})"
class CursorAdapter(BaseAdapter):
class CursorAdapter(_BaseCursorAdapter, BaseAdapter):
"""CursorAdapter — 游标适配器"""
FLAG_AUTO_REQUERY = 1
@@ -2039,7 +2073,7 @@ class CursorAdapter(BaseAdapter):
def __init__(self, context: Any = None, cursor: Any = None,
flags: int = FLAG_AUTO_REQUERY):
super().__init__()
BaseAdapter.__init__(self)
self._context = context
self._cursor = cursor
self._flags = flags
+209 -26
View File
@@ -237,7 +237,10 @@ class File(Stub):
return _os.path.abspath(self._path)
def getCanonicalPath(self):
return _os.path.realpath(self._path)
try:
return _os.path.realpath(self._path)
except Exception:
return self.getAbsolutePath()
def getParent(self):
p = _os.path.dirname(self._path)
@@ -439,6 +442,43 @@ class File(Stub):
def toPath(self):
return _PathLibPath(self._path)
@staticmethod
def createTempFile(prefix, suffix=None, directory=None):
import tempfile
if directory:
fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix or ".tmp", dir=directory)
else:
fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix or ".tmp")
_os.close(fd)
return File(path)
def deleteOnExit(self):
import atexit
atexit.register(lambda: self.delete() if self.exists() else None)
def setLastModified(self, time):
try:
_os.utime(self._path, (time / 1000, time / 1000))
return True
except Exception:
return False
def setReadOnly(self):
try:
_os.chmod(self._path, 0o444)
return True
except Exception:
return False
def isAbsolute(self):
return _os.path.isabs(self._path)
def getAbsoluteFile(self):
return File(self.getAbsolutePath())
def getCanonicalFile(self):
return File(self.getCanonicalPath())
def __str__(self):
return self._path
@@ -705,14 +745,48 @@ class BufferedInputStream(FilterInputStream):
return total if total > 0 else -1
def _fill(self):
data = self._in.read(bytearray(self._buf_size), 0, self._buf_size)
if data is None or data == -1:
tmp = bytearray(self._buf_size)
n = self._in.read(tmp, 0, self._buf_size)
if n is None or n == -1:
return False
n = data if isinstance(data, int) else len(data)
self._buffer = bytearray(self._in._buffer if hasattr(self._in, '_buffer') else data) if n > 0 else bytearray()
if isinstance(n, (bytes, bytearray)):
# defensive: underlying stream returned raw data instead of a count
tmp = bytearray(n)
n = len(tmp)
if not isinstance(n, int) or n <= 0:
return False
self._buffer = bytearray(tmp[:n])
self._pos = 0
self._count = n
return n > 0
return True
def available(self):
buf_avail = self._count - self._pos
if buf_avail < 0:
buf_avail = 0
try:
underlying = self._in.available()
if underlying and underlying > 0:
return buf_avail + underlying
except Exception:
pass
return buf_avail
def mark(self, readlimit):
self._mark_pos = self._pos
self._mark_buffer = bytes(self._buffer)
self._mark_count = self._count
self._mark_limit = readlimit
def reset(self):
if not hasattr(self, '_mark_pos'):
raise IOException("Resetting to invalid mark")
self._buffer = bytearray(self._mark_buffer)
self._count = self._mark_count
self._pos = self._mark_pos
def markSupported(self):
return True
# ============================================================
@@ -994,6 +1068,26 @@ class BufferedWriter(Closeable, Flushable):
def newLine(self):
self._buffer.append("\n")
def append(self, csq=None, start=None, end=None):
if csq is None:
return self
if isinstance(csq, int):
self._buffer.append(chr(csq))
else:
s = str(csq)
if start is not None and end is not None:
s = s[start:end]
elif start is not None:
s = s[start:]
self._buffer.append(s)
if sum(len(x) for x in self._buffer) >= self._buf_size:
self.flush()
return self
def check(self):
if getattr(self._writer, '_closed', False):
raise IOException("Stream closed")
def flush(self):
if self._buffer:
data = "".join(self._buffer)
@@ -1131,6 +1225,20 @@ class PrintWriter(Closeable, Flushable):
else:
self._write_str(str(s))
def append(self, csq=None, start=None, end=None):
if csq is None:
csq = "null"
if isinstance(csq, int):
self._write_str(chr(csq))
else:
s = str(csq)
if start is not None and end is not None:
s = s[start:end]
elif start is not None:
s = s[start:]
self._write_str(s)
return self
def format(self, fmt, *args):
s = str(fmt) % args if args else str(fmt)
self._write_str(s)
@@ -1216,6 +1324,9 @@ class ByteArrayInputStream(Closeable):
def reset(self):
self._pos = self._mark
def markSupported(self):
return True
# ============================================================
# ByteArrayOutputStream
@@ -1243,6 +1354,11 @@ class ByteArrayOutputStream(Closeable, Flushable):
def toByteArray(self):
return bytes(self._buf)
def writeTo(self, out):
if self._closed:
raise IOException("Stream closed")
out.write(bytes(self._buf))
def size(self):
return len(self._buf)
@@ -1690,37 +1806,104 @@ class PushbackInputStream(FilterInputStream):
# SequenceInputStream
# ============================================================
def _is_enumeration(obj) -> bool:
"""True if obj really exposes hasMoreElements/nextElement (not via Stub)."""
for cls in type(obj).__mro__:
if ('hasMoreElements' in cls.__dict__
and 'nextElement' in cls.__dict__):
return True
return False
class SequenceInputStream(Closeable):
def __init__(self, streams):
def __init__(self, *args):
super().__init__()
if hasattr(streams, 'hasMoreElements'):
self._streams = []
while streams.hasMoreElements():
self._streams.append(streams.nextElement())
else:
self._streams = list(streams)
self._streams = []
self._current_idx = 0
self._closed = False
if len(args) == 0:
return
if len(args) == 1:
streams = args[0]
if isinstance(streams, (list, tuple)):
self._streams = list(streams)
elif _is_enumeration(streams):
while streams.hasMoreElements():
self._streams.append(streams.nextElement())
else:
# single stream
self._streams = [streams]
else:
# SequenceInputStream(s1, s2, ...)
self._streams = list(args)
def _current(self):
if self._current_idx < len(self._streams):
return self._streams[self._current_idx]
while self._current_idx < len(self._streams):
inp = self._streams[self._current_idx]
if inp is None:
self._current_idx += 1
continue
return inp
return None
def read(self, b=None, off=None, length=None):
while self._current():
inp = self._current()
if b is None:
result = inp.read()
else:
result = inp.read(b, off, length)
if result != -1:
return result
def _advance(self):
if self._current_idx < len(self._streams):
cur = self._streams[self._current_idx]
if cur is not None:
try:
cur.close()
except Exception:
pass
self._current_idx += 1
return -1
def read(self, b=None, off=None, length=None):
if self._closed:
raise IOException("Stream closed")
if b is None:
while True:
inp = self._current()
if inp is None:
return -1
result = inp.read()
if result != -1:
return result
self._advance()
if off is None:
off = 0
if length is None:
length = len(b)
total = 0
while total < length:
inp = self._current()
if inp is None:
break
n = inp.read(b, off + total, length - total)
if n is None or n == -1:
self._advance()
continue
if n == 0:
self._advance()
continue
total += n
return total if total > 0 else -1
def available(self):
inp = self._current()
if inp is None:
return 0
try:
return inp.available()
except Exception:
return 0
def close(self):
self._closed = True
for s in self._streams:
s.close()
if s is not None:
try:
s.close()
except Exception:
pass
# ============================================================
+344 -2
View File
@@ -8,6 +8,7 @@ import re as _re
import math as _py_math
import sys as _sys
import os as _os
import platform
import time as _time
import threading as _threading
import inspect as _inspect
@@ -28,6 +29,12 @@ __all__ = [
"CloneNotSupportedException", "IllegalMonitorStateException",
"IllegalThreadStateException", "EnumConstantNotPresentException",
"TypeNotPresentException",
"ClassNotFoundException", "IllegalAccessException", "InstantiationException",
"InvocationTargetException", "NoSuchElementException",
"AssertionError", "OutOfMemoryError", "StackOverflowError",
"NoClassDefFoundError", "UnsatisfiedLinkError", "LinkageError",
"VerifyError", "AbstractMethodError", "IncompatibleClassChangeError",
"ExceptionInInitializerError",
"String", "Integer", "Long", "Short", "Byte", "Float", "Double",
"Boolean", "Character", "Math", "StrictMath", "System",
"Thread", "ThreadLocal", "StringBuilder", "StringBuffer",
@@ -227,6 +234,80 @@ class TypeNotPresentException(RuntimeException):
self.typeName = typeName
# ===== 缺失的 Exception 子类 =====
class ClassNotFoundException(Exception):
def __init__(self, message="", cause=None):
super().__init__(message, cause)
self.ex = message
class IllegalAccessException(Exception):
def __init__(self, message=""):
super().__init__(message)
class InstantiationException(Exception):
def __init__(self, message=""):
super().__init__(message)
class InvocationTargetException(Exception):
def __init__(self, target=None, cause=None):
if target is not None and cause is None:
super().__init__(str(target))
self.target = target
else:
super().__init__(str(target) if target else "", cause)
self.target = target
class NoSuchElementException(RuntimeException):
def __init__(self, message=""):
super().__init__(message)
# ===== 缺失的 Error 类 =====
class AssertionError(Error):
def __init__(self, message=None):
if message is None:
super().__init__("Assertion failed")
else:
super().__init__(str(message))
class OutOfMemoryError(Error):
def __init__(self, message=""):
super().__init__(message)
class StackOverflowError(Error):
def __init__(self, message=""):
super().__init__(message)
class NoClassDefFoundError(Error):
def __init__(self, message=""):
super().__init__(message)
class UnsatisfiedLinkError(Error):
def __init__(self, message=""):
super().__init__(message)
class LinkageError(Error):
def __init__(self, message=""):
super().__init__(message)
class VerifyError(LinkageError):
def __init__(self, message=""):
super().__init__(message)
class AbstractMethodError(LinkageError):
def __init__(self, message=""):
super().__init__(message)
class IncompatibleClassChangeError(LinkageError):
def __init__(self, message=""):
super().__init__(message)
class ExceptionInInitializerError(LinkageError):
def __init__(self, message="", cause=None):
super().__init__(message)
self._cause = cause
self.exception = cause
# ============================================================
# StackTraceElement
# ============================================================
@@ -753,6 +834,62 @@ class Integer(Number, Comparable):
return Integer(int(env_val))
return Integer(val) if val is not None else None
@staticmethod
def compare(x, y):
return -1 if x < y else (1 if x > y else 0)
@staticmethod
def compareUnsigned(x, y):
ux = x & 0xFFFFFFFF if x < 0 else x
uy = y & 0xFFFFFFFF if y < 0 else y
return -1 if ux < uy else (1 if ux > uy else 0)
@staticmethod
def divideUnsigned(dividend, divisor):
ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend
uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor
return ux // uy
@staticmethod
def remainderUnsigned(dividend, divisor):
ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend
uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor
return ux % uy
@staticmethod
def parseUnsignedInt(s, radix=10):
return int(s, radix)
@staticmethod
def toUnsignedString(i, radix=10):
if i < 0:
i = i & 0xFFFFFFFF
if radix == 10:
return str(i)
elif radix == 16:
return format(i, 'x')
elif radix == 2:
return format(i, 'b')
elif radix == 8:
return format(i, 'o')
return str(i)
@staticmethod
def toUnsignedLong(i):
return i & 0xFFFFFFFF
@staticmethod
def sum(a, b):
return a + b
@staticmethod
def max(a, b):
return a if a > b else b
@staticmethod
def min(a, b):
return a if a < b else b
def toString(self):
return String(str(self._value))
@@ -1418,9 +1555,92 @@ class Math:
@staticmethod
def copySign(m, s): return abs(m) * (1 if s >= 0 else -1)
@staticmethod
def floorDiv(x, y): return x // y if (x ^ y) >= 0 else -(abs(x) // abs(y))
def floorDiv(x, y):
return math.floor(x / y) if y != 0 else 0
@staticmethod
def floorMod(x, y): return x - Math.floorDiv(x, y) * y
def floorMod(x, y):
return x - Math.floorDiv(x, y) * y
@staticmethod
def subtractExact(x, y):
result = x - y
if isinstance(x, int) and isinstance(y, int):
if result > 0 and x < 0 and y > 0:
raise ArithmeticException("integer overflow")
if result < 0 and x > 0 and y < 0:
raise ArithmeticException("integer overflow")
return result
@staticmethod
def multiplyExact(x, y):
result = x * y
if isinstance(x, int) and isinstance(y, int):
if result // y != x:
raise ArithmeticException("integer overflow")
return result
@staticmethod
def incrementExact(a):
if a == 2147483647:
raise ArithmeticException("integer overflow")
return a + 1
@staticmethod
def decrementExact(a):
if a == -2147483648:
raise ArithmeticException("integer overflow")
return a - 1
@staticmethod
def negateExact(a):
if a == -2147483648:
raise ArithmeticException("integer overflow")
return -a
@staticmethod
def toIntExact(value):
if value < -2147483648 or value > 2147483647:
raise ArithmeticException("integer overflow")
return int(value)
@staticmethod
def log1p(x):
return math.log1p(x)
@staticmethod
def expm1(x):
return math.expm1(x)
@staticmethod
def ulp(x):
return math.ulp(x) if hasattr(math, 'ulp') else abs(x) * 2.220446049250313e-16
@staticmethod
def getExponent(x):
return math.frexp(x)[1] - 1
@staticmethod
def nextAfter(start, direction):
return math.nextafter(start, direction)
@staticmethod
def nextUp(x):
return math.nextafter(x, math.inf)
@staticmethod
def nextDown(x):
return math.nextafter(x, -math.inf)
@staticmethod
def rint(x):
return math.rint(x) if hasattr(math, 'rint') else float(math.floor(x + 0.5))
@staticmethod
def IEEEremainder(x, y):
return math.remainder(x, y) if hasattr(math, 'remainder') else x - y * math.floor(x / y)
@staticmethod
def compareUnsigned(x, y):
ux = x & 0xFFFFFFFF if x < 0 else x
uy = y & 0xFFFFFFFF if y < 0 else y
return -1 if ux < uy else (1 if ux > uy else 0)
@staticmethod
def divideUnsigned(dividend, divisor):
ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend
uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor
return ux // uy
@staticmethod
def remainderUnsigned(dividend, divisor):
ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend
uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor
return ux % uy
@staticmethod
def fma(a, b, c):
return a * b + c
class StrictMath(Math):
@@ -1529,6 +1749,39 @@ class System:
System.err = err_stream
@staticmethod
def lineSeparator(): return _os.linesep
@staticmethod
def getProperties():
return {
"java.version": "1.8.0",
"java.vendor": "Android-pyApi",
"java.vendor.url": "https://gitee.com/shuiyu1123/Android-pyApi",
"java.home": _os.environ.get("JAVA_HOME", ""),
"java.class.version": "52.0",
"os.name": platform.system(),
"os.arch": platform.machine(),
"os.version": platform.release(),
"file.separator": _os.sep,
"path.separator": _os.pathsep,
"line.separator": _os.linesep,
"user.name": _os.environ.get("USER", _os.environ.get("USERNAME", "")),
"user.home": _os.path.expanduser("~"),
"user.dir": _os.getcwd(),
}
@staticmethod
def setProperties(props):
System._properties = props
@staticmethod
def mapLibraryName(libname):
if platform.system() == "Windows":
return libname + ".dll"
elif platform.system() == "Darwin":
return "lib" + libname + ".dylib"
else:
return "lib" + libname + ".so"
@staticmethod
def getLogger(name):
import logging
return logging.getLogger(name)
in_ = _sys.stdin
out = _sys.stdout
err = _sys.stderr
@@ -1683,6 +1936,66 @@ class Thread(Runnable):
def getDefaultUncaughtExceptionHandler():
return Thread._default_uncaught_exception_handler
def getUncaughtExceptionHandler(self):
return getattr(self, '_uncaught_handler', Thread._default_uncaught_exception_handler)
def setUncaughtExceptionHandler(self, handler):
self._uncaught_handler = handler
@staticmethod
def getAllStackTraces():
import traceback
result = {}
# 只返回当前线程
result[Thread.currentThread()] = traceback.extract_stack()
return result
def getStackTrace(self):
import traceback
return traceback.extract_stack()
@staticmethod
def enumerate(array):
# 返回活动线程数
return 0
@staticmethod
def activeCount():
import threading
return threading.active_count()
def getContextClassLoader(self):
return getattr(self, '_context_classloader', None)
def setContextClassLoader(self, cl):
self._context_classloader = cl
def getThreadGroup(self):
return getattr(self, '_thread_group', None)
@staticmethod
def holdLock(obj):
return False
@staticmethod
def dumpStack():
import traceback
traceback.print_stack()
@staticmethod
def onSpinWait():
pass
def isVirtual(self):
return False
@staticmethod
def ofVirtual():
"""创建虚拟线程 (Java 21+)"""
t = Thread(target=lambda: None)
t._virtual = True
return t
class ThreadLocal(Stub):
"""java.lang.ThreadLocal"""
@@ -1744,6 +2057,35 @@ class StringBuilder(Stub):
def lastIndexOf(self, s, f=None):
return self.toString().rfind(s)
def ensureCapacity(self, minimumCapacity):
pass # Python list 自动扩容
def trimToSize(self):
pass
def setLength(self, newLength):
current = len(self._buf)
if newLength < current:
del self._buf[newLength:]
else:
self._buf.extend([" "] * (newLength - current))
def subSequence(self, start, end):
return "".join(self._buf[start:end])
def getChars(self, srcBegin, srcEnd, dst, dstBegin):
for i in range(srcBegin, srcEnd):
dst[dstBegin + i - srcBegin] = self._buf[i]
def codePointAt(self, index):
return ord(self._buf[index])
def codePointBefore(self, index):
return ord(self._buf[index - 1])
def codePointCount(self, beginIndex, endIndex):
return endIndex - beginIndex
class StringBuffer(StringBuilder):
"""java.lang.StringBuffer — 线程安全的可变字符序列"""
+200 -23
View File
@@ -24,7 +24,7 @@ __all__ = [
"InetAddress", "InetSocketAddress",
"Socket", "ServerSocket", "DatagramSocket", "DatagramPacket",
"Proxy", "ProxySelector", "Authenticator", "PasswordAuthentication",
"NetworkInterface", "URI",
"NetworkInterface", "InterfaceAddress", "URI",
"CookieHandler", "CookieManager", "HttpCookie",
"URLDecoder", "URLEncoder",
"SocketException", "ConnectException", "BindException",
@@ -1289,26 +1289,79 @@ class PasswordAuthentication(Stub):
return self._password
# ============================================================
# InterfaceAddress
# ============================================================
class InterfaceAddress(Stub):
"""java.net.InterfaceAddress — 接口地址信息 (地址/广播/前缀长度)"""
def __init__(self, address=None, broadcast=None, prefix_length=0):
super().__init__()
self._address = address # InetAddress
self._broadcast = broadcast # InetAddress or None
self._prefix_length = prefix_length
def getAddress(self):
return self._address
def getBroadcast(self):
return self._broadcast
def getNetworkPrefixLength(self):
return self._prefix_length
def equals(self, other):
if isinstance(other, InterfaceAddress):
return (self._address == other._address
and self._prefix_length == other._prefix_length)
return False
def hashCode(self):
return hash((repr(self._address), self._prefix_length)) & 0x7FFFFFFF
def toString(self):
addr = self._address.getHostAddress() if self._address else "0.0.0.0"
return f"{addr}/{self._prefix_length}"
# ============================================================
# NetworkInterface
# ============================================================
class NetworkInterface(Stub):
_cache = {}
_initialized = False
def __init__(self, name, display_name, addrs):
super().__init__()
self._name = name
self._display_name = display_name
self._addrs = addrs
self._mac = None # bytes, hardware address
self._mtu = 1500
self._state = "unknown"
self._flags = 0
self._interface_addrs = [] # List[InterfaceAddress]
@staticmethod
def _read_sys_file(path):
try:
with open(path, "r") as f:
return f.read().strip()
except Exception:
return None
@staticmethod
def getByName(name):
if name in NetworkInterface._cache:
return NetworkInterface._cache[name]
if name is None:
return None
NetworkInterface._ensure_init()
return NetworkInterface._cache.get(name)
@staticmethod
def getByInetAddress(addr):
NetworkInterface._ensure_init()
for ni in NetworkInterface._cache.values():
for a in ni._addrs:
if a.equals(addr):
@@ -1317,8 +1370,18 @@ class NetworkInterface(Stub):
@staticmethod
def getNetworkInterfaces():
NetworkInterface._ensure_init()
return list(NetworkInterface._cache.values())
@staticmethod
def _ensure_init():
if not NetworkInterface._initialized:
NetworkInterface._initialized = True
try:
NetworkInterface._init_from_socket()
except Exception:
pass
def getName(self):
return self._name
@@ -1329,25 +1392,25 @@ class NetworkInterface(Stub):
return iter(self._addrs)
def getInterfaceAddresses(self):
return self._addrs
return list(self._interface_addrs)
def isUp(self):
return True
return self._state.lower() == "up" or (self._flags & 0x1) != 0
def isLoopback(self):
return self._name == "lo"
return (self._flags & 0x8) != 0 or self._name == "lo"
def isPointToPoint(self):
return False
return (self._flags & 0x10) != 0
def supportsMulticast(self):
return True
return (self._flags & 0x1000) != 0
def getHardwareAddress(self):
return None
return self._mac
def getMTU(self):
return 1500
return self._mtu
def getSubInterfaces(self):
return iter([])
@@ -1363,25 +1426,139 @@ class NetworkInterface(Stub):
@staticmethod
def _init_from_socket():
import netifaces
for iface_name in netifaces.interfaces():
"""从 /sys/class/net/ 和 socket/fcntl 枚举真实网络接口 (不依赖 netifaces)"""
import os as _os
net_dir = "/sys/class/net"
if not _os.path.isdir(net_dir):
# 非 Linux 环境: 尝试用 socket 获取本机地址
try:
hostname = _socket.gethostname()
addr = InetAddress.getByName(hostname)
ni = NetworkInterface(hostname, hostname, [addr])
NetworkInterface._cache[hostname] = ni
except Exception:
pass
return
# 尝试用 fcntl.ioctl 获取 IPv4 地址/掩码/广播
fcntl_available = True
try:
import fcntl as _fcntl
except ImportError:
fcntl_available = False
for iface_name in _os.listdir(net_dir):
addrs = []
ifaddresses = netifaces.ifaddresses(iface_name)
for family in [netifaces.AF_INET, netifaces.AF_INET6]:
if family in ifaddresses:
for addr_info in ifaddresses[family]:
interface_addrs = []
mac = None
mtu = 1500
state = "unknown"
flags = 0
# --- MAC 地址 ---
mac_str = NetworkInterface._read_sys_file(f"{net_dir}/{iface_name}/address")
if mac_str:
try:
mac = bytes(int(b, 16) for b in mac_str.split(":"))
except Exception:
mac = None
# --- MTU ---
mtu_str = NetworkInterface._read_sys_file(f"{net_dir}/{iface_name}/mtu")
if mtu_str:
try:
mtu = int(mtu_str)
except ValueError:
pass
# --- operstate ---
state = NetworkInterface._read_sys_file(
f"{net_dir}/{iface_name}/operstate") or "unknown"
# --- flags (十六进制字符串, 如 0x1003) ---
flags_str = NetworkInterface._read_sys_file(
f"{net_dir}/{iface_name}/flags")
if flags_str:
try:
flags = int(flags_str, 16)
except ValueError:
pass
# --- IPv4 地址 (fcntl.ioctl) ---
if fcntl_available:
sock = None
try:
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
packed = _struct.pack('256s', iface_name.encode()[:15])
# SIOCGIFADDR = 0x8915
result = _fcntl.ioctl(sock.fileno(), 0x8915, packed)
ip_raw = result[20:24]
inet_addr = InetAddress(iface_name, bytes(ip_raw))
addrs.append(inet_addr)
# SIOCGIFNETMASK = 0x891b
prefix = 24
try:
result = _fcntl.ioctl(sock.fileno(), 0x891b, packed)
netmask_str = _socket.inet_ntoa(result[20:24])
prefix = sum(bin(int(b)).count('1') for b in netmask_str.split('.'))
except Exception:
pass
# SIOCGIFBRDADDR = 0x8919 (广播地址)
broadcast_addr = None
try:
result = _fcntl.ioctl(sock.fileno(), 0x8919, packed)
bcast_raw = result[20:24]
broadcast_addr = InetAddress(iface_name, bytes(bcast_raw))
except Exception:
pass
interface_addrs.append(InterfaceAddress(
address=inet_addr,
broadcast=broadcast_addr,
prefix_length=prefix
))
except OSError:
pass # 接口没有 IPv4 地址
finally:
if sock is not None:
try:
addr = addr_info["addr"]
if "." in addr:
addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET, addr)))
else:
addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET6, addr)))
sock.close()
except Exception:
pass
NetworkInterface._cache[iface_name] = NetworkInterface(iface_name, iface_name, addrs)
# --- IPv6 地址 (/proc/net/if_inet6) ---
try:
with open("/proc/net/if_inet6", "r") as f:
for line in f:
parts = line.split()
if len(parts) >= 6 and parts[5] == iface_name:
hex_addr = parts[0]
raw = bytes.fromhex(hex_addr)
prefix6 = int(parts[2], 16) if parts[2] else 64
inet6_addr = InetAddress(iface_name, raw)
addrs.append(inet6_addr)
interface_addrs.append(InterfaceAddress(
address=inet6_addr,
broadcast=None,
prefix_length=prefix6
))
except Exception:
pass
ni = NetworkInterface(iface_name, iface_name, addrs)
ni._mac = mac
ni._mtu = mtu
ni._state = state
ni._flags = flags
ni._interface_addrs = interface_addrs
NetworkInterface._cache[iface_name] = ni
# 初始化缓存
try:
NetworkInterface._init_from_socket()
NetworkInterface._ensure_init()
except Exception:
pass
+396
View File
@@ -148,6 +148,240 @@ class Enumeration(Stub):
return Iterator(self._it)
# ============================================================
# Stream / Spliterator helpers (lightweight, stdlib-backed)
# ============================================================
class _Spliterator(Stub):
"""Lightweight java.util.Spliterator backed by a Python sequence."""
ORDERED = 0x00000010
DISTINCT = 0x00000001
SORTED = 0x00000004
SIZED = 0x00000040
NONNULL = 0x00000100
IMMUTABLE = 0x00000400
CONCURRENT = 0x00001000
SUBSIZED = 0x00004000
def __init__(self, data, characteristics: int = 0):
super().__init__()
self._data = list(data)
self._index = 0
self._characteristics = (characteristics
| _Spliterator.SIZED
| _Spliterator.SUBSIZED)
def tryAdvance(self, action) -> bool:
if self._index < len(self._data):
action(self._data[self._index])
self._index += 1
return True
return False
def forEachRemaining(self, action) -> None:
while self._index < len(self._data):
action(self._data[self._index])
self._index += 1
def trySplit(self):
mid = (self._index + len(self._data)) // 2
if self._index >= mid:
return None
split = _Spliterator(self._data[self._index:mid],
self._characteristics)
self._index = mid
return split
def estimateSize(self) -> int:
return len(self._data) - self._index
def getExactSizeIfKnown(self) -> int:
return self.estimateSize()
def characteristics(self) -> int:
return self._characteristics
def hasCharacteristics(self, characteristics: int) -> bool:
return (self._characteristics & characteristics) == characteristics
def iterator(self) -> Iterator:
return Iterator(self._data[self._index:])
class _Stream(Stub):
"""Lightweight java.util.stream.Stream wrapper over a Python list."""
def __init__(self, data):
super().__init__()
self._data = list(data)
self._parallel = False
def iterator(self) -> Iterator:
return Iterator(self._data)
def spliterator(self) -> _Spliterator:
return _Spliterator(self._data)
def isParallel(self) -> bool:
return self._parallel
def parallel(self) -> '_Stream':
self._parallel = True
return self
def sequential(self) -> '_Stream':
self._parallel = False
return self
def unordered(self) -> '_Stream':
return self
def filter(self, predicate) -> '_Stream':
return _Stream([x for x in self._data if predicate(x)])
def map(self, mapper) -> '_Stream':
return _Stream([mapper(x) for x in self._data])
def mapToInt(self, mapper) -> '_Stream':
return _Stream([mapper(x) for x in self._data])
def mapToLong(self, mapper) -> '_Stream':
return _Stream([mapper(x) for x in self._data])
def mapToDouble(self, mapper) -> '_Stream':
return _Stream([mapper(x) for x in self._data])
def flatMap(self, mapper) -> '_Stream':
result = []
for x in self._data:
part = mapper(x)
if isinstance(part, (list, tuple)):
result.extend(part)
else:
result.extend(list(part))
return _Stream(result)
def peek(self, action) -> '_Stream':
for x in self._data:
action(x)
return self
def distinct(self) -> '_Stream':
seen = []
seen_keys = set()
for x in self._data:
key = x if isinstance(x, (int, str, tuple, frozenset)) else id(x)
if key not in seen_keys:
seen_keys.add(key)
seen.append(x)
return _Stream(seen)
def sorted(self, key=None, reverse: bool = False) -> '_Stream':
return _Stream(sorted(self._data, key=key, reverse=reverse))
def limit(self, maxSize: int) -> '_Stream':
return _Stream(self._data[:maxSize])
def skip(self, n: int) -> '_Stream':
return _Stream(self._data[n:])
def forEach(self, action) -> None:
for x in self._data:
action(x)
def forEachOrdered(self, action) -> None:
for x in self._data:
action(x)
def toArray(self, generator=None):
if generator is None:
return list(self._data)
return list(self._data)
def reduce(self, *args):
if len(args) == 1:
accumulator = args[0]
it = iter(self._data)
try:
acc = next(it)
except StopIteration:
return Optional.empty()
for x in it:
acc = accumulator(acc, x)
return Optional.of(acc)
elif len(args) == 2:
identity, accumulator = args
acc = identity
for x in self._data:
acc = accumulator(acc, x)
return acc
else:
identity, accumulator, combiner = args
acc = identity
for x in self._data:
acc = accumulator(acc, x)
return acc
def collect(self, *args):
if not args:
return list(self._data)
if len(args) == 1:
collector = args[0]
supplier = getattr(collector, 'supplier', None)
accumulator = getattr(collector, 'accumulator', None)
finisher = getattr(collector, 'finisher', None)
if supplier is not None and accumulator is not None:
container = supplier() if callable(supplier) else supplier
for x in self._data:
if callable(accumulator):
accumulator(container, x)
if finisher is not None and callable(finisher):
return finisher(container)
return container
if callable(collector):
return collector(list(self._data))
return list(self._data)
supplier, accumulator, combiner = args[0], args[1], args[2]
container = supplier() if callable(supplier) else supplier
for x in self._data:
if callable(accumulator):
accumulator(container, x)
return container
def count(self) -> int:
return len(self._data)
def findFirst(self):
return Optional.of(self._data[0]) if self._data else Optional.empty()
def findAny(self):
return self.findFirst()
def anyMatch(self, predicate) -> bool:
return any(predicate(x) for x in self._data)
def allMatch(self, predicate) -> bool:
return all(predicate(x) for x in self._data)
def noneMatch(self, predicate) -> bool:
return not any(predicate(x) for x in self._data)
def min(self, key=None):
if not self._data:
return Optional.empty()
return Optional.of(min(self._data, key=key) if key else min(self._data))
def max(self, key=None):
if not self._data:
return Optional.empty()
return Optional.of(max(self._data, key=key) if key else max(self._data))
def onClose(self, close_handler) -> '_Stream':
return self
def close(self) -> None:
pass
# ============================================================
# ArrayList
# ============================================================
@@ -294,6 +528,12 @@ class ArrayList(Stub):
def replaceAll(self, operator) -> None:
self._data = [operator(x) for x in self._data]
def spliterator(self) -> _Spliterator:
return _Spliterator(self._data, _Spliterator.ORDERED)
def stream(self) -> _Stream:
return _Stream(self._data)
def equals(self, other) -> bool:
if isinstance(other, ArrayList):
return self._data == other._data
@@ -789,6 +1029,10 @@ class LinkedHashMap(HashMap):
key = next(iter(self._data))
return (key, self._data[key])
def clone(self) -> 'LinkedHashMap':
result = LinkedHashMap(self._data, 0.75, self._access_order)
return result
# ============================================================
# TreeMap
@@ -993,6 +1237,48 @@ class TreeMap(Stub):
return True
return False
def replaceAll(self, function) -> None:
for k in list(self._data.keys()):
self._data[k] = function(k, self._data[k])
def compute(self, key, remapping_function):
old = self._data.get(key)
new_val = remapping_function(key, old)
if new_val is not None:
self._data[key] = new_val
return new_val
self._data.pop(key, None)
return None
def computeIfAbsent(self, key, mapping_function):
if key not in self._data:
new_val = mapping_function(key)
if new_val is not None:
self._data[key] = new_val
return new_val
return self._data.get(key)
def computeIfPresent(self, key, remapping_function):
if key in self._data:
new_val = remapping_function(key, self._data[key])
if new_val is not None:
self._data[key] = new_val
return new_val
self._data.pop(key, None)
return None
def merge(self, key, value, remapping_function):
old = self._data.get(key)
if old is None:
self._data[key] = value
return value
new_val = remapping_function(old, value)
if new_val is not None:
self._data[key] = new_val
return new_val
self._data.pop(key, None)
return None
def size(self) -> int:
return len(self._data)
@@ -1140,6 +1426,19 @@ class HashSet(Stub):
for item in list(self._data):
action(item)
def removeIf(self, predicate) -> bool:
old_size = len(self._data)
to_remove = [x for x in self._data if predicate(x)]
for x in to_remove:
self._data.discard(x)
return len(self._data) != old_size
def spliterator(self) -> _Spliterator:
return _Spliterator(self._data, _Spliterator.DISTINCT)
def stream(self) -> _Stream:
return _Stream(self._data)
def equals(self, other) -> bool:
if isinstance(other, HashSet):
return self._data == other._data
@@ -1848,6 +2147,60 @@ class Collections:
return _LifoQueue(deque)
@staticmethod
def addAll(collection, *elements):
for e in elements:
collection.add(e)
return len(elements) > 0
@staticmethod
def emptyList():
return []
@staticmethod
def emptySet():
return set()
@staticmethod
def emptyMap():
return {}
@staticmethod
def reverseOrder(comparator=None):
if comparator is None:
return lambda a, b: -1 if a < b else (1 if a > b else 0)
return lambda a, b: -comparator(a, b)
@staticmethod
def singletonSet(element):
s = HashSet()
s.add(element)
return s
@staticmethod
def checkedList(lst, type_):
return lst
@staticmethod
def checkedSet(s, type_):
return s
@staticmethod
def checkedMap(m, keyType, valueType):
return m
@staticmethod
def unmodifiableCollection(c):
return c
@staticmethod
def unmodifiableQueue(q):
return q
@staticmethod
def unmodifiableDeque(d):
return d
# ============================================================
# Arrays
@@ -2334,6 +2687,37 @@ class Calendar(Stub):
def getInstance() -> 'GregorianCalendar':
return GregorianCalendar()
def clear(self, field=None):
if field is None:
self._fields = {}
else:
if field in self._fields:
del self._fields[field]
def isSet(self, field):
return field in self._fields
def getCalendarType(self):
return "gregory"
@staticmethod
def getAvailableLocales():
from .util import Locale
return [Locale.US, Locale.CHINA, Locale.JAPAN, Locale.KOREA, Locale.GERMANY, Locale.FRANCE]
def toInstant(self):
from .time import Instant
return Instant.ofEpochMilli(self.getTimeInMillis())
def getWeekYear(self):
return self.get(Calendar.YEAR)
def isWeekDateSupported(self):
return False
def getWeeksInWeekYear(self):
return 52
class GregorianCalendar(Calendar):
"""java.util.GregorianCalendar"""
@@ -3360,6 +3744,18 @@ class PriorityQueue(Stub):
for item in list(self._heap):
action(item)
def spliterator(self) -> _Spliterator:
if self._comparator:
items = [v[1] for v in self._heap]
else:
items = list(self._heap)
return _Spliterator(items)
def clone(self) -> 'PriorityQueue':
result = PriorityQueue(comparator=self._comparator)
result._heap = list(self._heap)
return result
def toString(self) -> str:
items = ", ".join(str(x) for x in self._heap)
return f"[{items}]"
+52
View File
@@ -115,6 +115,41 @@ class JSONObject(Stub):
self._map[k] = arr
return self
def append(self, key, value):
"""Append value to the JSONArray under key.
If the key is not present a new JSONArray containing value is created.
If the key maps to a JSONArray the value is appended to it.
Otherwise the existing value and the new value are placed into a new
JSONArray.
"""
k = str(key)
current = self._map.get(k)
if isinstance(current, JSONArray):
current.put(value)
else:
arr = JSONArray()
if current is not None and current is not JSONObject.NULL:
arr.put(current)
arr.put(value)
self._map[k] = arr
return self
def putOnce(self, key, value):
"""Put a key/value pair only if the key is not already present."""
if key is None:
raise JSONException("Null key.")
k = str(key)
if k not in self._map:
self._map[k] = JSONObject._wrap(value)
return self
def putOpt(self, key, value):
"""Put a key/value pair, but only if key and value are both non-null."""
if key is not None and value is not None:
return self.put(key, value)
return self
def get(self, key):
k = str(key)
if k not in self._map:
@@ -239,6 +274,14 @@ class JSONObject(Stub):
def has(self, key):
return str(key) in self._map
def isNull(self, key):
"""Returns true if this object has no mapping for key or if it has a
mapping whose value is JSONObject.NULL."""
k = str(key)
if k not in self._map:
return True
return self._map[k] is JSONObject.NULL
def remove(self, key):
k = str(key)
if k in self._map:
@@ -255,6 +298,15 @@ class JSONObject(Stub):
arr.put(k)
return arr
@staticmethod
def getNames(jsonObject):
"""Returns an array of field names, or None if the object is None/empty."""
if jsonObject is None:
return None
if not jsonObject._map:
return None
return [str(k) for k in jsonObject._map.keys()]
def length(self):
return len(self._map)
+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)