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行
This commit is contained in:
shuiyu1123 committed 2026-06-25 00:17:50 +00:00
1 parent 39b1da37c9
commit 4235f8692c
13 files changed
+2248 -163

No files matched your search

+2
View File
@@ -503,6 +503,7 @@ __all__ = [
"UwbManager", "UwbManager",
"View", "ViewGroup", "View", "ViewGroup",
"VirtualDisplay", "VirtualDisplay",
"Vibrator", "VibratorEffect",
"VpnManager", "VpnService", "VpnManager", "VpnService",
"WebView", "WebView",
"WifiAwareManager", "WifiAwareManager",
@@ -514,6 +515,7 @@ __all__ = [
"InputStreamReader", "OutputStreamWriter", "InputStreamReader", "OutputStreamWriter",
"URL", "HttpURLConnection", "Socket", "ServerSocket", "URL", "HttpURLConnection", "Socket", "ServerSocket",
"ArrayList", "HashMap", "HashSet", "LinkedList", "ArrayList", "HashMap", "HashSet", "LinkedList",
"TreeMap", "LinkedHashMap", "PriorityQueue",
"Collections", "Arrays", "Calendar", "Date", "Random", "Collections", "Arrays", "Calendar", "Date", "Random",
"UUID", "Scanner", "Timer", "TimerTask", "UUID", "Scanner", "Timer", "TimerTask",
"Pattern", "Matcher", "Pattern", "Matcher",
+285 -27
View File
@@ -4,6 +4,7 @@ Android Content 组件 — Context, ContentResolver, Uri, SharedPreferences 等
""" """
import json import json
import mimetypes
import os import os
import sys import sys
import threading import threading
@@ -372,34 +373,134 @@ class ContentResolver(Stub):
super().__init__() super().__init__()
self._observers: Dict[str, List[Any]] = {} self._observers: Dict[str, List[Any]] = {}
@staticmethod
def _uri_to_file_path(uri) -> Optional[str]:
"""将 file:// 或纯路径 Uri 转换为本地文件系统路径"""
try:
uri_str = str(uri)
except Exception:
return None
# 处理字符串输入
if isinstance(uri, str):
if uri.startswith("file://"):
return uri[7:]
if "://" not in uri:
return uri if uri else None
return None
scheme = getattr(uri, "_scheme", "") or ""
path = getattr(uri, "_path", "") or ""
if scheme == "file":
return path if path else None
if scheme == "" and path:
return path
return None
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str], def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str], selection_args: Optional[List[str]], sort_order: Optional[str],
cancellation_signal: Any = None) -> Any: cancellation_signal: Any = None) -> Any:
"""Query content provider""" """Query content provider. 对 file:// URI 返回基于目录列表的 Cursor。"""
sys.stderr.write(f"[ContentResolver] query: uri={uri}\n") path = self._uri_to_file_path(uri)
if path is not None and os.path.isdir(path):
try:
from .database import Cursor, MatrixCursor
cols = projection or ["_id", "name", "size", "last_modified"]
cursor = MatrixCursor(cols)
for idx, name in enumerate(sorted(os.listdir(path))):
full = os.path.join(path, name)
try:
st = os.stat(full)
size = st.st_size
mtime = int(st.st_mtime * 1000)
except OSError:
size = 0
mtime = 0
cursor.addRow([idx, name, size, mtime])
return cursor
except Exception:
pass
return None return None
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]: def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
sys.stderr.write(f"[ContentResolver] insert: uri={uri}\n") """Insert. 对 file:// URI 将 ContentValues 写为 JSON 文件。"""
path = self._uri_to_file_path(uri)
if path is not None and isinstance(values, ContentValues):
try:
os.makedirs(path if os.path.isdir(path) else os.path.dirname(path) or ".", exist_ok=True)
target = path if not os.path.isdir(path) else os.path.join(path, "insert.json")
data = values._values if hasattr(values, "_values") else {}
with open(target, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, default=str)
return Uri.withAppendedPath(uri, os.path.basename(target))
except OSError:
pass
return uri return uri
def update(self, uri: Uri, values: ContentValues, selection: Optional[str], def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int: selection_args: Optional[List[str]]) -> int:
sys.stderr.write(f"[ContentResolver] update: uri={uri}\n") """Update. 对 file:// URI 更新对应 JSON 文件。"""
path = self._uri_to_file_path(uri)
if path is not None and isinstance(values, ContentValues) and os.path.isfile(path):
try:
data = values._values if hasattr(values, "_values") else {}
with open(path, "r", encoding="utf-8") as f:
existing = json.load(f)
if isinstance(existing, dict):
existing.update(data)
with open(path, "w", encoding="utf-8") as f:
json.dump(existing, f, ensure_ascii=False, default=str)
return 1
except (OSError, ValueError):
pass
return 0 return 0
def delete(self, uri: Uri, selection: Optional[str], def delete(self, uri: Uri, selection: Optional[str],
selection_args: Optional[List[str]]) -> int: selection_args: Optional[List[str]]) -> int:
sys.stderr.write(f"[ContentResolver] delete: uri={uri}\n") """Delete. 对 file:// URI 删除对应文件或目录。"""
path = self._uri_to_file_path(uri)
if path is not None:
try:
if os.path.isfile(path):
os.remove(path)
return 1
if os.path.isdir(path):
import shutil
shutil.rmtree(path)
return 1
except OSError:
pass
return 0 return 0
def applyBatch(self, authority: str, operations: List[Any]) -> List[Any]: def applyBatch(self, authority: str, operations: List[Any]) -> List[Any]:
sys.stderr.write(f"[ContentResolver] applyBatch: authority={authority}\n") """Apply batch operations, returning per-op results."""
return [] results: List[Any] = []
for op in operations:
try:
op_type = getattr(op, "_type", None)
uri = getattr(op, "_uri", None)
values = getattr(op, "_values", None)
if op_type == 1 and uri is not None: # INSERT
results.append(ContentProviderResult(uri=self.insert(uri, values or ContentValues())))
elif op_type == 2 and uri is not None: # UPDATE
count = self.update(uri, values or ContentValues(),
getattr(op, "_selection", None),
getattr(op, "_selection_args", None))
results.append(ContentProviderResult(count=count))
elif op_type == 3 and uri is not None: # DELETE
count = self.delete(uri, getattr(op, "_selection", None),
getattr(op, "_selection_args", None))
results.append(ContentProviderResult(count=count))
else:
results.append(ContentProviderResult(count=0))
except Exception:
results.append(ContentProviderResult(count=0))
return results
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int: def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
sys.stderr.write(f"[ContentResolver] bulkInsert: uri={uri}\n") count = 0
return len(values) if values else 0 for v in values or []:
if self.insert(uri, v) is not None:
count += 1
return count
def call(self, uri: Uri, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]: def call(self, uri: Uri, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
return None return None
@@ -426,7 +527,17 @@ class ContentResolver(Stub):
if not self._observers[uri_str]: if not self._observers[uri_str]:
del self._observers[uri_str] del self._observers[uri_str]
def getType(self, uri: Uri) -> Optional[str]: def getType(self, uri) -> Optional[str]:
"""返回 URI 指向资源的 MIME 类型。"""
path = self._uri_to_file_path(uri)
if path:
mime, _ = mimetypes.guess_type(path)
return mime or "application/octet-stream"
scheme = getattr(uri, "_scheme", "") or ""
if isinstance(uri, str) and "://" in uri:
scheme = uri.split("://")[0]
if scheme == "content":
return "vnd.android.cursor.dir/vnd.android.content"
return None return None
def acquireContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]: def acquireContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]:
@@ -436,12 +547,40 @@ class ContentResolver(Stub):
return ContentProviderClient(uri) return ContentProviderClient(uri)
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
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: def openInputStream(self, uri: Uri) -> Any:
"""打开输入流。对 file:// URI 返回真实文件流。"""
path = self._uri_to_file_path(uri)
if path and os.path.isfile(path):
try:
return open(path, "rb")
except OSError:
return None
return None return None
def openOutputStream(self, uri: Uri) -> Any: def openOutputStream(self, uri: Uri) -> Any:
"""打开输出流。对 file:// URI 返回真实文件流。"""
path = self._uri_to_file_path(uri)
if path:
try:
d = os.path.dirname(path)
if d:
os.makedirs(d, exist_ok=True)
return open(path, "wb")
except OSError:
return None
return None return None
@@ -594,10 +733,16 @@ class Context(Stub):
return PackageManager() return PackageManager()
def getResources(self): def getResources(self):
return None from .content_res import Resources, AssetManager
if not hasattr(self, "_resources") or self._resources is None:
self._resources = Resources(self.getAssets())
return self._resources
def getAssets(self): def getAssets(self):
return None from .content_res import AssetManager
if not hasattr(self, "_assets") or self._assets is None:
self._assets = AssetManager()
return self._assets
def getMainLooper(self): def getMainLooper(self):
from .os import Looper from .os import Looper
@@ -668,15 +813,34 @@ class Context(Stub):
pass pass
def getString(self, res_id: int, *args) -> str: def getString(self, res_id: int, *args) -> str:
"""返回资源字符串。优先委托给 Resources,否则返回合理默认值。"""
try:
res = self.getResources()
val = res.getString(res_id, *args)
if val is not None:
return val
except Exception:
pass
return str(res_id) return str(res_id)
def getColor(self, color_id: int) -> int: def getColor(self, color_id: int) -> int:
return 0 """返回资源颜色。优先委托给 Resources,否则返回不透明黑色。"""
try:
res = self.getResources()
return res.getColor(color_id)
except Exception:
return 0xFF000000
def getDrawable(self, drawable_id: int): def getDrawable(self, drawable_id: int):
return None """返回 Drawable。优先委托给 Resources。"""
try:
res = self.getResources()
return res.getDrawable(drawable_id)
except Exception:
return None
def getWallpaper(self): def getWallpaper(self):
"""返回壁纸 Drawable。桌面端返回 None(无系统壁纸)。"""
return None return None
def getWallpaperDesiredMinimumWidth(self) -> int: def getWallpaperDesiredMinimumWidth(self) -> int:
@@ -795,18 +959,55 @@ class Context(Stub):
self._theme = resid self._theme = resid
def getText(self, resId): def getText(self, resId):
"""返回资源文本。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getText(resId)
if val is not None:
return val
except Exception:
pass
return str(resId) return str(resId)
def getQuantityString(self, resId, quantity, *formatArgs): def getQuantityString(self, resId, quantity, *formatArgs):
return str(resId) """返回复数字符串。优先委托给 Resources。"""
try:
res = self.getResources()
return res.getQuantityString(resId, quantity, *formatArgs)
except Exception:
return str(resId)
def getStringArray(self, resId): def getStringArray(self, resId):
"""返回字符串数组。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getStringArray(resId)
if val is not None:
return val
except Exception:
pass
return [] return []
def getTextArray(self, resId): def getTextArray(self, resId):
"""返回文本数组。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getTextArray(resId)
if val is not None:
return val
except Exception:
pass
return [] return []
def getIntArray(self, resId): def getIntArray(self, resId):
"""返回整型数组。优先委托给 Resources。"""
try:
res = self.getResources()
val = res.getIntArray(resId)
if val is not None:
return val
except Exception:
pass
return [] return []
# ─── Context 派生 / 回调 (扩展) ─────────────────────────── # ─── Context 派生 / 回调 (扩展) ───────────────────────────
@@ -1386,29 +1587,55 @@ class ContentProvider(Stub):
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str], def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]: selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]:
"""默认实现:委托给关联 Context 的 ContentResolver。"""
if self._context is not None:
return self._context.getContentResolver().query(
uri, projection, selection, selection_args, sort_order)
return None return None
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]: def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
if self._context is not None:
return self._context.getContentResolver().insert(uri, values)
return None return None
def update(self, uri: Uri, values: ContentValues, selection: Optional[str], def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int: selection_args: Optional[List[str]]) -> int:
if self._context is not None:
return self._context.getContentResolver().update(uri, values, selection, selection_args)
return 0 return 0
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int: def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int:
if self._context is not None:
return self._context.getContentResolver().delete(uri, selection, selection_args)
return 0 return 0
def getType(self, uri: Uri) -> Optional[str]: def getType(self, uri: Uri) -> Optional[str]:
"""返回 URI 的 MIME 类型。"""
path = ContentResolver._uri_to_file_path(uri)
if path:
mime, _ = mimetypes.guess_type(path)
return mime or "application/octet-stream"
return None return None
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
return None mime = self.getType(uri)
if mime is None:
return None
return [mime]
def openFile(self, uri: Uri, mode: str) -> Any: def openFile(self, uri: Uri, mode: str) -> Any:
"""打开文件。对 file:// URI 返回真实文件对象。"""
path = ContentResolver._uri_to_file_path(uri)
if path and os.path.exists(path):
try:
fmode = "rb" if "r" in mode else "wb"
return open(path, fmode)
except OSError:
return None
return None return None
def openAssetFile(self, uri: Uri, mode: str) -> Any: def openAssetFile(self, uri: Uri, mode: str) -> Any:
return None return self.openFile(uri, mode)
def attachInfo(self, context: Context, info: Any): def attachInfo(self, context: Context, info: Any):
self._context = context self._context = context
@@ -1444,29 +1671,44 @@ class ContentProviderClient(Stub):
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str], def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]: selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]:
return None """委托给 ContentResolver 处理 file:// URI。"""
resolver = ContentResolver()
return resolver.query(uri, projection, selection, selection_args, sort_order)
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]: def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
return uri resolver = ContentResolver()
return resolver.insert(uri, values)
def update(self, uri: Uri, values: ContentValues, selection: Optional[str], def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
selection_args: Optional[List[str]]) -> int: selection_args: Optional[List[str]]) -> int:
return 0 resolver = ContentResolver()
return resolver.update(uri, values, selection, selection_args)
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int: def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int:
return 0 resolver = ContentResolver()
return resolver.delete(uri, selection, selection_args)
def applyBatch(self, operations: List[Any]) -> List[Any]: def applyBatch(self, operations: List[Any]) -> List[Any]:
return [] resolver = ContentResolver()
return resolver.applyBatch("", operations)
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int: def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
return len(values) if values else 0 resolver = ContentResolver()
return resolver.bulkInsert(uri, values)
def getType(self, uri: Uri) -> Optional[str]: def getType(self, uri: Uri) -> Optional[str]:
"""返回 URI 的 MIME 类型。"""
path = ContentResolver._uri_to_file_path(uri)
if path:
mime, _ = mimetypes.guess_type(path)
return mime or "application/octet-stream"
return None return None
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
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]: def call(self, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
return None return None
@@ -1478,13 +1720,21 @@ class ContentProviderClient(Stub):
pass pass
def openFile(self, uri: Uri, mode: str) -> Any: def openFile(self, uri: Uri, mode: str) -> Any:
"""打开文件。对 file:// URI 返回真实文件对象。"""
path = ContentResolver._uri_to_file_path(uri)
if path and os.path.exists(path):
try:
fmode = "rb" if "r" in mode else "wb"
return open(path, fmode)
except OSError:
return None
return None return None
def openAssetFile(self, uri: Uri, mode: str) -> Any: def openAssetFile(self, uri: Uri, mode: str) -> Any:
return None return self.openFile(uri, mode)
def openTypedAssetFile(self, uri: Uri, mime_type: str, opts: Optional[ContentValues]) -> Any: def openTypedAssetFile(self, uri: Uri, mime_type: str, opts: Optional[ContentValues]) -> Any:
return None return self.openFile(uri, "r")
def getLocalContentProvider(self) -> Optional[ContentProvider]: def getLocalContentProvider(self) -> Optional[ContentProvider]:
return None return None
@@ -1658,6 +1908,14 @@ class CursorLoader(Loader):
self._sort_order = sort_order self._sort_order = sort_order
def loadInBackground(self): def loadInBackground(self):
"""在后台加载数据,委托给 ContentResolver.query。"""
if self._context is not None:
try:
resolver = self._context.getContentResolver()
return resolver.query(self._uri, self._projection, self._selection,
self._selection_args, self._sort_order)
except Exception:
pass
return None return None
+23
View File
@@ -36,6 +36,8 @@ class Resources(Stub):
self._drawables: Dict[str, Any] = {} self._drawables: Dict[str, Any] = {}
self._booleans: Dict[str, bool] = {} self._booleans: Dict[str, bool] = {}
self._integers: Dict[str, int] = {} self._integers: Dict[str, int] = {}
self._string_arrays: Dict[str, List[str]] = {}
self._int_arrays: Dict[str, List[int]] = {}
self._identifiers: Dict[str, int] = {} self._identifiers: Dict[str, int] = {}
self._next_id = 0x7f010000 self._next_id = 0x7f010000
self._configuration = Configuration() self._configuration = Configuration()
@@ -125,6 +127,27 @@ class Resources(Stub):
return val return val
return 0 return 0
# ── Arrays ──
def getStringArray(self, id: Union[int, str]) -> List[str]:
if isinstance(id, str):
return self._string_arrays.get(id, [])
for key, val in self._string_arrays.items():
if self._identifiers.get(key) == id:
return val
return []
def getTextArray(self, id: Union[int, str]) -> List[str]:
return self.getStringArray(id)
def getIntArray(self, id: Union[int, str]) -> List[int]:
if isinstance(id, str):
return self._int_arrays.get(id, [])
for key, val in self._int_arrays.items():
if self._identifiers.get(key) == id:
return val
return []
# ── Layout / Animation / Xml ── # ── Layout / Animation / Xml ──
def getLayout(self, id: Union[int, str]) -> Any: def getLayout(self, id: Union[int, str]) -> Any:
+598 -40
View File
@@ -8,6 +8,7 @@ import math
import struct import struct
import io import io
import os import os
import time
from enum import IntEnum from enum import IntEnum
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
@@ -732,6 +733,42 @@ class Path(Stub):
pass pass
def op(self, other: "Path", op: int) -> bool: def op(self, other: "Path", op: int) -> bool:
"""对两个路径执行布尔运算。基于包围盒的近似实现。"""
if other is None:
return False
self_bounds = self.computeBounds()
other_bounds = other.computeBounds()
if op == Path.Op.UNION:
# 合并:将 other 的段追加到 self
self.addPath(other)
return True
if op == Path.Op.INTERSECT:
# 交集:仅当包围盒相交时保留相交区域
if not RectF.intersects(self_bounds, other_bounds):
self.reset()
return False
# 用相交包围盒作为近似结果
inter = RectF(self_bounds.left, self_bounds.top,
self_bounds.right, self_bounds.bottom)
inter.intersect(other_bounds.left, other_bounds.top,
other_bounds.right, other_bounds.bottom)
self.reset()
self.addRect(inter.left, inter.top, inter.right, inter.bottom)
return True
if op == Path.Op.DIFFERENCE:
# 差集:若包围盒不相交则 self 不变,否则标记(近似)
if not RectF.intersects(self_bounds, other_bounds):
return True
# 近似:无法精确计算,保留 self 不变
return True
if op == Path.Op.REVERSE_DIFFERENCE:
# 反向差集:用 other 替换 self
self.set(other)
return True
if op == Path.Op.XOR:
# 异或:合并两者
self.addPath(other)
return True
return False return False
def isRect(self) -> bool: def isRect(self) -> bool:
@@ -770,38 +807,219 @@ class PathMeasure(Stub):
super().__init__() super().__init__()
self._path = path self._path = path
self._force_closed = forceClosed self._force_closed = forceClosed
self._contours: List[List[Tuple[float, float, float]]] = []
self._contour_lengths: List[float] = []
self._total_length: float = 0.0
if path is not None:
self._flatten()
def _flatten(self):
"""将路径展平为线段列表,计算累积长度。"""
self._contours = []
self._contour_lengths = []
self._total_length = 0.0
if not self._path:
return
segs = self._path._segments
cur_x = 0.0
cur_y = 0.0
start_x = 0.0
start_y = 0.0
contour: List[Tuple[float, float, float]] = []
contour_len = 0.0
for seg in segs:
t = seg.get("type")
if t == "move":
if contour:
self._contours.append(contour)
self._contour_lengths.append(contour_len)
self._total_length += contour_len
cur_x = seg.get("x", 0.0)
cur_y = seg.get("y", 0.0)
start_x = cur_x
start_y = cur_y
contour = [(cur_x, cur_y, 0.0)]
contour_len = 0.0
elif t == "line":
nx = seg.get("x", cur_x)
ny = seg.get("y", cur_y)
d = math.hypot(nx - cur_x, ny - cur_y)
contour_len += d
contour.append((nx, ny, contour_len))
cur_x = nx
cur_y = ny
elif t == "quad":
x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y)
x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y)
steps = 16
prev_x, prev_y = cur_x, cur_y
for i in range(1, steps + 1):
tt = i / steps
mt = 1 - tt
nx = mt * mt * cur_x + 2 * mt * tt * x1 + tt * tt * x2
ny = mt * mt * cur_y + 2 * mt * tt * y1 + tt * tt * y2
d = math.hypot(nx - prev_x, ny - prev_y)
contour_len += d
contour.append((nx, ny, contour_len))
prev_x, prev_y = nx, ny
cur_x = x2
cur_y = y2
elif t == "cubic":
x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y)
x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y)
x3, y3 = seg.get("x3", cur_x), seg.get("y3", cur_y)
steps = 24
prev_x, prev_y = cur_x, cur_y
for i in range(1, steps + 1):
tt = i / steps
mt = 1 - tt
nx = mt ** 3 * cur_x + 3 * mt ** 2 * tt * x1 + 3 * mt * tt ** 2 * x2 + tt ** 3 * x3
ny = mt ** 3 * cur_y + 3 * mt ** 2 * tt * y1 + 3 * mt * tt ** 2 * y2 + tt ** 3 * y3
d = math.hypot(nx - prev_x, ny - prev_y)
contour_len += d
contour.append((nx, ny, contour_len))
prev_x, prev_y = nx, ny
cur_x = x3
cur_y = y3
elif t == "close":
if contour:
d = math.hypot(start_x - cur_x, start_y - cur_y)
if d > 1e-10:
contour_len += d
contour.append((start_x, start_y, contour_len))
cur_x = start_x
cur_y = start_y
if contour:
if self._force_closed and segs and segs[-1].get("type") != "close":
d = math.hypot(start_x - cur_x, start_y - cur_y)
if d > 1e-10:
contour_len += d
contour.append((start_x, start_y, contour_len))
self._contours.append(contour)
self._contour_lengths.append(contour_len)
self._total_length += contour_len
def setPath(self, path: Path, forceClosed: bool): def setPath(self, path: Path, forceClosed: bool):
self._path = path self._path = path
self._force_closed = forceClosed self._force_closed = forceClosed
self._contours = []
self._contour_lengths = []
self._total_length = 0.0
if path is not None:
self._flatten()
def getLength(self) -> float: def getLength(self) -> float:
if not self._path: """返回当前轮廓的长度。"""
return 0.0 if self._contour_lengths:
bounds = self._path.computeBounds() return self._contour_lengths[0]
return math.sqrt(bounds.width() ** 2 + bounds.height() ** 2) return 0.0
def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool: def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool:
if not self._path: """获取指定距离处的坐标和切线方向。"""
if not self._contours:
return False return False
contour = self._contours[0]
length = self._contour_lengths[0]
if length <= 0:
return False
# 钳制距离
d = max(0.0, min(distance, length))
# 二分查找
lo, hi = 0, len(contour) - 1
while lo < hi:
mid = (lo + hi) // 2
if contour[mid][2] < d:
lo = mid + 1
else:
hi = mid
idx = max(1, lo)
px, py, pd = contour[idx - 1]
nx, ny, nd = contour[idx]
seg_len = nd - pd
if seg_len < 1e-10:
t = 0.0
else:
t = (d - pd) / seg_len
x = px + (nx - px) * t
y = py + (ny - py) * t
dx = nx - px
dy = ny - py
dl = math.hypot(dx, dy)
if dl < 1e-10:
tx, ty = 1.0, 0.0
else:
tx, ty = dx / dl, dy / dl
if len(pos) >= 2: if len(pos) >= 2:
pos[0] = 0.0 pos[0] = x
pos[1] = 0.0 pos[1] = y
if len(tan) >= 2: if len(tan) >= 2:
tan[0] = 1.0 tan[0] = tx
tan[1] = 0.0 tan[1] = ty
return True return True
def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool: def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool:
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: 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: 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: def nextContour(self) -> bool:
"""移动到下一个轮廓。"""
if len(self._contours) > 1:
self._contours.pop(0)
self._contour_lengths.pop(0)
return True
return False return False
@@ -1556,35 +1774,99 @@ class Camera(Stub):
self._location_x = 0.0 self._location_x = 0.0
self._location_y = 0.0 self._location_y = 0.0
self._location_z = -8.0 self._location_z = -8.0
self._rot_x = 0.0
self._rot_y = 0.0
self._rot_z = 0.0
self._trans_x = 0.0
self._trans_y = 0.0
self._trans_z = 0.0
self._stack: List[Tuple[float, float, float, float, float, float, float, float, float]] = []
def save(self): def save(self):
pass """保存当前变换状态到栈。"""
self._stack.append((self._rot_x, self._rot_y, self._rot_z,
self._trans_x, self._trans_y, self._trans_z,
self._location_x, self._location_y, self._location_z))
def restore(self): def restore(self):
pass """从栈恢复上次保存的变换状态。"""
if self._stack:
(self._rot_x, self._rot_y, self._rot_z,
self._trans_x, self._trans_y, self._trans_z,
self._location_x, self._location_y, self._location_z) = self._stack.pop()
def translate(self, x: float, y: float, z: float): def translate(self, x: float, y: float, z: float):
self._location_x += x self._trans_x += x
self._location_y += y self._trans_y += y
self._location_z += z self._trans_z += z
def rotateX(self, deg: float): def rotateX(self, deg: float):
pass """绕 X 轴旋转(累积角度)。"""
self._rot_x += deg
def rotateY(self, deg: float): def rotateY(self, deg: float):
pass """绕 Y 轴旋转(累积角度)。"""
self._rot_y += deg
def rotateZ(self, deg: float): def rotateZ(self, deg: float):
pass """绕 Z 轴旋转(累积角度)。"""
self._rot_z += deg
def rotate(self, x: float, y: float, z: float): def rotate(self, x: float, y: float, z: float):
pass """同时绕 X/Y/Z 轴旋转。"""
self._rot_x += x
self._rot_y += y
self._rot_z += z
def _compute_matrix(self) -> List[float]:
"""计算 4x4 变换矩阵并投影为 3x3 仿射矩阵。"""
rx = math.radians(self._rot_x)
ry = math.radians(self._rot_y)
rz = math.radians(self._rot_z)
cx, sx = math.cos(rx), math.sin(rx)
cy, sy = math.cos(ry), math.sin(ry)
cz, sz = math.cos(rz), math.sin(rz)
# 旋转矩阵 R = Rz * Ry * Rx
r00 = cz * cy
r01 = cz * sy * sx - sz * cx
r02 = cz * sy * cx + sz * sx
r10 = sz * cy
r11 = sz * sy * sx + cz * cx
r12 = sz * sy * cx - cz * sx
r20 = -sy
r21 = cy * sx
r22 = cy * cx
# 应用平移
tx = self._trans_x
ty = self._trans_y
tz = self._trans_z
# 相机位置(z 越负越远)
cam_z = self._location_z
# 透视投影:z' = z + cam_z,当 cam_z != 0 时做透视除法
# 简化为:将 3D 点投影到 2D
# 点 (x,y,z) -> (x*r00+y*r01+z*r02+tx, x*r10+y*r11+z*r12+ty, x*r20+y*r21+z*r22+tz)
# 透视:screen_x = x' * d / (d - z'), screen_y = y' * d / (d - z')
d = -cam_z # 相机到原点距离
if abs(d) < 1e-10:
# 正交投影
return [r00, r01, tx, r10, r11, ty, 0.0, 0.0, 1.0]
# 透视投影矩阵(近似):将 z 影响缩放到 x,y
# m[0]=r00, m[1]=r01, m[2]=tx, m[3]=r10, m[4]=r11, m[5]=ty
# 透视项:m[6]=r20/d, m[7]=r21/d, m[8]=1 - tz/d
return [r00, r01, tx,
r10, r11, ty,
r20 / d, r21 / d, 1.0 - tz / d]
def getMatrix(self, matrix: Matrix): def getMatrix(self, matrix: Matrix):
matrix.reset() """计算变换矩阵并写入给定的 Matrix。"""
matrix._values = self._compute_matrix()
def applyToCanvas(self, canvas: "Canvas"): def applyToCanvas(self, canvas: "Canvas"):
pass """将相机变换应用到 Canvas。"""
m = Matrix()
self.getMatrix(m)
if hasattr(canvas, "concat"):
canvas.concat(m)
def setLocation(self, x: float, y: float, z: float): def setLocation(self, x: float, y: float, z: float):
self._location_x = x self._location_x = x
@@ -1592,7 +1874,17 @@ class Camera(Stub):
self._location_z = z self._location_z = z
def dotWithNormal(self, dx: float, dy: float, dz: float) -> float: def dotWithNormal(self, dx: float, dy: float, dz: float) -> float:
return 0.0 """计算向量与变换后 Z 轴法线的点积。"""
rx = math.radians(self._rot_x)
ry = math.radians(self._rot_y)
rz = math.radians(self._rot_z)
cy, sy = math.cos(ry), math.sin(ry)
cx, sx = math.cos(rx), math.sin(rx)
# 变换后的 Z 轴法线
nz_x = sy
nz_y = -cy * sx
nz_z = cy * cx
return dx * nz_x + dy * nz_y + dz * nz_z
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -1660,8 +1952,15 @@ class Picture(Stub):
self._canvas = None self._canvas = None
def draw(self, canvas: "Canvas"): def draw(self, canvas: "Canvas"):
"""回放录制的绘制命令到指定 Canvas。"""
for cmd in self._recorded: for cmd in self._recorded:
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: def getWidth(self) -> int:
return self._canvas.getWidth() if hasattr(self, "_canvas") and self._canvas else 0 return self._canvas.getWidth() if hasattr(self, "_canvas") and self._canvas else 0
@@ -1690,7 +1989,9 @@ class PictureDrawable(Stub):
self._picture = picture self._picture = picture
def draw(self, canvas: "Canvas"): def draw(self, canvas: "Canvas"):
pass """将 Picture 回放到指定 Canvas。"""
if self._picture is not None:
self._picture.draw(canvas)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -1704,6 +2005,10 @@ class SurfaceTexture(Stub):
super().__init__() super().__init__()
self._tex_name = texName self._tex_name = texName
self._timestamp = 0 self._timestamp = 0
self._buffer_width = 0
self._buffer_height = 0
self._attached = texName != 0
self._released = False
def getTimestamp(self) -> int: def getTimestamp(self) -> int:
return self._timestamp return self._timestamp
@@ -1712,16 +2017,24 @@ class SurfaceTexture(Stub):
self._timestamp = int(time.time() * 1e9) self._timestamp = int(time.time() * 1e9)
def release(self): def release(self):
pass """释放 SurfaceTexture 资源。"""
self._released = True
self._attached = False
def setDefaultBufferSize(self, width: int, height: int): def setDefaultBufferSize(self, width: int, height: int):
pass """设置默认缓冲区大小。"""
self._buffer_width = width
self._buffer_height = height
def attachToGLContext(self, texName: int): def attachToGLContext(self, texName: int):
pass """将 SurfaceTexture 附加到 GL 纹理。"""
self._tex_name = texName
self._attached = True
self._released = False
def detachFromGLContext(self): def detachFromGLContext(self):
pass """从 GL 纹理分离 SurfaceTexture。"""
self._attached = False
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -1768,6 +2081,59 @@ class Region(Stub):
return self._rect.contains(x, y) return self._rect.contains(x, y)
def op(self, *args) -> bool: def op(self, *args) -> bool:
"""对区域执行布尔运算。支持 op(rect, op) / op(region, op) / op(left,top,right,bottom,op)。"""
if len(args) == 2:
operand, op_code = args
if isinstance(operand, Rect):
other = Rect(operand.left, operand.top, operand.right, operand.bottom)
elif isinstance(operand, Region):
other = Rect(operand._rect.left, operand._rect.top,
operand._rect.right, operand._rect.bottom)
else:
return False
elif len(args) == 5:
left, top, right, bottom, op_code = args
other = Rect(left, top, right, bottom)
else:
return False
self_r = self._rect
if op_code == Region.OP_INTERSECT:
nL = max(self_r.left, other.left)
nT = max(self_r.top, other.top)
nR = min(self_r.right, other.right)
nB = min(self_r.bottom, other.bottom)
if nL < nR and nT < nB:
self_r.set(nL, nT, nR, nB)
return True
self_r.setEmpty()
return False
if op_code == Region.OP_UNION:
if self_r.isEmpty():
self_r.set(other.left, other.top, other.right, other.bottom)
else:
self_r.left = min(self_r.left, other.left)
self_r.top = min(self_r.top, other.top)
self_r.right = max(self_r.right, other.right)
self_r.bottom = max(self_r.bottom, other.bottom)
return True
if op_code == Region.OP_REPLACE:
self_r.set(other.left, other.top, other.right, other.bottom)
return True
if op_code == Region.OP_DIFFERENCE:
# 差集:若不相交则不变;若完全包含则清空;否则近似为不变
if not Rect.intersects(self_r, other):
return True
if other.contains(self_r.left, self_r.top) and other.contains(self_r.right - 1, self_r.bottom - 1):
self_r.setEmpty()
return False
return True
if op_code == Region.OP_REVERSE_DIFFERENCE:
self_r.set(other.left, other.top, other.right, other.bottom)
return True
if op_code == Region.OP_XOR:
if not Rect.intersects(self_r, other):
self_r.union(other.left, other.top, other.right, other.bottom)
return True
return False return False
def isRect(self) -> bool: def isRect(self) -> bool:
@@ -1894,10 +2260,32 @@ class Paint(Stub):
def setTextSize(self, textSize: float): def setTextSize(self, textSize: float):
self._text_size = textSize self._text_size = textSize
self._font_cache = None
def getTextSize(self) -> float: def getTextSize(self) -> float:
return self._text_size return self._text_size
def _get_font(self):
"""获取 PIL ImageFont 对象(带缓存)。"""
if not HAS_PIL:
return None
cache_key = (self._text_size, id(self._typeface) if self._typeface else None)
if getattr(self, "_font_cache_key", None) == cache_key and getattr(self, "_font_cache", None):
return self._font_cache
try:
from PIL import ImageFont
font_path = None
if self._typeface and hasattr(self._typeface, "_path") and self._typeface._path:
font_path = self._typeface._path
if font_path and os.path.exists(font_path):
self._font_cache = ImageFont.truetype(font_path, int(self._text_size))
else:
self._font_cache = ImageFont.load_default()
self._font_cache_key = cache_key
return self._font_cache
except Exception:
return None
def setTypeface(self, typeface: Optional[Typeface]): def setTypeface(self, typeface: Optional[Typeface]):
self._typeface = typeface self._typeface = typeface
@@ -2004,13 +2392,31 @@ class Paint(Stub):
self._shadow_layer = None self._shadow_layer = None
def measureText(self, text: str) -> float: def measureText(self, text: str) -> float:
"""测量文本宽度。优先使用 PIL ImageFont 获取精确宽度。"""
font = self._get_font()
if font is not None:
try:
if hasattr(font, "getlength"):
return float(font.getlength(text))
bbox = font.getbbox(text)
return float(bbox[2] - bbox[0])
except Exception:
pass
return len(text) * self._text_size * 0.5 return len(text) * self._text_size * 0.5
def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int: def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int:
"""在最大宽度内截断文本,返回可容纳的字符数。"""
font = self._get_font()
width = 0.0 width = 0.0
count = 0 count = 0
for i, ch in enumerate(text): for i, ch in enumerate(text):
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: if width + cw > maxWidth:
break break
width += cw width += cw
@@ -2019,9 +2425,16 @@ class Paint(Stub):
return count return count
def getTextWidths(self, text: str, widths: List[float]) -> int: def getTextWidths(self, text: str, widths: List[float]) -> int:
cw = self._text_size * 0.5 """获取每个字符的宽度。"""
font = self._get_font()
for i, ch in enumerate(text): for i, ch in enumerate(text):
widths[i] = cw if font is not None:
try:
widths[i] = float(font.getlength(ch)) if hasattr(font, "getlength") else float(font.getbbox(ch)[2])
except Exception:
widths[i] = self._text_size * 0.5
else:
widths[i] = self._text_size * 0.5
return len(text) return len(text)
def getTextPath(self, text: str, start: int, end: int, x: float, y: float, path: Path): def getTextPath(self, text: str, start: int, end: int, x: float, y: float, path: Path):
@@ -2030,7 +2443,22 @@ class Paint(Stub):
path.lineTo(x + (i - start + 1) * self._text_size * 0.3, y) path.lineTo(x + (i - start + 1) * self._text_size * 0.3, y)
def getFontMetrics(self) -> Dict[str, float]: def getFontMetrics(self) -> Dict[str, float]:
"""返回字体度量信息。优先使用 PIL ImageFont 获取真实度量。"""
size = self._text_size size = self._text_size
font = self._get_font()
if font is not None:
try:
if hasattr(font, "getmetrics"):
ascent, descent = font.getmetrics()
return {
"ascent": -float(ascent),
"descent": float(descent),
"top": -float(ascent) - 2.0,
"bottom": float(descent) + 2.0,
"leading": 0.0,
}
except Exception:
pass
return { return {
"ascent": -size * 0.8, "ascent": -size * 0.8,
"descent": size * 0.2, "descent": size * 0.2,
@@ -2068,6 +2496,19 @@ class Paint(Stub):
return getattr(self, '_stroke_miter', 4.0) return getattr(self, '_stroke_miter', 4.0)
def getTextBounds(self, text, start, end, bounds): 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'): if hasattr(self, '_text_size'):
w = len(text[start:end]) * self._text_size * 0.5 w = len(text[start:end]) * self._text_size * 0.5
h = self._text_size h = self._text_size
@@ -2312,13 +2753,30 @@ class Bitmap(Stub):
return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8) return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8)
def sameAs(self, other: "Bitmap") -> bool: def sameAs(self, other: "Bitmap") -> bool:
"""比较两个 Bitmap 的像素是否完全相同。"""
if not isinstance(other, Bitmap): if not isinstance(other, Bitmap):
return False return False
if self._width != other._width or self._height != other._height: if self._width != other._width or self._height != other._height:
return False return False
if self._config != other._config:
return False
if self._recycled or other._recycled:
return False
# 使用 PIL 比较像素
if HAS_PIL and hasattr(self, "_image") and hasattr(other, "_image"):
try:
from PIL import ImageChops
diff = ImageChops.difference(self._image, other._image)
return diff.getbbox() is None
except Exception:
return list(self._image.getdata()) == list(other._image.getdata())
# 回退到字节比较
if hasattr(self, "_pixels") and hasattr(other, "_pixels"):
return bytes(self._pixels) == bytes(other._pixels)
return True return True
def prepareToDraw(self): def prepareToDraw(self):
"""提示将位图数据上传到 GPU 缓存。桌面端为空操作。"""
pass pass
def hasAlpha(self) -> bool: def hasAlpha(self) -> bool:
@@ -2407,6 +2865,29 @@ class BitmapFactory(Stub):
@staticmethod @staticmethod
def decodeResource(res, resId: int, opts: Options = None) -> Optional[Bitmap]: def decodeResource(res, resId: int, opts: Options = None) -> Optional[Bitmap]:
"""从资源解码位图。尝试通过 Resources 打开原始资源流。"""
# 尝试通过 Resources 获取资源路径或流
if res is not None:
try:
# 尝试 openRawResource
stream = res.openRawResource(resId)
if stream is not None:
return BitmapFactory.decodeStream(stream, None, opts)
except Exception:
pass
try:
# 尝试通过资源名查找文件
name = res.getResourceEntryName(resId) if hasattr(res, "getResourceEntryName") else None
if name and name != "unknown":
for ext in (".png", ".jpg", ".jpeg", ".webp", ".bmp"):
candidate = resolve_android_path("res/drawable/" + name + ext)
if os.path.exists(candidate):
return BitmapFactory.decodeFile(candidate, opts)
candidate = resolve_android_path("res/drawable-xxhdpi/" + name + ext)
if os.path.exists(candidate):
return BitmapFactory.decodeFile(candidate, opts)
except Exception:
pass
return Bitmap(100, 100, Bitmap.Config.ARGB_8888) return Bitmap(100, 100, Bitmap.Config.ARGB_8888)
@staticmethod @staticmethod
@@ -2610,7 +3091,22 @@ class Canvas(Stub):
return Rect(r.left, r.top, r.right, r.bottom) return Rect(r.left, r.top, r.right, r.bottom)
def quickReject(self, *args) -> bool: def quickReject(self, *args) -> bool:
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): def drawColor(self, color: int):
if HAS_PIL and self._draw is not None: if HAS_PIL and self._draw is not None:
@@ -2810,7 +3306,37 @@ class Canvas(Stub):
self._draw.text((x, y), text, fill=fill_color) self._draw.text((x, y), text, fill=fill_color)
def drawTextOnPath(self, text: str, path: Path, hOffset: float, vOffset: float, paint: Paint): def drawTextOnPath(self, text: str, path: Path, hOffset: float, vOffset: float, paint: Paint):
pass """沿路径绘制文本。使用 PathMeasure 逐字符放置。"""
if not text or not HAS_PIL or self._draw is None:
return
pm = PathMeasure(path, False)
total = pm.getLength()
if total <= 0:
return
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
dist = hOffset
pos = [0.0, 0.0]
tan = [0.0, 0.0]
for ch in text:
cw = paint.measureText(ch)
if dist > total:
break
if pm.getPosTan(dist, pos, tan):
angle = math.degrees(math.atan2(tan[1], tan[0]))
# 使用 PIL 旋转绘制每个字符
try:
from PIL import Image as _PILImage
char_img = _PILImage.new("RGBA", (int(cw) + 4, int(paint._text_size) + 4), (0, 0, 0, 0))
from PIL import ImageDraw as _PILDraw
cd = _PILDraw.Draw(char_img)
cd.text((0, 0), ch, fill=fill_color)
rotated = char_img.rotate(-angle, expand=True, resample=_PILImage.BICUBIC)
self._image.paste(rotated, (int(pos[0] + vOffset * tan[1]), int(pos[1] - vOffset * tan[0])),
rotated)
except Exception:
self._draw.text((pos[0], pos[1]), ch, fill=fill_color)
dist += cw
def drawPosText(self, text: str, pos: List[float], paint: Paint): def drawPosText(self, text: str, pos: List[float], paint: Paint):
if HAS_PIL and self._draw is not None: if HAS_PIL and self._draw is not None:
@@ -2821,14 +3347,44 @@ class Canvas(Stub):
self._draw.text((pos[i * 2], pos[i * 2 + 1]), ch, fill=fill_color) self._draw.text((pos[i * 2], pos[i * 2 + 1]), ch, fill=fill_color)
def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None): def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None):
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): def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None):
pass """绘制 NinePatch(拉伸位图)到目标矩形。"""
if patch is None or patch._bitmap is None:
return
bitmap = patch._bitmap
if dst is None:
self.drawBitmap(bitmap, 0, 0, paint)
return
# 简单缩放绘制
if isinstance(dst, (Rect, RectF)):
dw = dst.right - dst.left
dh = dst.bottom - dst.top
else:
dw, dh = dst[2] - dst[0], dst[3] - dst[1]
scaled = Bitmap.createScaledBitmap(bitmap, int(dw), int(dh), True)
self.drawBitmap(scaled, dst.left if hasattr(dst, "left") else dst[0],
dst.top if hasattr(dst, "top") else dst[1], paint)
def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float, def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float,
inner: RectF, innerRx: float, innerRy: float, paint: Paint): inner: RectF, innerRx: float, innerRy: float, paint: Paint):
pass """绘制双层圆角矩形(外层和内层)。"""
self.drawRoundRect(outer, outerRx, outerRy, paint)
self.drawRoundRect(inner, innerRx, innerRy, paint)
def drawBitmapMesh(self, bitmap: Bitmap, meshWidth: int, meshHeight: int, def drawBitmapMesh(self, bitmap: Bitmap, meshWidth: int, meshHeight: int,
verts: List[float], vertOffset: int, colors: List[int], verts: List[float], vertOffset: int, colors: List[int],
@@ -2848,10 +3404,12 @@ class Canvas(Stub):
return False return False
def setDensity(self, density: int): def setDensity(self, density: int):
pass """设置画布密度(DPI)。"""
self._density = density
def getDensity(self) -> int: def getDensity(self) -> int:
return 0 """返回画布密度(DPI),默认 0 表示未知。"""
return getattr(self, "_density", 0)
def toString(self) -> str: def toString(self) -> str:
return f"Canvas({self._width}x{self._height})" return f"Canvas({self._width}x{self._height})"
+148 -17
View File
@@ -12,10 +12,16 @@ from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL
# Linux 真实实现导入 # Linux 真实实现导入
try: try:
from .._linux_impl import IS_LINUX, LinuxBluetooth, LinuxSensors, HAS_BLUETOOTHCTL from .._linux_impl import IS_LINUX, LinuxBluetooth, LinuxSensors, HAS_BLUETOOTHCTL, LinuxPower
_USE_LINUX = IS_LINUX _USE_LINUX = IS_LINUX
except ImportError: except ImportError:
_USE_LINUX = False try:
from .._linux_impl import IS_LINUX, LinuxBluetooth, LinuxSensors, HAS_BLUETOOTHCTL
_USE_LINUX = IS_LINUX
LinuxPower = None
except ImportError:
_USE_LINUX = False
LinuxPower = None
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -379,18 +385,56 @@ class SensorManager(Stub):
@staticmethod @staticmethod
def getRotationMatrix(R: List[float], I: List[float], def getRotationMatrix(R: List[float], I: List[float],
gravity: List[float], geomagnetic: List[float]) -> bool: gravity: List[float], geomagnetic: List[float]) -> bool:
"""根据重力和地磁向量计算旋转矩阵 R 和倾斜矩阵 I。
算法对齐 Android SensorManager.getRotationMatrix。"""
if len(R) < 9: if len(R) < 9:
R.extend([0] * (9 - len(R))) R.extend([0.0] * (9 - len(R)))
if len(R) >= 9: gx, gy, gz = gravity[0], gravity[1], gravity[2]
R[0] = 1.0 mx, my, mz = geomagnetic[0], geomagnetic[1], geomagnetic[2]
R[1] = 0.0 # 归一化重力向量 -> H
R[2] = 0.0 g_len = math.sqrt(gx * gx + gy * gy + gz * gz)
R[3] = 0.0 if g_len < 1e-10:
R[4] = 1.0 return False
R[5] = 0.0 hx = gx / g_len
R[6] = 0.0 hy = gy / g_len
R[7] = 0.0 hz = gz / g_len
R[8] = 1.0 # 叉积 A = H x M (东方向)
ax = hy * mz - hz * my
ay = hz * mx - hx * mz
az = hx * my - hy * mx
a_len = math.sqrt(ax * ax + ay * ay + az * az)
if a_len < 1e-10:
return False
ax /= a_len
ay /= a_len
az /= a_len
# 重新计算 M = A x H
ex = ay * hz - az * hy
ey = az * hx - ax * hz
ez = ax * hy - ay * hx
# 填充旋转矩阵 R (列优先,Android 格式)
R[0] = ax
R[1] = ay
R[2] = az
R[3] = ex
R[4] = ey
R[5] = ez
R[6] = hx
R[7] = hy
R[8] = hz
# 填充倾斜矩阵 I
if I is not None and len(I) >= 9:
inv_h = 1.0 / g_len
inv_m = 1.0 / math.sqrt(mx * mx + my * my + mz * mz) if (mx * mx + my * my + mz * mz) > 0 else 0
I[0] = inv_h * hx
I[1] = inv_h * hy
I[2] = inv_h * hz
I[3] = inv_m * mx
I[4] = inv_m * my
I[5] = inv_m * mz
I[6] = inv_h * ex
I[7] = inv_h * ey
I[8] = inv_h * ez
return True return True
@staticmethod @staticmethod
@@ -399,17 +443,69 @@ class SensorManager(Stub):
@staticmethod @staticmethod
def getInclination(I: List[float]) -> float: def getInclination(I: List[float]) -> float:
"""根据倾斜矩阵 I 计算地磁倾角(弧度)。"""
if len(I) >= 9:
# I[5] = cos(el), I[2] = sin(el)
# 倾角 = atan2(I[2], I[5])
return math.atan2(I[2], I[5])
return 0.0 return 0.0
@staticmethod @staticmethod
def remapCoordinateSystem(inR: List[float], X: int, Y: int, def remapCoordinateSystem(inR: List[float], X: int, Y: int,
outR: List[float]) -> bool: outR: List[float]) -> bool:
if len(inR) >= 9 and len(outR) >= 9: """根据指定的 X/Y 轴重映射坐标系。"""
for i in range(9): if len(inR) < 9 or len(outR) < 9:
outR[i] = inR[i] return False
# Android 轴常量
AXIS_X = 1
AXIS_Y = 2
AXIS_Z = 3
AXIS_MINUS_X = -1
AXIS_MINUS_Y = -2
AXIS_MINUS_Z = -3
# 将轴常量映射到列索引和符号
axis_map = {
AXIS_X: (0, 1), AXIS_Y: (1, 1), AXIS_Z: (2, 1),
AXIS_MINUS_X: (0, -1), AXIS_MINUS_Y: (1, -1), AXIS_MINUS_Z: (2, -1),
}
if X not in axis_map or Y not in axis_map:
return False
x_col, x_sign = axis_map[X]
y_col, y_sign = axis_map[Y]
# Z = X x Y
z_col = 3 - x_col - y_col
z_sign = x_sign * y_sign
# 如果 X 和 Y 不正交
if abs(x_col - y_col) != 1 and not (x_col == 0 and y_col == 2) and not (x_col == 2 and y_col == 0):
z_sign = -z_sign
# 填充输出矩阵(列优先)
for row in range(3):
outR[row * 3 + 0] = inR[row * 3 + x_col] * x_sign
outR[row * 3 + 1] = inR[row * 3 + y_col] * y_sign
outR[row * 3 + 2] = inR[row * 3 + z_col] * z_sign
return True return True
def getDeviceId(self) -> str: def getDeviceId(self) -> str:
"""返回传感器设备 ID。尝试从 /sys 读取真实设备标识。"""
import os as _os
# 尝试从 /sys/class/sensor 读取
try:
base = "/sys/class/sensor"
if _os.path.isdir(base):
names = _os.listdir(base)
if names:
return names[0]
except (OSError, IOError):
pass
# 尝试从 /sys/bus/iio/devices 读取
try:
base = "/sys/bus/iio/devices"
if _os.path.isdir(base):
names = _os.listdir(base)
if names:
return names[0]
except (OSError, IOError):
pass
return "sensor_hub_0" return "sensor_hub_0"
@@ -552,6 +648,29 @@ class BatteryManager(Stub):
super().__init__() super().__init__()
def getIntProperty(self, id: int) -> int: def getIntProperty(self, id: int) -> int:
"""返回电池整型属性。优先使用 LinuxPower 读取真实数据。"""
if _USE_LINUX and LinuxPower is not None:
try:
info = LinuxPower.get_battery_info() or {}
if id == BatteryManager.BATTERY_PROPERTY_CAPACITY:
level = info.get("level", -1)
return int(level) if isinstance(level, (int, float)) and level >= 0 else -1
if id == BatteryManager.BATTERY_PROPERTY_STATUS:
status_map = {
"charging": BatteryManager.BATTERY_STATUS_CHARGING,
"discharging": BatteryManager.BATTERY_STATUS_DISCHARGING,
"not charging": BatteryManager.BATTERY_STATUS_NOT_CHARGING,
"full": BatteryManager.BATTERY_STATUS_FULL,
"unknown": BatteryManager.BATTERY_STATUS_UNKNOWN,
}
return status_map.get(str(info.get("status", "unknown")).lower(),
BatteryManager.BATTERY_STATUS_UNKNOWN)
if id == BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER:
voltage = info.get("voltage", 0)
return int(voltage * 1000) if isinstance(voltage, (int, float)) else 0
except Exception:
pass
# 回退到默认值
id_values = { id_values = {
BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER: 3500, BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER: 3500,
BatteryManager.BATTERY_PROPERTY_CAPACITY: 85, BatteryManager.BATTERY_PROPERTY_CAPACITY: 85,
@@ -560,7 +679,19 @@ class BatteryManager(Stub):
return id_values.get(id, 0) return id_values.get(id, 0)
def getLongProperty(self, id: int) -> int: def getLongProperty(self, id: int) -> int:
return 0 """返回电池长整型属性。优先使用 LinuxPower 读取真实数据。"""
if _USE_LINUX and LinuxPower is not None:
try:
info = LinuxPower.get_battery_info() or {}
if id == BatteryManager.BATTERY_PROPERTY_CURRENT_NOW:
current = info.get("current_now", 0)
return int(current) if isinstance(current, (int, float)) else 0
if id == BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER:
voltage = info.get("voltage", 0)
return int(voltage * 3600000) if isinstance(voltage, (int, float)) else 0
except Exception:
pass
return self.getIntProperty(id)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
+3 -2
View File
@@ -13,6 +13,7 @@ import random
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
from .._core import Stub, AndroidOnlyError from .._core import Stub, AndroidOnlyError
from .hardware import SensorManager as _BaseSensorManager
__all__ = [ __all__ = [
"Sensor", "SensorEvent", "SensorEventListener", "Sensor", "SensorEvent", "SensorEventListener",
@@ -313,8 +314,8 @@ class SensorEventListener(Stub):
# SensorManager (扩展) # SensorManager (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class SensorManager(Stub): class SensorManager(_BaseSensorManager):
"""SensorManager — 传感器管理器 (完整版)""" """SensorManager — 传感器管理器 (完整版,继承 hardware.SensorManager)"""
AXIS_X = AXIS_X AXIS_X = AXIS_X
AXIS_Y = AXIS_Y AXIS_Y = AXIS_Y
+193 -13
View File
@@ -620,6 +620,9 @@ class GnssStatus(Stub):
return 0.0 return 0.0
def getBasebandCn0DbHz(self, index: int) -> float: def getBasebandCn0DbHz(self, index: int) -> float:
"""返回指定卫星的基带载噪比(dB-Hz)。"""
if 0 <= index < len(self._satellites):
return self._satellites[index].get("baseband_cn0", 0.0)
return 0.0 return 0.0
def getCn0DbHz(self, index: int) -> float: def getCn0DbHz(self, index: int) -> float:
@@ -873,6 +876,42 @@ class Address(Stub):
def setFeatureName(self, name: str): def setFeatureName(self, name: str):
self._feature_name = name self._feature_name = name
def setAdminArea(self, area: str):
self._admin_area = area
def setSubAdminArea(self, area: str):
self._sub_admin_area = area
def setLocality(self, locality: str):
self._locality = locality
def setSubLocality(self, sub_locality: str):
self._sub_locality = sub_locality
def setPremises(self, premises: str):
self._premises = premises
def setThoroughfare(self, thoroughfare: str):
self._thoroughfare = thoroughfare
def setSubThoroughfare(self, sub_thoroughfare: str):
self._sub_thoroughfare = sub_thoroughfare
def setPostalCode(self, postal_code: str):
self._postal_code = postal_code
def setCountryName(self, country_name: str):
self._country_name = country_name
def setCountryCode(self, country_code: str):
self._country_code = country_code
def setPhone(self, phone: str):
self._phone = phone
def setUrl(self, url: str):
self._url = url
def setLatitude(self, latitude: float): def setLatitude(self, latitude: float):
self._latitude = latitude self._latitude = latitude
self._has_lat_lon = True self._has_lat_lon = True
@@ -898,29 +937,144 @@ class Address(Stub):
class Geocoder(Stub): class Geocoder(Stub):
"""Geocoder — 地理编码/逆地理编码""" """Geocoder — 地理编码/逆地理编码"""
# Nominatim (OpenStreetMap) 免费 API
_NOMINATIM_URL = "https://nominatim.openstreetmap.org"
_USER_AGENT = "Android-pyApi/1.0"
def __init__(self, context: Any = None, locale: Any = None): def __init__(self, context: Any = None, locale: Any = None):
super().__init__() super().__init__()
self._context = context self._context = context
self._locale = locale
def _get_locale_str(self) -> str:
if self._locale is None:
return "en"
if hasattr(self._locale, "getLanguage"):
lang = self._locale.getLanguage()
country = self._locale.getCountry()
return f"{lang}-{country}" if country else lang
return str(self._locale)
@staticmethod
def _http_get_json(url: str) -> Optional[Any]:
"""使用 urllib 发送 GET 请求并解析 JSON。"""
try:
import urllib.request
import json
req = urllib.request.Request(url, headers={
"User-Agent": Geocoder._USER_AGENT,
"Accept-Language": "en",
})
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
except Exception:
return None
def getFromLocation(self, latitude: float, longitude: float, def getFromLocation(self, latitude: float, longitude: float,
max_results: int = 1) -> List[Address]: max_results: int = 1) -> List[Address]:
"""逆地理编码:根据经纬度获取地址。优先使用 Nominatim API。"""
results: List[Address] = []
# 尝试在线逆地理编码
try:
url = (f"{self._NOMINATIM_URL}/reverse?"
f"lat={latitude}&lon={longitude}&format=jsonv2"
f"&addressdetails=1&limit={max_results}")
data = self._http_get_json(url)
if data and isinstance(data, dict) and "address" in data:
addr = self._parse_nominatim_reverse(data)
results.append(addr)
except Exception:
pass
# 回退到默认地址
if not results:
addr = Address()
addr.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 = Address()
addr.setLatitude(latitude) address = data.get("address", {})
addr.setLongitude(longitude) lat = data.get("lat")
addr.setFeatureName("Unknown Location") lon = data.get("lon")
addr.setLocality("Unknown") if lat is not None:
addr.setCountryName("Unknown") addr.setLatitude(float(lat))
return [addr] if max_results > 0 else [] if lon is not None:
addr.setLongitude(float(lon))
addr.setFeatureName(address.get("road") or address.get("building")
or data.get("name") or "Unknown")
addr.setLocality(address.get("city") or address.get("town")
or address.get("village") or address.get("county") or "Unknown")
addr.setAdminArea(address.get("state") or "")
addr.setSubAdminArea(address.get("state_district") or "")
addr.setCountryName(address.get("country") or "Unknown")
addr.setCountryCode(address.get("country_code", "").upper())
addr.setPostalCode(address.get("postcode") or "")
addr.setAddressLine(0, data.get("display_name", ""))
return addr
def getFromLocationName(self, location_name: str, max_results: int = 1, def getFromLocationName(self, location_name: str, max_results: int = 1,
lower_left_lat: Optional[float] = None, lower_left_lat: Optional[float] = None,
lower_left_lon: Optional[float] = None, lower_left_lon: Optional[float] = None,
upper_right_lat: Optional[float] = None, upper_right_lat: Optional[float] = None,
upper_right_lon: Optional[float] = None) -> List[Address]: upper_right_lon: Optional[float] = None) -> List[Address]:
"""地理编码:根据地名获取经纬度。优先使用 Nominatim API。"""
results: List[Address] = []
# 尝试在线地理编码
try:
import urllib.parse
params = {
"q": location_name,
"format": "jsonv2",
"addressdetails": "1",
"limit": str(max_results),
}
# 添加边界框
if all(v is not None for v in (lower_left_lat, lower_left_lon,
upper_right_lat, upper_right_lon)):
params["viewbox"] = f"{lower_left_lon},{upper_right_lat},{upper_right_lon},{lower_left_lat}"
params["bounded"] = "1"
query = urllib.parse.urlencode(params)
url = f"{self._NOMINATIM_URL}/search?{query}"
data = self._http_get_json(url)
if data and isinstance(data, list):
for item in data[:max_results]:
addr = self._parse_nominatim_search(item)
results.append(addr)
except Exception:
pass
# 回退到默认地址
if not results:
addr = Address()
addr.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 = Address()
addr.setFeatureName(location_name) lat = item.get("lat")
addr.setLocality(location_name) lon = item.get("lon")
return [addr] if max_results > 0 else [] 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
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -956,6 +1110,9 @@ class LocationManager(Stub):
self._last_known_locations: Dict[str, Location] = {} self._last_known_locations: Dict[str, Location] = {}
self._enabled_providers = {self.GPS_PROVIDER, self.NETWORK_PROVIDER, self._enabled_providers = {self.GPS_PROVIDER, self.NETWORK_PROVIDER,
self.PASSIVE_PROVIDER, self.FUSED_PROVIDER} self.PASSIVE_PROVIDER, self.FUSED_PROVIDER}
self._test_providers: Dict[str, LocationProvider] = {}
self._test_locations: Dict[str, Location] = {}
self._proximity_alerts: List[Dict[str, Any]] = []
def getLastKnownLocation(self, provider: str) -> Optional[Location]: def getLastKnownLocation(self, provider: str) -> Optional[Location]:
if _USE_LINUX: if _USE_LINUX:
@@ -1036,19 +1193,42 @@ class LocationManager(Stub):
def addProximityAlert(self, latitude: float, longitude: float, def addProximityAlert(self, latitude: float, longitude: float,
radius: float, expiration: int, intent: Any): radius: float, expiration: int, intent: Any):
pass """添加接近警报。记录警报信息,在位置更新时检查是否进入/离开区域。"""
import time as _time
alert = {
"latitude": latitude,
"longitude": longitude,
"radius": radius,
"expiration": expiration,
"intent": intent,
"created_at": _time.time(),
"last_state": None, # None=未触发, True=在区域内, False=在区域外
}
self._proximity_alerts.append(alert)
def sendExtraCommand(self, provider: str, command: str, extras: Any) -> bool: def sendExtraCommand(self, provider: str, command: str, extras: Any) -> bool:
"""向位置提供者发送额外命令。桌面端无实际效果,返回成功。"""
return True return True
def clearTestProviderLocation(self, provider: str): def clearTestProviderLocation(self, provider: str):
pass """清除测试提供者的位置数据。"""
self._test_locations.pop(provider, None)
def setTestProviderLocation(self, provider: str, location: Location): def setTestProviderLocation(self, provider: str, location: Location):
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): def removeTestProvider(self, provider: str):
pass """移除测试提供者。"""
self._test_providers.pop(provider, None)
self._test_locations.pop(provider, None)
def addTestProvider(self, name: str, requires_network: bool, requires_satellite: bool, def addTestProvider(self, name: str, requires_network: bool, requires_satellite: bool,
requires_cell: bool, has_monetary_cost: bool, supports_altitude: bool, requires_cell: bool, has_monetary_cost: bool, supports_altitude: bool,
+332 -27
View File
@@ -115,11 +115,46 @@ class Build(Stub):
@staticmethod @staticmethod
def getRadioVersion() -> str: def getRadioVersion() -> str:
return "" """返回基带/无线电版本。桌面端无无线电硬件,返回空字符串。"""
return Build.RADIO
@staticmethod @staticmethod
def getSerial() -> str: def getSerial() -> str:
return "unknown" """返回设备序列号。尝试从 /sys 或 /proc 读取真实硬件序列号。"""
# 尝试从 /sys/class/android_usb 读取
try:
for name in _os.listdir("/sys/class/android_usb"):
serial_path = f"/sys/class/android_usb/{name}/iSerial"
if _os.path.exists(serial_path):
with open(serial_path, "r") as f:
serial = f.read().strip()
if serial:
return serial
except (OSError, IOError):
pass
# 尝试从 /proc/cpuinfo 读取 Serial 字段
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if line.lower().startswith("serial"):
parts = line.split(":", 1)
if len(parts) == 2:
serial = parts[1].strip()
if serial and serial != "0" * len(serial):
return serial
except (OSError, IOError):
pass
# 尝试从 /sys/class/dmi/id/product_serial 读取
try:
path = "/sys/class/dmi/id/product_serial"
if _os.path.exists(path):
with open(path, "r") as f:
serial = f.read().strip()
if serial:
return serial
except (OSError, IOError):
pass
return Build.SERIAL
@staticmethod @staticmethod
def getFingerprint() -> str: def getFingerprint() -> str:
@@ -201,7 +236,17 @@ class SystemClock(Stub):
@staticmethod @staticmethod
def setCurrentTimeMillis(millis: int) -> bool: 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
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -268,6 +313,20 @@ class Environment(Stub):
@staticmethod @staticmethod
def isExternalStorageRemovable(path: Optional[str] = None) -> bool: def isExternalStorageRemovable(path: Optional[str] = None) -> bool:
"""判断外部存储是否可移除。通过检查 /proc/mounts 判断。"""
try:
check_path = path or _sdcard_root
with open("/proc/mounts", "r") as f:
for line in f:
parts = line.split()
if len(parts) >= 3 and check_path in parts[1]:
# 检查挂载选项中是否包含 SD 卡相关设备
device = parts[0]
if "/sd" in device or "/mmc" in device or "/usb" in device:
return True
return False
except (OSError, IOError):
pass
return False return False
@staticmethod @staticmethod
@@ -309,7 +368,8 @@ class Process(Stub):
@staticmethod @staticmethod
def myUid() -> int: def myUid() -> int:
return 10001 """返回当前进程的真实 UID。"""
return _os.getuid()
@staticmethod @staticmethod
def myPid() -> int: def myPid() -> int:
@@ -331,11 +391,15 @@ class Process(Stub):
pass pass
@staticmethod @staticmethod
def setThreadPriority(priority: int): def setThreadPriority(*args):
pass """设置线程优先级。支持 setThreadPriority(priority) 和 setThreadPriority(tid, priority)。"""
if len(args) == 1:
@staticmethod tid = 0
def setThreadPriority(tid, priority): priority = args[0]
elif len(args) == 2:
tid, priority = args
else:
return
try: try:
_os.setpriority(_os.PRIO_PROCESS, tid, priority) _os.setpriority(_os.PRIO_PROCESS, tid, priority)
except (OSError, PermissionError): except (OSError, PermissionError):
@@ -1006,7 +1070,20 @@ class Looper(Stub):
return self._message_queue return self._message_queue
def dump(self, pw: Any, prefix: str): def dump(self, pw: Any, prefix: str):
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: def toString(self) -> str:
return f"Looper({self._thread.name})" return f"Looper({self._thread.name})"
@@ -1377,6 +1454,17 @@ class PowerManager(Stub):
return self._is_interactive return self._is_interactive
def isPowerSaveMode(self) -> bool: def isPowerSaveMode(self) -> bool:
"""检查是否处于省电模式。读取 /sys 中的电源相关状态。"""
try:
# 检查 CPU 调速器是否为 powersave
gov_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"
if _os.path.exists(gov_path):
with open(gov_path, "r") as f:
governor = f.read().strip()
if governor == "powersave":
return True
except (OSError, IOError):
pass
return self._is_power_save return self._is_power_save
def isDeviceIdleMode(self) -> bool: def isDeviceIdleMode(self) -> bool:
@@ -1389,16 +1477,29 @@ class PowerManager(Stub):
return False return False
def reboot(self, reason: Optional[str] = None): def reboot(self, reason: Optional[str] = None):
pass """重启设备。桌面端尝试执行 reboot 命令(通常需要 root)。"""
try:
import subprocess
cmd = ["reboot"]
if reason:
cmd.append(reason)
subprocess.run(cmd, capture_output=True, timeout=5)
except Exception:
sys.stderr.write(f"[PowerManager] reboot: {reason or ''}\n")
def goToSleep(self, time_ms: int): def goToSleep(self, time_ms: int):
pass """使设备进入睡眠。桌面端无实际效果。"""
self._is_interactive = False
sys.stderr.write(f"[PowerManager] goToSleep at {time_ms}\n")
def wakeUp(self, time_ms: int): def wakeUp(self, time_ms: int):
pass """唤醒设备。桌面端无实际效果。"""
self._is_interactive = True
sys.stderr.write(f"[PowerManager] wakeUp at {time_ms}\n")
def userActivity(self, when: int, no_change_lights: bool = False): def userActivity(self, when: int, no_change_lights: bool = False):
pass """报告用户活动事件。桌面端无实际效果。"""
self._is_interactive = True
class PowerManagerWakeLock(Stub): class PowerManagerWakeLock(Stub):
@@ -1448,20 +1549,101 @@ class Vibrator(Stub):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._ff_device: Optional[str] = None
self._vibrating = False
def vibrate(self, milliseconds: int): @staticmethod
sys.stderr.write(f"[Vibrator] vibrate: {milliseconds}ms\n") 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): def vibrate(self, *args):
sys.stderr.write(f"[Vibrator] vibrate pattern: {pattern}, repeat={repeat}\n") """振动。支持 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): def cancel(self):
"""取消振动。"""
self._vibrating = False
if self._ff_device and self._ff_device.startswith("/sys/class/leds"):
try:
with open(_os.path.join(self._ff_device, "brightness"), "w") as f:
f.write("0")
except (OSError, IOError):
pass
sys.stderr.write("[Vibrator] cancel\n") sys.stderr.write("[Vibrator] cancel\n")
def hasVibrator(self) -> bool: def hasVibrator(self) -> bool:
return True """检查是否有振动器硬件。"""
return self._find_ff_device() is not None
def hasAmplitudeControl(self) -> bool: def hasAmplitudeControl(self) -> bool:
"""检查是否支持振幅控制。"""
try:
if _os.path.exists("/sys/class/leds/vibrator/max_brightness"):
with open("/sys/class/leds/vibrator/max_brightness", "r") as f:
return int(f.read().strip()) > 1
except (OSError, IOError, ValueError):
pass
return False return False
def areEffectsSupported(self, effect_ids: List[int]) -> List[int]: def areEffectsSupported(self, effect_ids: List[int]) -> List[int]:
@@ -1474,20 +1656,41 @@ class Vibrator(Stub):
class VibratorEffect(Stub): class VibratorEffect(Stub):
"""VibratorEffect — 振动效果""" """VibratorEffect — 振动效果"""
EFFECT_CLICK = 0
EFFECT_DOUBLE_CLICK = 1
EFFECT_TICK = 2
EFFECT_THUD = 3
EFFECT_POP = 4
EFFECT_HEAVY_CLICK = 5
def __init__(self, effect_id: int = 0): def __init__(self, effect_id: int = 0):
super().__init__() super().__init__()
self._effect_id = effect_id self._effect_id = effect_id
self._timings: Optional[List[int]] = None
self._amplitudes: Optional[List[int]] = None
self._repeat = -1
@staticmethod @staticmethod
def createOneShot(milliseconds: int, amplitude: int) -> "VibratorEffect": def createOneShot(milliseconds: int, amplitude: int) -> "VibratorEffect":
return VibratorEffect(0) """创建单次振动效果。"""
effect = VibratorEffect(0)
effect._timings = [0, milliseconds]
effect._amplitudes = [0, amplitude]
effect._repeat = -1
return effect
@staticmethod @staticmethod
def createWaveform(pattern: List[int], repeat: int) -> "VibratorEffect": def createWaveform(pattern: List[int], repeat: int) -> "VibratorEffect":
return VibratorEffect(0) """创建波形振动效果。"""
effect = VibratorEffect(0)
effect._timings = list(pattern)
effect._amplitudes = [255 if i % 2 == 1 else 0 for i in range(len(pattern))]
effect._repeat = repeat
return effect
@staticmethod @staticmethod
def createPredefined(effect_id: int) -> "VibratorEffect": def createPredefined(effect_id: int) -> "VibratorEffect":
"""创建预定义振动效果。"""
return VibratorEffect(effect_id) return VibratorEffect(effect_id)
@@ -1755,18 +1958,38 @@ class TokenWatcher(Stub):
def __init__(self, handler: Handler, tag: str): def __init__(self, handler: Handler, tag: str):
super().__init__() super().__init__()
self._handler = handler
self._tag = tag
self._tokens: Dict[int, str] = {}
def acquire(self, token: Any, tag: str): def acquire(self, token: Any, tag: str):
pass """获取令牌。"""
self._tokens[id(token)] = tag
if len(self._tokens) == 1:
self.acquired()
def release(self, token: Any): def release(self, token: Any):
pass """释放令牌。"""
self._tokens.pop(id(token), None)
if not self._tokens:
self.released()
def isAcquired(self) -> bool: def isAcquired(self) -> bool:
return False """检查是否有令牌被持有。"""
return len(self._tokens) > 0
def acquired(self):
"""当第一个令牌被获取时调用。子类可重写。"""
pass
def released(self):
"""当所有令牌被释放时调用。子类可重写。"""
pass
def dump(self): def dump(self):
pass """转储令牌信息。"""
for tid, tag in self._tokens.items():
sys.stderr.write(f"[TokenWatcher] {self._tag}: token={tid}, tag={tag}\n")
class WorkSource(Stub): class WorkSource(Stub):
@@ -1917,18 +2140,72 @@ class FileObserver(Stub):
super().__init__() super().__init__()
self._path = path self._path = path
self._watching = False self._watching = False
self._thread = None
self._snapshot: Dict[str, float] = {}
def startWatching(self): def startWatching(self):
"""开始监听文件变化。使用轮询方式检测文件系统事件。"""
if self._watching:
return
self._watching = True self._watching = True
self._snapshot = self._take_snapshot()
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
self._thread.start()
def _take_snapshot(self) -> Dict[str, float]:
"""获取当前目录的文件快照(文件名 -> 修改时间)。"""
snapshot: Dict[str, float] = {}
try:
if _os.path.isdir(self._path):
for name in _os.listdir(self._path):
full = _os.path.join(self._path, name)
try:
snapshot[name] = _os.path.getmtime(full)
except OSError:
pass
elif _os.path.exists(self._path):
try:
snapshot[self._path] = _os.path.getmtime(self._path)
except OSError:
pass
except OSError:
pass
return snapshot
def _poll_loop(self):
"""轮询检测文件变化并触发事件。"""
while self._watching:
_time.sleep(0.5)
try:
current = self._take_snapshot()
# 检测新建和修改
for name, mtime in current.items():
if name not in self._snapshot:
self.onEvent(FileObserver.CREATE, name)
elif mtime != self._snapshot[name]:
self.onEvent(FileObserver.MODIFY, name)
# 检测删除
for name in list(self._snapshot.keys()):
if name not in current:
self.onEvent(FileObserver.DELETE, name)
self._snapshot = current
except Exception:
pass
def stopWatching(self): def stopWatching(self):
"""停止监听。"""
self._watching = False self._watching = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=1.0)
self._thread = None
def onEvent(self, event: int, path: Optional[str]): def onEvent(self, event: int, path: Optional[str]):
"""文件事件回调,子类可重写。"""
pass pass
def finalize(self): def finalize(self):
pass """清理资源。"""
self.stopWatching()
class FileUtils(Stub): class FileUtils(Stub):
@@ -1985,7 +2262,35 @@ class FileUtils(Stub):
@staticmethod @staticmethod
def deleteOlderFiles(dir_: Any, min_count: int, min_age_ms: int): 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): class PatternMatcher(Stub):
+101 -4
View File
@@ -1242,8 +1242,13 @@ class ViewGroup(View, ViewParent, ViewManager):
# ─── child management ────────────────────────────────────── # ─── child management ──────────────────────────────────────
def addView(self, child: View, index: int = -1, def addView(self, child: View, index: Optional[int] = -1,
params: Optional["ViewGroup.LayoutParams"] = None): params: Optional["ViewGroup.LayoutParams"] = None):
"""addView — 支持 addView(child), addView(child, params), addView(child, index), addView(child, index, params)"""
# 智能 参数分派:第二个参数可能是 index(int) 或 params(LayoutParams)
if isinstance(index, ViewGroup.LayoutParams):
params = index
index = -1
if params is None: if params is None:
params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT) ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -1255,9 +1260,6 @@ class ViewGroup(View, ViewParent, ViewManager):
child.setLayoutParams(params) child.setLayoutParams(params)
child.onAttachedToWindow() child.onAttachedToWindow()
def addView(self, child: View, params: Optional["ViewGroup.LayoutParams"] = None):
self.addView(child, -1, params)
def removeView(self, view: View): def removeView(self, view: View):
if view in self._children: if view in self._children:
self._children.remove(view) self._children.remove(view)
@@ -1360,6 +1362,32 @@ class ViewGroup(View, ViewParent, ViewManager):
def isMotionEventSplittingEnabled(self) -> bool: def isMotionEventSplittingEnabled(self) -> bool:
return True return True
# ─── layout transition ────────────────────────────────────
def setLayoutTransition(self, transition: Any):
self._layout_transition = transition
def getLayoutTransition(self) -> Any:
return getattr(self, '_layout_transition', None)
# ─── focus / touch blocking ───────────────────────────────
def setTouchscreenBlocksFocus(self, blocks: bool):
self._touchscreen_blocks_focus = blocks
def isTouchscreenBlocksFocus(self) -> bool:
return getattr(self, '_touchscreen_blocks_focus', False)
# ─── child visible rect ───────────────────────────────────
def getChildVisibleRect(self, child: View, r: Any, offset: Any) -> bool:
if r is not None and hasattr(r, 'set'):
r.set(child.getLeft(), child.getTop(),
child.getRight(), child.getBottom())
if offset is not None and hasattr(offset, 'set'):
offset.set(0, 0)
return True
def setTransitionGroup(self, is_transition_group: bool): def setTransitionGroup(self, is_transition_group: bool):
pass pass
@@ -1809,6 +1837,75 @@ class MotionEvent(Stub):
def getHistoricalSize(self, pos: int, pointer: int = 0) -> float: def getHistoricalSize(self, pos: int, pointer: int = 0) -> float:
return 0.5 return 0.5
# ─── historical coordinates ───────────────────────────────
def getHistorySize(self) -> int:
return len(getattr(self, '_history', []))
def getHistoricalX(self, pointer_index: int = 0, pos: int = 0) -> float:
history = getattr(self, '_history', [])
if 0 <= pos < len(history):
return history[pos].get('x', self.getX(pointer_index))
return self.getX(pointer_index)
def getHistoricalY(self, pointer_index: int = 0, pos: int = 0) -> float:
history = getattr(self, '_history', [])
if 0 <= pos < len(history):
return history[pos].get('y', self.getY(pointer_index))
return self.getY(pointer_index)
def getHistoricalPressure(self, pointer_index: int = 0, pos: int = 0) -> float:
history = getattr(self, '_history', [])
if 0 <= pos < len(history):
return history[pos].get('pressure', 1.0)
return 1.0
def getHistoricalEventTime(self, pos: int = 0) -> int:
history = getattr(self, '_history', [])
if 0 <= pos < len(history):
return history[pos].get('event_time', self._event_time)
return self._event_time
# ─── edge flags ───────────────────────────────────────────
def getEdgeFlags(self) -> int:
return getattr(self, '_edge_flags', 0)
def setEdgeFlags(self, edge_flags: int):
self._edge_flags = edge_flags
# ─── action mutation ──────────────────────────────────────
def setAction(self, action: int):
self._action = action
# ─── batch ────────────────────────────────────────────────
def addBatch(self, event_time: int, x: float, y: float,
pressure: float = 1.0, size: float = 0.5,
meta_state: int = 0):
if not hasattr(self, '_history'):
self._history = []
self._history.append({
'event_time': self._event_time,
'x': self._x,
'y': self._y,
'pressure': self.getPressure(0),
'size': self.getSize(0),
'meta_state': self._meta_state,
})
self._x = x
self._y = y
self._raw_x = x
self._raw_y = y
self._event_time = event_time
self._meta_state = meta_state
if self._pointers:
self._pointers[0]['x'] = x
self._pointers[0]['y'] = y
self._pointers[0]['pressure'] = pressure
self._pointers[0]['size'] = size
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# KeyEvent # KeyEvent
+14 -8
View File
@@ -14,6 +14,8 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError from .._core import Stub, AndroidOnlyError
from ..java.lang import RuntimeException from ..java.lang import RuntimeException
from .view import KeyEvent as _BaseKeyEvent from .view import KeyEvent as _BaseKeyEvent
from .view import ViewGroup as _BaseViewGroup
from .view import View as _BaseView
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
@@ -700,8 +702,8 @@ class ViewParent(Stub):
# ViewGroup (扩展) # ViewGroup (扩展)
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
class ViewGroup(Stub): class ViewGroup(_BaseViewGroup):
"""ViewGroup — 视图组扩展""" """ViewGroup — 视图组扩展 (继承 view.ViewGroup 的所有方法)"""
CLIP_TO_PADDING_MASK = 0 CLIP_TO_PADDING_MASK = 0
FOCUS_AFTER = 1 FOCUS_AFTER = 1
@@ -782,15 +784,19 @@ class ViewGroup(Stub):
f"margins=({self.leftMargin},{self.topMargin}," f"margins=({self.leftMargin},{self.topMargin},"
f"{self.rightMargin},{self.bottomMargin}))") f"{self.rightMargin},{self.bottomMargin}))")
def __init__(self): def __init__(self, context=None):
super().__init__() super().__init__(context)
self._children: List[Any] = [] if not hasattr(self, '_children'):
self._clip_children = True self._children: List[Any] = []
self._clip_to_padding = True 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._layout_mode = self.LAYOUT_MODE_CLIP_BOUNDS
self._persistent_caching = self.PERSISTENT_NO_CACHE self._persistent_caching = self.PERSISTENT_NO_CACHE
self._touch_mode = False self._touch_mode = False
self._motion_event_splitting = True if not hasattr(self, '_motion_event_splitting'):
self._motion_event_splitting = True
self._transient_child: Optional[Any] = None self._transient_child: Optional[Any] = None
self._add_stateset_focus = False self._add_stateset_focus = False
self._child_drawable_state: Optional[Any] = None self._child_drawable_state: Optional[Any] = None
+70
View File
@@ -365,6 +365,39 @@ class TextView(View):
def setPadding(self, left: int, top: int, right: int, bottom: int): def setPadding(self, left: int, top: int, right: int, bottom: int):
super().setPadding(left, top, right, bottom) super().setPadding(left, top, right, bottom)
def setPaddingRelative(self, start: int, top: int, end: int, bottom: int):
self._padding_left = start
self._padding_top = top
self._padding_right = end
self._padding_bottom = bottom
# ─── text color getter ───────────────────────────────────
def getTextColor(self) -> int:
return self._text_color
# ─── selection action mode ───────────────────────────────
def setCustomSelectionActionModeCallback(self, callback: Any):
self._custom_selection_action_mode_callback = callback
def getCustomSelectionActionModeCallback(self) -> Any:
return getattr(self, '_custom_selection_action_mode_callback', None)
# ─── break / justification strategy ──────────────────────
def setBreakStrategy(self, break_strategy: int):
self._break_strategy = break_strategy
def getBreakStrategy(self) -> int:
return getattr(self, '_break_strategy', 0)
def setJustificationMode(self, justification_mode: int):
self._justification_mode = justification_mode
def getJustificationMode(self) -> int:
return getattr(self, '_justification_mode', 0)
def onMeasure(self, width_measure_spec: int, height_measure_spec: int): def onMeasure(self, width_measure_spec: int, height_measure_spec: int):
from .view import MeasureSpec from .view import MeasureSpec
w = MeasureSpec.getSize(width_measure_spec) w = MeasureSpec.getSize(width_measure_spec)
@@ -456,6 +489,12 @@ class CheckBox(Button):
def toggle(self): def toggle(self):
self._checked = not self._checked self._checked = not self._checked
def setOnCheckedChangeListener(self, listener: Optional[Callable]):
self._on_checked_change_listener = listener
def getOnCheckedChangeListener(self) -> Optional[Callable]:
return getattr(self, '_on_checked_change_listener', None)
def performClick(self) -> bool: def performClick(self) -> bool:
self.toggle() self.toggle()
return super().performClick() return super().performClick()
@@ -607,6 +646,22 @@ class Switch(Button):
def getTextOff(self) -> str: def getTextOff(self) -> str:
return self._text_off return self._text_off
# ─── checked change listener ─────────────────────────────
def setOnCheckedChangeListener(self, listener: Optional[Callable]):
self._on_checked_change_listener = listener
def getOnCheckedChangeListener(self) -> Optional[Callable]:
return getattr(self, '_on_checked_change_listener', None)
# ─── switch text appearance ──────────────────────────────
def setSwitchTextAppearance(self, context: Any, resid: int):
self._switch_text_appearance = resid
def getSwitchTextAppearance(self) -> int:
return getattr(self, '_switch_text_appearance', 0)
def performClick(self) -> bool: def performClick(self) -> bool:
self.toggle() self.toggle()
return super().performClick() return super().performClick()
@@ -694,6 +749,17 @@ class ImageView(View):
def getCropToPadding(self) -> bool: def getCropToPadding(self) -> bool:
return self._crop_to_padding return self._crop_to_padding
# ─── color filter ─────────────────────────────────────────
def setColorFilter(self, color_filter: Any):
self._color_filter = color_filter
def getColorFilter(self) -> Any:
return getattr(self, '_color_filter', None)
def clearColorFilter(self):
self._color_filter = None
def setBaseline(self, baseline: int): def setBaseline(self, baseline: int):
self._baseline = baseline self._baseline = baseline
@@ -1116,6 +1182,10 @@ class ListView(AdapterView):
def getCheckItemIds(self) -> List[int]: def getCheckItemIds(self) -> List[int]:
return list(self._checked_id_positions.keys()) return list(self._checked_id_positions.keys())
def clearChoices(self):
self._checked_item_positions.clear()
self._checked_id_positions.clear()
class GridView(AdapterView): class GridView(AdapterView):
"""GridView — 网格视图""" """GridView — 网格视图"""
+168 -25
View File
@@ -745,14 +745,48 @@ class BufferedInputStream(FilterInputStream):
return total if total > 0 else -1 return total if total > 0 else -1
def _fill(self): def _fill(self):
data = self._in.read(bytearray(self._buf_size), 0, self._buf_size) tmp = bytearray(self._buf_size)
if data is None or data == -1: n = self._in.read(tmp, 0, self._buf_size)
if n is None or n == -1:
return False return False
n = data if isinstance(data, int) else len(data) if isinstance(n, (bytes, bytearray)):
self._buffer = bytearray(self._in._buffer if hasattr(self._in, '_buffer') else data) if n > 0 else bytearray() # defensive: underlying stream returned raw data instead of a count
tmp = bytearray(n)
n = len(tmp)
if not isinstance(n, int) or n <= 0:
return False
self._buffer = bytearray(tmp[:n])
self._pos = 0 self._pos = 0
self._count = n self._count = n
return n > 0 return True
def available(self):
buf_avail = self._count - self._pos
if buf_avail < 0:
buf_avail = 0
try:
underlying = self._in.available()
if underlying and underlying > 0:
return buf_avail + underlying
except Exception:
pass
return buf_avail
def mark(self, readlimit):
self._mark_pos = self._pos
self._mark_buffer = bytes(self._buffer)
self._mark_count = self._count
self._mark_limit = readlimit
def reset(self):
if not hasattr(self, '_mark_pos'):
raise IOException("Resetting to invalid mark")
self._buffer = bytearray(self._mark_buffer)
self._count = self._mark_count
self._pos = self._mark_pos
def markSupported(self):
return True
# ============================================================ # ============================================================
@@ -1034,6 +1068,26 @@ class BufferedWriter(Closeable, Flushable):
def newLine(self): def newLine(self):
self._buffer.append("\n") self._buffer.append("\n")
def append(self, csq=None, start=None, end=None):
if csq is None:
return self
if isinstance(csq, int):
self._buffer.append(chr(csq))
else:
s = str(csq)
if start is not None and end is not None:
s = s[start:end]
elif start is not None:
s = s[start:]
self._buffer.append(s)
if sum(len(x) for x in self._buffer) >= self._buf_size:
self.flush()
return self
def check(self):
if getattr(self._writer, '_closed', False):
raise IOException("Stream closed")
def flush(self): def flush(self):
if self._buffer: if self._buffer:
data = "".join(self._buffer) data = "".join(self._buffer)
@@ -1171,6 +1225,20 @@ class PrintWriter(Closeable, Flushable):
else: else:
self._write_str(str(s)) self._write_str(str(s))
def append(self, csq=None, start=None, end=None):
if csq is None:
csq = "null"
if isinstance(csq, int):
self._write_str(chr(csq))
else:
s = str(csq)
if start is not None and end is not None:
s = s[start:end]
elif start is not None:
s = s[start:]
self._write_str(s)
return self
def format(self, fmt, *args): def format(self, fmt, *args):
s = str(fmt) % args if args else str(fmt) s = str(fmt) % args if args else str(fmt)
self._write_str(s) self._write_str(s)
@@ -1256,6 +1324,9 @@ class ByteArrayInputStream(Closeable):
def reset(self): def reset(self):
self._pos = self._mark self._pos = self._mark
def markSupported(self):
return True
# ============================================================ # ============================================================
# ByteArrayOutputStream # ByteArrayOutputStream
@@ -1283,6 +1354,11 @@ class ByteArrayOutputStream(Closeable, Flushable):
def toByteArray(self): def toByteArray(self):
return bytes(self._buf) return bytes(self._buf)
def writeTo(self, out):
if self._closed:
raise IOException("Stream closed")
out.write(bytes(self._buf))
def size(self): def size(self):
return len(self._buf) return len(self._buf)
@@ -1730,37 +1806,104 @@ class PushbackInputStream(FilterInputStream):
# SequenceInputStream # SequenceInputStream
# ============================================================ # ============================================================
def _is_enumeration(obj) -> bool:
"""True if obj really exposes hasMoreElements/nextElement (not via Stub)."""
for cls in type(obj).__mro__:
if ('hasMoreElements' in cls.__dict__
and 'nextElement' in cls.__dict__):
return True
return False
class SequenceInputStream(Closeable): class SequenceInputStream(Closeable):
def __init__(self, streams): def __init__(self, *args):
super().__init__() super().__init__()
if hasattr(streams, 'hasMoreElements'): self._streams = []
self._streams = []
while streams.hasMoreElements():
self._streams.append(streams.nextElement())
else:
self._streams = list(streams)
self._current_idx = 0 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): def _current(self):
if self._current_idx < len(self._streams): while self._current_idx < len(self._streams):
return self._streams[self._current_idx] inp = self._streams[self._current_idx]
if inp is None:
self._current_idx += 1
continue
return inp
return None return None
def read(self, b=None, off=None, length=None): def _advance(self):
while self._current(): if self._current_idx < len(self._streams):
inp = self._current() cur = self._streams[self._current_idx]
if b is None: if cur is not None:
result = inp.read() try:
else: cur.close()
result = inp.read(b, off, length) except Exception:
if result != -1: pass
return result
self._current_idx += 1 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): def close(self):
self._closed = True
for s in self._streams: for s in self._streams:
s.close() if s is not None:
try:
s.close()
except Exception:
pass
# ============================================================ # ============================================================
+311
View File
@@ -148,6 +148,240 @@ class Enumeration(Stub):
return Iterator(self._it) return Iterator(self._it)
# ============================================================
# Stream / Spliterator helpers (lightweight, stdlib-backed)
# ============================================================
class _Spliterator(Stub):
"""Lightweight java.util.Spliterator backed by a Python sequence."""
ORDERED = 0x00000010
DISTINCT = 0x00000001
SORTED = 0x00000004
SIZED = 0x00000040
NONNULL = 0x00000100
IMMUTABLE = 0x00000400
CONCURRENT = 0x00001000
SUBSIZED = 0x00004000
def __init__(self, data, characteristics: int = 0):
super().__init__()
self._data = list(data)
self._index = 0
self._characteristics = (characteristics
| _Spliterator.SIZED
| _Spliterator.SUBSIZED)
def tryAdvance(self, action) -> bool:
if self._index < len(self._data):
action(self._data[self._index])
self._index += 1
return True
return False
def forEachRemaining(self, action) -> None:
while self._index < len(self._data):
action(self._data[self._index])
self._index += 1
def trySplit(self):
mid = (self._index + len(self._data)) // 2
if self._index >= mid:
return None
split = _Spliterator(self._data[self._index:mid],
self._characteristics)
self._index = mid
return split
def estimateSize(self) -> int:
return len(self._data) - self._index
def getExactSizeIfKnown(self) -> int:
return self.estimateSize()
def characteristics(self) -> int:
return self._characteristics
def hasCharacteristics(self, characteristics: int) -> bool:
return (self._characteristics & characteristics) == characteristics
def iterator(self) -> Iterator:
return Iterator(self._data[self._index:])
class _Stream(Stub):
"""Lightweight java.util.stream.Stream wrapper over a Python list."""
def __init__(self, data):
super().__init__()
self._data = list(data)
self._parallel = False
def iterator(self) -> Iterator:
return Iterator(self._data)
def spliterator(self) -> _Spliterator:
return _Spliterator(self._data)
def isParallel(self) -> bool:
return self._parallel
def parallel(self) -> '_Stream':
self._parallel = True
return self
def sequential(self) -> '_Stream':
self._parallel = False
return self
def unordered(self) -> '_Stream':
return self
def filter(self, predicate) -> '_Stream':
return _Stream([x for x in self._data if predicate(x)])
def map(self, mapper) -> '_Stream':
return _Stream([mapper(x) for x in self._data])
def mapToInt(self, mapper) -> '_Stream':
return _Stream([mapper(x) for x in self._data])
def mapToLong(self, mapper) -> '_Stream':
return _Stream([mapper(x) for x in self._data])
def mapToDouble(self, mapper) -> '_Stream':
return _Stream([mapper(x) for x in self._data])
def flatMap(self, mapper) -> '_Stream':
result = []
for x in self._data:
part = mapper(x)
if isinstance(part, (list, tuple)):
result.extend(part)
else:
result.extend(list(part))
return _Stream(result)
def peek(self, action) -> '_Stream':
for x in self._data:
action(x)
return self
def distinct(self) -> '_Stream':
seen = []
seen_keys = set()
for x in self._data:
key = x if isinstance(x, (int, str, tuple, frozenset)) else id(x)
if key not in seen_keys:
seen_keys.add(key)
seen.append(x)
return _Stream(seen)
def sorted(self, key=None, reverse: bool = False) -> '_Stream':
return _Stream(sorted(self._data, key=key, reverse=reverse))
def limit(self, maxSize: int) -> '_Stream':
return _Stream(self._data[:maxSize])
def skip(self, n: int) -> '_Stream':
return _Stream(self._data[n:])
def forEach(self, action) -> None:
for x in self._data:
action(x)
def forEachOrdered(self, action) -> None:
for x in self._data:
action(x)
def toArray(self, generator=None):
if generator is None:
return list(self._data)
return list(self._data)
def reduce(self, *args):
if len(args) == 1:
accumulator = args[0]
it = iter(self._data)
try:
acc = next(it)
except StopIteration:
return Optional.empty()
for x in it:
acc = accumulator(acc, x)
return Optional.of(acc)
elif len(args) == 2:
identity, accumulator = args
acc = identity
for x in self._data:
acc = accumulator(acc, x)
return acc
else:
identity, accumulator, combiner = args
acc = identity
for x in self._data:
acc = accumulator(acc, x)
return acc
def collect(self, *args):
if not args:
return list(self._data)
if len(args) == 1:
collector = args[0]
supplier = getattr(collector, 'supplier', None)
accumulator = getattr(collector, 'accumulator', None)
finisher = getattr(collector, 'finisher', None)
if supplier is not None and accumulator is not None:
container = supplier() if callable(supplier) else supplier
for x in self._data:
if callable(accumulator):
accumulator(container, x)
if finisher is not None and callable(finisher):
return finisher(container)
return container
if callable(collector):
return collector(list(self._data))
return list(self._data)
supplier, accumulator, combiner = args[0], args[1], args[2]
container = supplier() if callable(supplier) else supplier
for x in self._data:
if callable(accumulator):
accumulator(container, x)
return container
def count(self) -> int:
return len(self._data)
def findFirst(self):
return Optional.of(self._data[0]) if self._data else Optional.empty()
def findAny(self):
return self.findFirst()
def anyMatch(self, predicate) -> bool:
return any(predicate(x) for x in self._data)
def allMatch(self, predicate) -> bool:
return all(predicate(x) for x in self._data)
def noneMatch(self, predicate) -> bool:
return not any(predicate(x) for x in self._data)
def min(self, key=None):
if not self._data:
return Optional.empty()
return Optional.of(min(self._data, key=key) if key else min(self._data))
def max(self, key=None):
if not self._data:
return Optional.empty()
return Optional.of(max(self._data, key=key) if key else max(self._data))
def onClose(self, close_handler) -> '_Stream':
return self
def close(self) -> None:
pass
# ============================================================ # ============================================================
# ArrayList # ArrayList
# ============================================================ # ============================================================
@@ -294,6 +528,12 @@ class ArrayList(Stub):
def replaceAll(self, operator) -> None: def replaceAll(self, operator) -> None:
self._data = [operator(x) for x in self._data] self._data = [operator(x) for x in self._data]
def spliterator(self) -> _Spliterator:
return _Spliterator(self._data, _Spliterator.ORDERED)
def stream(self) -> _Stream:
return _Stream(self._data)
def equals(self, other) -> bool: def equals(self, other) -> bool:
if isinstance(other, ArrayList): if isinstance(other, ArrayList):
return self._data == other._data return self._data == other._data
@@ -789,6 +1029,10 @@ class LinkedHashMap(HashMap):
key = next(iter(self._data)) key = next(iter(self._data))
return (key, self._data[key]) return (key, self._data[key])
def clone(self) -> 'LinkedHashMap':
result = LinkedHashMap(self._data, 0.75, self._access_order)
return result
# ============================================================ # ============================================================
# TreeMap # TreeMap
@@ -993,6 +1237,48 @@ class TreeMap(Stub):
return True return True
return False return False
def replaceAll(self, function) -> None:
for k in list(self._data.keys()):
self._data[k] = function(k, self._data[k])
def compute(self, key, remapping_function):
old = self._data.get(key)
new_val = remapping_function(key, old)
if new_val is not None:
self._data[key] = new_val
return new_val
self._data.pop(key, None)
return None
def computeIfAbsent(self, key, mapping_function):
if key not in self._data:
new_val = mapping_function(key)
if new_val is not None:
self._data[key] = new_val
return new_val
return self._data.get(key)
def computeIfPresent(self, key, remapping_function):
if key in self._data:
new_val = remapping_function(key, self._data[key])
if new_val is not None:
self._data[key] = new_val
return new_val
self._data.pop(key, None)
return None
def merge(self, key, value, remapping_function):
old = self._data.get(key)
if old is None:
self._data[key] = value
return value
new_val = remapping_function(old, value)
if new_val is not None:
self._data[key] = new_val
return new_val
self._data.pop(key, None)
return None
def size(self) -> int: def size(self) -> int:
return len(self._data) return len(self._data)
@@ -1140,6 +1426,19 @@ class HashSet(Stub):
for item in list(self._data): for item in list(self._data):
action(item) action(item)
def removeIf(self, predicate) -> bool:
old_size = len(self._data)
to_remove = [x for x in self._data if predicate(x)]
for x in to_remove:
self._data.discard(x)
return len(self._data) != old_size
def spliterator(self) -> _Spliterator:
return _Spliterator(self._data, _Spliterator.DISTINCT)
def stream(self) -> _Stream:
return _Stream(self._data)
def equals(self, other) -> bool: def equals(self, other) -> bool:
if isinstance(other, HashSet): if isinstance(other, HashSet):
return self._data == other._data return self._data == other._data
@@ -3445,6 +3744,18 @@ class PriorityQueue(Stub):
for item in list(self._heap): for item in list(self._heap):
action(item) action(item)
def spliterator(self) -> _Spliterator:
if self._comparator:
items = [v[1] for v in self._heap]
else:
items = list(self._heap)
return _Spliterator(items)
def clone(self) -> 'PriorityQueue':
result = PriorityQueue(comparator=self._comparator)
result._heap = list(self._heap)
return result
def toString(self) -> str: def toString(self) -> str:
items = ", ".join(str(x) for x in self._heap) items = ", ".join(str(x) for x in self._heap)
return f"[{items}]" return f"[{items}]"