feat: 第三轮深度完善 — SQLite/JSON/网络/媒体/文本 真实实现

SQLite真实数据库:
- SQLiteDatabase: execSQL/insert/insertOrThrow/replace/update/delete/query 用sqlite3
- 修复getVersion/setVersion用PRAGMA user_version
- 修复getMaximumSize/getPageSize/setMaximumSize/setPageSize用PRAGMA
- SQLiteStatement: execute/simpleQueryForLong/simpleQueryForString/execQuery
- SQLiteOpenHelper: onCreate/onUpgrade/onDowngrade回调正确触发
- 修复SQLiteCursor.moveToPosition越界处理
- 修复ContentValues值对提取bug

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

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

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

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

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

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

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

统计: 221模块, 3666类, 31005方法, 183529行
This commit is contained in:
shuiyu1123 committed 2026-06-25 00:40:59 +00:00
1 parent 4235f8692c
commit fbb8263b13
13 files changed
+1373 -146

No files matched your search

+3
View File
@@ -446,6 +446,7 @@ __all__ = [
"MediaPlayer",
"MediaProjectionManager",
"MediaRecorder",
"MediaMetadataRetriever",
"MediaSession",
"MediaStore",
"Message",
@@ -475,6 +476,7 @@ __all__ = [
"Slice", "SliceManager", "SliceProvider",
"SmsManager",
"SpannableString",
"SpannableStringBuilder",
"SpeechRecognizer",
"Spinner",
"SQLiteDatabase", "SQLiteOpenHelper",
@@ -514,6 +516,7 @@ __all__ = [
"BufferedReader", "BufferedWriter",
"InputStreamReader", "OutputStreamWriter",
"URL", "HttpURLConnection", "Socket", "ServerSocket",
"NetworkInterface", "InetAddress", "InetSocketAddress",
"ArrayList", "HashMap", "HashSet", "LinkedList",
"TreeMap", "LinkedHashMap", "PriorityQueue",
"Collections", "Arrays", "Calendar", "Date", "Random",
+39
View File
@@ -320,6 +320,45 @@ class LinuxBluetooth:
return LinuxBluetooth.STATE_ON
return LinuxBluetooth.STATE_OFF
@staticmethod
def get_address() -> Optional[str]:
"""获取本机蓝牙适配器地址"""
if not HAS_BLUETOOTHCTL:
return None
code, out, _ = _run(["bluetoothctl", "show"])
if code == 0 and out:
for line in out.split("\n"):
line = line.strip()
if line.startswith("Controller "):
parts = line.split()
if len(parts) >= 2:
return parts[1]
return None
@staticmethod
def get_name() -> Optional[str]:
"""获取本机蓝牙适配器名称 (Alias 优先, 回退到 Controller 行括号内名称)"""
if not HAS_BLUETOOTHCTL:
return None
code, out, _ = _run(["bluetoothctl", "show"])
if code == 0 and out:
alias = None
controller_name = None
for line in out.split("\n"):
stripped = line.strip()
if stripped.startswith("Controller ") and "(" in stripped:
start = stripped.find("(")
end = stripped.find(")", start)
if start != -1 and end != -1:
controller_name = stripped[start + 1:end]
elif stripped.startswith("Alias:"):
alias = stripped[len("Alias:"):].strip()
if alias:
return alias
if controller_name:
return controller_name
return None
@staticmethod
def enable() -> bool:
if not HAS_BLUETOOTHCTL:
+8
View File
@@ -671,9 +671,17 @@ class BluetoothAdapter(Stub):
return True
def getAddress(self) -> str:
if _USE_LINUX:
addr = LinuxBluetooth.get_address()
if addr:
return addr
return self._address
def getName(self) -> str:
if _USE_LINUX:
name = LinuxBluetooth.get_name()
if name:
return name
return self._name
def setName(self, name: str) -> bool:
+19 -3
View File
@@ -331,21 +331,31 @@ class Cursor(Stub):
return self.moveToPosition(self._pos + offset)
def moveToPosition(self, position: int) -> bool:
if position < -1 or position >= self.getCount():
count = self.getCount()
if position >= count:
self._pos = count
return False
if position < -1:
self._pos = -1
return False
self._pos = position
return True
def isFirst(self) -> bool:
return self._pos == 0
return self._pos == 0 and self.getCount() > 0
def isLast(self) -> bool:
return self._pos == self.getCount() - 1
count = self.getCount()
return count > 0 and self._pos == count - 1
def isBeforeFirst(self) -> bool:
if self.getCount() == 0:
return True
return self._pos < 0
def isAfterLast(self) -> bool:
if self.getCount() == 0:
return True
return self._pos >= self.getCount()
def getColumnIndex(self, column_name: str) -> int:
@@ -370,6 +380,9 @@ class Cursor(Stub):
def getColumnNames(self) -> List[str]:
return []
def getColumnCount(self) -> int:
return len(self.getColumnNames())
def getType(self, column_index: int) -> int:
return self.FIELD_TYPE_NULL
@@ -759,6 +772,9 @@ class MatrixCursor(AbstractCursor):
def getLong(self, column_index: int) -> int:
return self.getInt(column_index)
def getShort(self, column_index: int) -> int:
return self.getInt(column_index)
def getFloat(self, column_index: int) -> float:
v = self.getString(column_index)
if v is None:
+203 -50
View File
@@ -181,9 +181,13 @@ class SQLiteProgram(SQLiteClosable):
def clearBindings(self):
self._bindings.clear()
def _ordered_bind_args(self) -> list:
"""按绑定索引升序返回参数列表(1-based 索引排序)。"""
return [self._bindings[k] for k in sorted(self._bindings.keys())]
def _bind_parameters(self, cursor: _sqlite3.Cursor):
for idx, val in sorted(self._bindings.items()):
cursor.execute(f"PRAGMA dummy={idx}")
for idx in sorted(self._bindings.keys()):
cursor.bind(idx, self._bindings[idx])
# ═══════════════════════════════════════════════════════════════
@@ -199,16 +203,18 @@ class SQLiteStatement(SQLiteProgram):
def execute(self):
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
conn.commit()
conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit()
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
def executeUpdateDelete(self) -> int:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
conn.commit()
cursor = conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit()
return cursor.rowcount
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
@@ -216,8 +222,9 @@ class SQLiteStatement(SQLiteProgram):
def executeInsert(self) -> int:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
conn.commit()
cursor = conn.execute(self._sql, self._ordered_bind_args())
if not self._db._in_transaction:
conn.commit()
return cursor.lastrowid
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
@@ -225,9 +232,10 @@ class SQLiteStatement(SQLiteProgram):
def simpleQueryForLong(self) -> int:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
cursor = conn.execute(self._sql, self._ordered_bind_args())
row = cursor.fetchone()
conn.commit()
if not self._db._in_transaction:
conn.commit()
if row:
return int(row[0]) if row[0] is not None else 0
raise SQLiteDoneException("no results")
@@ -237,15 +245,43 @@ class SQLiteStatement(SQLiteProgram):
def simpleQueryForString(self) -> Optional[str]:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, list(self._bindings.values()))
cursor = conn.execute(self._sql, self._ordered_bind_args())
row = cursor.fetchone()
conn.commit()
if not self._db._in_transaction:
conn.commit()
if row:
return str(row[0]) if row[0] is not None else None
raise SQLiteDoneException("no results")
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
def simpleQueryForBlobString(self) -> Optional[bytes]:
try:
conn = self._db._acquire_connection()
cursor = conn.execute(self._sql, self._ordered_bind_args())
row = cursor.fetchone()
if not self._db._in_transaction:
conn.commit()
if row:
v = row[0]
if v is None:
return None
if isinstance(v, bytes):
return v
if isinstance(v, str):
return v.encode("utf-8")
return bytes(str(v), "utf-8")
raise SQLiteDoneException("no results")
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), self._sql)
def execQuery(self) -> "SQLiteCursor":
"""执行查询 SQL 并返回 SQLiteCursor。"""
query = SQLiteQuery(self._db, self._sql)
for idx, val in self._bindings.items():
query.bind(idx, val)
return SQLiteCursor(self._db, query)
# ═══════════════════════════════════════════════════════════════
# SQLiteQuery
@@ -313,10 +349,17 @@ class SQLiteCursor(AbstractCursor):
def moveToPosition(self, position: int) -> bool:
self._load()
if position < -1 or position >= len(self._rows):
count = len(self._rows)
if position >= count:
self._pos = count
self._current_row = None
return False
if position < -1:
self._pos = -1
self._current_row = None
return False
self._pos = position
if 0 <= position < len(self._rows):
if 0 <= position < count:
self._current_row = self._rows[position]
else:
self._current_row = None
@@ -499,6 +542,39 @@ def _map_sqlite_error(msg: str, sql: str = "") -> SQLiteException:
return SQLiteException(msg)
def _extract_value_pairs(values: Any) -> List[Tuple[str, Any]]:
"""从 ContentValues / dict / 类似对象中提取 (列名, 值) 列表。
注意: 本实现中 ContentValues.valueSet() 返回的是键集合而非 (键, 值) 对,
因此需要通过 get() 取值。
"""
if values is None:
return []
if isinstance(values, dict):
return list(values.items())
# ContentValues 内部使用 _values dict
inner = getattr(values, "_values", None)
if isinstance(inner, dict):
return list(inner.items())
# ContentValues.valueSet() 在本实现中返回键集合
if hasattr(values, "valueSet") and hasattr(values, "get"):
try:
return [(str(k), values.get(k)) for k in values.valueSet()]
except Exception:
pass
if hasattr(values, "keySet") and hasattr(values, "get"):
try:
return [(str(k), values.get(k)) for k in values.keySet()]
except Exception:
pass
if hasattr(values, "items"):
try:
return [(str(k), v) for k, v in values.items()]
except Exception:
pass
return []
class SQLiteDatabase(SQLiteClosable):
"""SQLiteDatabase — 数据库操作类"""
@@ -536,10 +612,10 @@ class SQLiteDatabase(SQLiteClosable):
try:
db._conn = _sqlite3.connect(path)
db._conn.execute("PRAGMA journal_mode=WAL")
conn_cursor = db._conn.execute("PRAGMA schema_version")
conn_cursor = db._conn.execute("PRAGMA user_version")
row = conn_cursor.fetchone()
if row:
db._version = row[0]
db._version = int(row[0])
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e))
SQLiteDatabase._db_registry[path] = db
@@ -610,23 +686,37 @@ class SQLiteDatabase(SQLiteClosable):
self._ensure_open()
if values is None:
return -1
cols = []
vals = []
if hasattr(values, "valueSet"):
items = list(values.valueSet())
elif hasattr(values, "key_set"):
items = [(k, getattr(values, f"get{k[0].upper()}{k[1:]}", lambda: None)())
for k in values.key_set()]
elif isinstance(values, dict):
items = list(values.items())
else:
return -1
for k, v in items:
cols.append(str(k))
vals.append(v)
if len(cols) == 0:
items = _extract_value_pairs(values)
if not items:
cols = ["_id"]
vals = [None]
vals: List[Any] = [None]
else:
cols = [str(k) for k, _ in items]
vals = [v for _, v in items]
col_str = ", ".join(cols)
placeholders = ", ".join(["?"] * len(vals))
sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})"
try:
conn = self._acquire_connection()
cursor = conn.execute(sql, vals)
if not self._in_transaction:
conn.commit()
return cursor.lastrowid
except _sqlite3.DatabaseError:
return -1
def insertOrThrow(self, table: str, null_column_hack: Optional[str],
values: Any) -> int:
self._ensure_open()
if values is None:
raise SQLiteException("null values not allowed")
items = _extract_value_pairs(values)
if not items:
cols = ["_id"]
vals: List[Any] = [None]
else:
cols = [str(k) for k, _ in items]
vals = [v for _, v in items]
col_str = ", ".join(cols)
placeholders = ", ".join(["?"] * len(vals))
sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})"
@@ -639,23 +729,41 @@ class SQLiteDatabase(SQLiteClosable):
except _sqlite3.DatabaseError as e:
raise _map_sqlite_error(str(e), sql)
def replace(self, table: str, null_column_hack: Optional[str],
values: Any) -> int:
self._ensure_open()
if values is None:
return -1
items = _extract_value_pairs(values)
if not items:
cols = ["_id"]
vals: List[Any] = [None]
else:
cols = [str(k) for k, _ in items]
vals = [v for _, v in items]
col_str = ", ".join(cols)
placeholders = ", ".join(["?"] * len(vals))
sql = f"INSERT OR REPLACE INTO {table} ({col_str}) VALUES ({placeholders})"
try:
conn = self._acquire_connection()
cursor = conn.execute(sql, vals)
if not self._in_transaction:
conn.commit()
return cursor.lastrowid
except _sqlite3.DatabaseError:
return -1
def update(self, table: str, values: Any,
where_clause: Optional[str] = None,
where_args: Optional[List[Any]] = None) -> int:
self._ensure_open()
if values is None:
return 0
set_pairs = []
vals = []
if isinstance(values, dict):
items = list(values.items())
elif hasattr(values, "valueSet"):
items = list(values.valueSet())
else:
items = _extract_value_pairs(values)
if not items:
return 0
for k, v in items:
set_pairs.append(f"{k} = ?")
vals.append(v)
set_pairs = [f"{k} = ?" for k, _ in items]
vals: List[Any] = [v for _, v in items]
set_str = ", ".join(set_pairs)
sql = f"UPDATE {table} SET {set_str}"
if where_clause:
@@ -748,13 +856,24 @@ class SQLiteDatabase(SQLiteClosable):
return self._path
def getVersion(self) -> int:
if self._conn:
try:
cursor = self._conn.execute("PRAGMA user_version")
row = cursor.fetchone()
if row:
self._version = int(row[0])
return self._version
except _sqlite3.DatabaseError:
pass
return self._version
def setVersion(self, version: int):
self._version = version
self._version = int(version)
if self._conn:
try:
self._conn.execute(f"PRAGMA schema_version = {version}")
self._conn.execute(f"PRAGMA user_version = {int(version)}")
if not self._in_transaction:
self._conn.commit()
except _sqlite3.DatabaseError:
pass
@@ -784,20 +903,54 @@ class SQLiteDatabase(SQLiteClosable):
return self._enable_wal
def getMaximumSize(self) -> int:
if self._conn:
try:
page_size = self.getPageSize()
if page_size <= 0:
page_size = 4096
cursor = self._conn.execute("PRAGMA max_page_count")
row = cursor.fetchone()
if row:
self._max_size = int(row[0]) * page_size
return self._max_size
except _sqlite3.DatabaseError:
pass
return self._max_size
def getPageSize(self) -> int:
if self._conn:
try:
cursor = self._conn.execute("PRAGMA page_size")
row = cursor.fetchone()
if row:
self._page_size = int(row[0])
return self._page_size
except _sqlite3.DatabaseError:
pass
return self._page_size
def setMaximumSize(self, size: int) -> int:
self._max_size = size
return size
def setPageSize(self, size: int):
self._page_size = size
self._max_size = int(size)
if self._conn:
try:
self._conn.execute(f"PRAGMA page_size = {size}")
page_size = self.getPageSize()
if page_size <= 0:
page_size = 4096
max_pages = max(1, int(size) // page_size)
cursor = self._conn.execute(f"PRAGMA max_page_count = {max_pages}")
row = cursor.fetchone()
if row:
self._max_size = int(row[0]) * page_size
return self._max_size
except _sqlite3.DatabaseError:
pass
return self._max_size
def setPageSize(self, size: int):
self._page_size = int(size)
if self._conn:
try:
self._conn.execute(f"PRAGMA page_size = {int(size)}")
except _sqlite3.DatabaseError:
pass
+462 -33
View File
@@ -4,8 +4,10 @@ Android Media 组件 — MediaPlayer, MediaRecorder, SoundPool, AudioManager 等
"""
import io
import json
import math
import os
import subprocess
import time
import struct
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
@@ -21,6 +23,113 @@ except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# ffprobe / ffmpeg 辅助函数
# ═══════════════════════════════════════════════════════════════
def _ffprobe_metadata(path: str) -> Optional[Dict[str, Any]]:
"""用 ffprobe 获取媒体文件元数据,返回解析后的 JSON 字典。
失败时返回 None。"""
try:
result = subprocess.run(
["ffprobe", "-v", "quiet", "-print_format", "json",
"-show_format", "-show_streams", path],
capture_output=True, text=True, timeout=30,
)
if result.returncode != 0:
return None
return json.loads(result.stdout)
except (FileNotFoundError, subprocess.SubprocessError,
json.JSONDecodeError, OSError):
return None
def _ffprobe_duration_ms(path: str) -> int:
"""用 ffprobe 获取时长(毫秒),失败返回 0。"""
data = _ffprobe_metadata(path)
if not data:
return 0
try:
fmt = data.get("format", {})
duration = fmt.get("duration")
if duration is not None:
return int(float(duration) * 1000)
# 回退:从流中找
for stream in data.get("streams", []):
d = stream.get("duration")
if d is not None:
return int(float(d) * 1000)
except (ValueError, TypeError):
pass
return 0
def _ffprobe_video_size(path: str) -> Tuple[int, int]:
"""用 ffprobe 获取视频分辨率 (width, height),失败返回 (0, 0)。"""
data = _ffprobe_metadata(path)
if not data:
return (0, 0)
try:
for stream in data.get("streams", []):
if stream.get("codec_type") == "video":
w = int(stream.get("width", 0))
h = int(stream.get("height", 0))
if w > 0 and h > 0:
return (w, h)
except (ValueError, TypeError):
pass
return (0, 0)
def _ffmpeg_extract_frame(path: str, time_us: int = -1,
width: int = 0, height: int = 0) -> Optional[Any]:
"""用 ffmpeg 提取指定时间点的视频帧,返回 PIL Image。
time_us < 0 表示提取第一帧。失败返回 None。"""
if not HAS_PIL:
return None
try:
from PIL import Image
cmd = ["ffmpeg", "-v", "quiet"]
if time_us is not None and time_us >= 0:
cmd += ["-ss", str(time_us / 1_000_000.0)]
cmd += ["-i", path, "-frames:v", "1", "-f", "image2pipe",
"-vcodec", "png", "-"]
if width > 0 and height > 0:
# 在解码后缩放
cmd = ["ffmpeg", "-v", "quiet"]
if time_us is not None and time_us >= 0:
cmd += ["-ss", str(time_us / 1_000_000.0)]
cmd += ["-i", path, "-frames:v", "1", "-vf",
f"scale={width}:{height}", "-f", "image2pipe",
"-vcodec", "png", "-"]
result = subprocess.run(cmd, capture_output=True, timeout=30)
if result.returncode != 0 or not result.stdout:
return None
return Image.open(io.BytesIO(result.stdout))
except (FileNotFoundError, subprocess.SubprocessError, OSError):
return None
except Exception:
return None
def _ffmpeg_extract_cover(path: str) -> Optional[bytes]:
"""用 ffmpeg 提取嵌入的封面图片,返回原始字节。
失败返回 None。"""
try:
# 尝试提取附加图片流(封面)
cmd = ["ffmpeg", "-v", "quiet", "-i", path,
"-map", "0:v", "-frames:v", "1", "-f", "image2pipe",
"-vcodec", "png", "-"]
result = subprocess.run(cmd, capture_output=True, timeout=30)
if result.returncode == 0 and result.stdout:
return result.stdout
return None
except (FileNotFoundError, subprocess.SubprocessError, OSError):
return None
except Exception:
return None
# ═══════════════════════════════════════════════════════════════
# AudioAttributes
# ═══════════════════════════════════════════════════════════════
@@ -243,6 +352,7 @@ class MediaPlayer(Stub):
def __init__(self):
super().__init__()
self._data_source = None
self._resolved_path = None
self._prepared = False
self._playing = False
self._looping = False
@@ -257,6 +367,8 @@ class MediaPlayer(Stub):
self._audio_attributes = None
self._display = None
self._surface = None
self._released = False
self._start_time = 0.0 # 用于模拟播放进度
# Listeners
self._on_prepared_listener = None
@@ -267,43 +379,126 @@ class MediaPlayer(Stub):
self._on_video_size_changed_listener = None
def setDataSource(self, path: str = None, uri=None, headers: Dict = None):
self._data_source = path or str(uri)
self._data_source = path or str(uri) if uri is not None else path
if self._data_source:
self._resolved_path = resolve_android_path(self._data_source)
def _load_media_info(self):
"""用 ffprobe 加载时长和分辨率信息(try/except 保护)。"""
if not self._resolved_path:
return
if not os.path.exists(self._resolved_path):
return
try:
duration = _ffprobe_duration_ms(self._resolved_path)
if duration > 0:
self._duration = duration
except Exception:
pass
try:
w, h = _ffprobe_video_size(self._resolved_path)
if w > 0 and h > 0:
self._video_width = w
self._video_height = h
if self._on_video_size_changed_listener:
self._on_video_size_changed_listener.onVideoSizeChanged(
self, w, h)
except Exception:
pass
def prepare(self):
if self._released:
return
if self._resolved_path and not os.path.exists(self._resolved_path):
if self._on_error_listener:
self._on_error_listener.onError(
self, MediaPlayer.MEDIA_ERROR_UNKNOWN, -1004)
raise IOError(f"数据源文件不存在: {self._resolved_path}")
self._load_media_info()
self._prepared = True
def prepareAsync(self):
if self._released:
return
if self._resolved_path and not os.path.exists(self._resolved_path):
if self._on_error_listener:
self._on_error_listener.onError(
self, MediaPlayer.MEDIA_ERROR_UNKNOWN, -1004)
return
self._load_media_info()
self._prepared = True
if self._on_prepared_listener:
self._on_prepared_listener.onPrepared(self)
def start(self):
if self._released:
return
self._playing = True
self._start_time = time.time()
def stop(self):
self._playing = False
def pause(self):
if self._playing:
# 记录已播放的进度
elapsed = int((time.time() - self._start_time) * 1000)
self._current_position = min(
self._current_position + elapsed, self._duration)
self._playing = False
def reset(self):
self._playing = False
self._prepared = False
self._current_position = 0
self._duration = 0
self._video_width = 0
self._video_height = 0
self._data_source = None
self._resolved_path = None
def release(self):
self._playing = False
self._prepared = False
self._released = True
self._data_source = None
self._resolved_path = None
self._on_prepared_listener = None
self._on_completion_listener = None
self._on_error_listener = None
self._on_info_listener = None
self._on_seek_complete_listener = None
self._on_video_size_changed_listener = None
def seekTo(self, msec: int):
self._current_position = max(0, min(msec, self._duration))
self._start_time = time.time()
if self._on_seek_complete_listener:
self._on_seek_complete_listener.onSeekComplete(self)
def getCurrentPosition(self) -> int:
if self._playing and self._start_time > 0:
elapsed = int((time.time() - self._start_time) * 1000)
pos = self._current_position + elapsed
if self._duration > 0 and pos >= self._duration:
pos = self._duration
if self._looping:
self._current_position = 0
self._start_time = time.time()
pos = 0
else:
self._playing = False
if self._on_completion_listener:
self._on_completion_listener.onCompletion(self)
return pos
return self._current_position
def getDuration(self) -> int:
if self._duration == 0 and self._resolved_path:
try:
self._duration = _ffprobe_duration_ms(self._resolved_path)
except Exception:
pass
return self._duration
def isPlaying(self) -> bool:
@@ -335,9 +530,25 @@ class MediaPlayer(Stub):
self._display = surface_holder
def getVideoWidth(self) -> int:
if self._video_width == 0 and self._resolved_path:
try:
w, h = _ffprobe_video_size(self._resolved_path)
if w > 0:
self._video_width = w
self._video_height = h
except Exception:
pass
return self._video_width
def getVideoHeight(self) -> int:
if self._video_height == 0 and self._resolved_path:
try:
w, h = _ffprobe_video_size(self._resolved_path)
if h > 0:
self._video_width = w
self._video_height = h
except Exception:
pass
return self._video_height
def setOnPreparedListener(self, listener: "MediaPlayer.OnPreparedListener"):
@@ -434,6 +645,15 @@ class MediaRecorder(Stub):
self._output_file = None
self._preview_display = None
self._recording = False
self._prepared = False
self._released = False
self._video_width = 0
self._video_height = 0
self._video_frame_rate = 0
self._video_encoding_bit_rate = 0
self._audio_encoding_bit_rate = 0
self._max_duration = 0
self._max_filesize = 0
def setAudioSource(self, audio_source: int):
self._audio_source = audio_source
@@ -453,13 +673,36 @@ class MediaRecorder(Stub):
def setOutputFile(self, path: str):
self._output_file = path
def setVideoSize(self, width: int, height: int):
self._video_width = width
self._video_height = height
def setVideoFrameRate(self, rate: int):
self._video_frame_rate = rate
def setVideoEncodingBitRate(self, bit_rate: int):
self._video_encoding_bit_rate = bit_rate
def setAudioEncodingBitRate(self, bit_rate: int):
self._audio_encoding_bit_rate = bit_rate
def setMaxDuration(self, max_duration_ms: int):
self._max_duration = max_duration_ms
def setMaxFileSize(self, max_filesize_bytes: int):
self._max_filesize = max_filesize_bytes
def setPreviewDisplay(self, surface_holder):
self._preview_display = surface_holder
def prepare(self):
pass
if self._released:
return
self._prepared = True
def start(self):
if self._released or not self._prepared:
return
self._recording = True
def stop(self):
@@ -467,11 +710,29 @@ class MediaRecorder(Stub):
def reset(self):
self._recording = False
self._prepared = False
self._audio_source = 0
self._video_source = 0
self._output_format = 0
self._audio_encoder = 0
self._video_encoder = 0
self._output_file = None
self._video_width = 0
self._video_height = 0
self._video_frame_rate = 0
self._video_encoding_bit_rate = 0
def release(self):
self._recording = False
self._prepared = False
self._released = True
self._output_file = None
self._preview_display = None
def getMaxAmplitude(self) -> int:
if self._recording:
import random
return random.randint(0, 32767)
return 0
@@ -1025,53 +1286,221 @@ class MediaMetadataRetriever(Stub):
METADATA_KEY_BITRATE = "bitrate"
METADATA_KEY_LOCATION = "location"
# 提取帧选项
OPTION_CLOSEST = 0x01
OPTION_CLOSEST_SYNC = 0x02
OPTION_NEXT_SYNC = 0x03
OPTION_PREVIOUS_SYNC = 0x04
# ffprobe format_name 到 MIME 类型映射
_MIME_MAP = {
"mov,mp4,m4a,3gp,3g2,mj2": "video/mp4",
"matroska,webm": "video/x-matroska",
"avi": "video/x-msvideo",
"flv": "video/x-flv",
"wav": "audio/wav",
"mp3": "audio/mpeg",
"aac": "audio/aac",
"ogg": "audio/ogg",
"flac": "audio/flac",
"asf": "video/x-ms-asf",
"mpeg": "video/mpeg",
"webm": "video/webm",
}
def __init__(self):
super().__init__()
self._metadata: Dict[str, str] = {}
self._resolved_path: Optional[str] = None
self._ffprobe_data: Optional[Dict[str, Any]] = None
def setDataSource(self, path: str):
resolved = resolve_android_path(path)
if os.path.exists(resolved):
size = os.path.getsize(resolved)
self._metadata = {
MediaMetadataRetriever.METADATA_KEY_DURATION: "30000",
MediaMetadataRetriever.METADATA_KEY_TITLE: os.path.basename(resolved),
MediaMetadataRetriever.METADATA_KEY_MIMETYPE: "video/mp4",
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH: "1920",
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT: "1080",
MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO: "yes",
MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO: "yes",
MediaMetadataRetriever.METADATA_KEY_BITRATE: str(size * 8 // 30),
}
def setDataSource(self, path: str, headers: Optional[Dict[str, str]] = None):
"""设置数据源并用 ffprobe 加载元数据。"""
self._resolved_path = resolve_android_path(path)
self._metadata = {}
self._ffprobe_data = None
if not self._resolved_path or not os.path.exists(self._resolved_path):
return
# 用 ffprobe 获取真实元数据(try/except 保护)
try:
self._ffprobe_data = _ffprobe_metadata(self._resolved_path)
except Exception:
self._ffprobe_data = None
if self._ffprobe_data:
self._parse_ffprobe_metadata()
def setDataSource(self, path: str, headers: Dict[str, str]):
self.setDataSource(path)
def _parse_ffprobe_metadata(self):
"""将 ffprobe 输出解析为 Android 风格的元数据键值对。"""
if not self._ffprobe_data:
return
data = self._ffprobe_data
fmt = data.get("format", {})
tags = fmt.get("tags", {})
streams = data.get("streams", [])
# 找到视频流和音频流
video_stream = None
audio_stream = None
for s in streams:
ct = s.get("codec_type", "")
if ct == "video" and video_stream is None:
video_stream = s
elif ct == "audio" and audio_stream is None:
audio_stream = s
md = self._metadata
# 时长(毫秒)
duration = fmt.get("duration")
if duration is None and video_stream:
duration = video_stream.get("duration")
if duration is None and audio_stream:
duration = audio_stream.get("duration")
if duration is not None:
try:
md[MediaMetadataRetriever.METADATA_KEY_DURATION] = str(
int(float(duration) * 1000))
except (ValueError, TypeError):
pass
# 标题
title = tags.get("title") or tags.get("TITLE")
if title:
md[MediaMetadataRetriever.METADATA_KEY_TITLE] = title
# 艺术家
artist = tags.get("artist") or tags.get("ARTIST")
if artist:
md[MediaMetadataRetriever.METADATA_KEY_ARTIST] = artist
# 专辑
album = tags.get("album") or tags.get("ALBUM")
if album:
md[MediaMetadataRetriever.METADATA_KEY_ALBUM] = album
# 作曲家
composer = tags.get("composer") or tags.get("COMPOSER")
if composer:
md[MediaMetadataRetriever.METADATA_KEY_COMPOSER] = composer
# 作者
writer = tags.get("writer") or tags.get("WRITER")
if writer:
md[MediaMetadataRetriever.METADATA_KEY_WRITER] = writer
# 流派
genre = tags.get("genre") or tags.get("GENRE")
if genre:
md[MediaMetadataRetriever.METADATA_KEY_GENRE] = genre
# 日期 / 年份
date = tags.get("date") or tags.get("DATE") or tags.get("year")
if date:
md[MediaMetadataRetriever.METADATA_KEY_DATE] = str(date)
# 尝试提取年份
year_str = str(date)[:4]
if year_str.isdigit():
md[MediaMetadataRetriever.METADATA_KEY_YEAR] = year_str
# 音轨号
track = tags.get("track") or tags.get("TRACK")
if track:
md[MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER] = str(track)
# 位置
location = tags.get("location") or tags.get("LOCATION")
if location:
md[MediaMetadataRetriever.METADATA_KEY_LOCATION] = str(location)
# MIME 类型
format_name = fmt.get("format_name", "")
if format_name:
mime = MediaMetadataRetriever._MIME_MAP.get(
format_name, f"video/{format_name.split(',')[0]}")
md[MediaMetadataRetriever.METADATA_KEY_MIMETYPE] = mime
# 轨道数
if streams:
md[MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS] = str(len(streams))
# 比特率
bit_rate = fmt.get("bit_rate")
if bit_rate:
md[MediaMetadataRetriever.METADATA_KEY_BITRATE] = str(bit_rate)
# 视频信息
if video_stream:
w = video_stream.get("width", 0)
h = video_stream.get("height", 0)
if w:
md[MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH] = str(w)
if h:
md[MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT] = str(h)
md[MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO] = "yes"
else:
md[MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO] = "no"
# 音频信息
if audio_stream:
md[MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO] = "yes"
else:
md[MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO] = "no"
def extractMetadata(self, keyCode: str) -> Optional[str]:
return self._metadata.get(keyCode)
def getEmbeddedPicture(self) -> Optional[bytes]:
"""用 ffmpeg 提取嵌入的封面图片,返回原始字节数据。
失败返回 None。"""
if not self._resolved_path or not os.path.exists(self._resolved_path):
return None
try:
return _ffmpeg_extract_cover(self._resolved_path)
except Exception:
return None
def getFrameAtTime(self, timeUs: int = -1, option: int = 0) -> Optional["Bitmap"]:
if HAS_PIL:
from PIL import Image
from .graphics import Bitmap
img = Image.new("RGB", (1920, 1080), (64, 64, 64))
bm = Bitmap(1920, 1080, Bitmap.Config.ARGB_8888)
bm._image = img
return bm
"""用 ffmpeg 提取指定时间点的视频帧,返回 Bitmap(内含 PIL Image)。
timeUs < 0 表示提取第一帧。失败返回 None。"""
if not self._resolved_path or not os.path.exists(self._resolved_path):
return None
try:
img = _ffmpeg_extract_frame(self._resolved_path, timeUs)
if img is not None:
from .graphics import Bitmap
w, h = img.size
bm = Bitmap(w, h, Bitmap.Config.ARGB_8888)
# 确保 RGBA 模式
if img.mode != "RGBA":
img = img.convert("RGBA")
bm._image = img
return bm
except Exception:
pass
return None
def getScaledFrameAtTime(self, timeUs: int, option: int, width: int, height: int) -> Optional["Bitmap"]:
if HAS_PIL:
from PIL import Image
from .graphics import Bitmap
img = Image.new("RGB", (width, height), (64, 64, 64))
bm = Bitmap(width, height, Bitmap.Config.ARGB_8888)
bm._image = img
return bm
def getScaledFrameAtTime(self, timeUs: int, option: int,
width: int, height: int) -> Optional["Bitmap"]:
"""用 ffmpeg 提取并缩放指定时间点的视频帧。"""
if not self._resolved_path or not os.path.exists(self._resolved_path):
return None
try:
img = _ffmpeg_extract_frame(self._resolved_path, timeUs, width, height)
if img is not None:
from .graphics import Bitmap
bm = Bitmap(width, height, Bitmap.Config.ARGB_8888)
if img.mode != "RGBA":
img = img.convert("RGBA")
bm._image = img
return bm
except Exception:
pass
return None
def release(self):
self._metadata.clear()
self._resolved_path = None
self._ffprobe_data = None
# ═══════════════════════════════════════════════════════════════
+34
View File
@@ -1371,9 +1371,43 @@ class ConnectivityManager(Stub):
return self._active_network_info
def getAllNetworks(self) -> List[Network]:
if _USE_LINUX:
networks: List[Network] = []
try:
interfaces = LinuxNetwork.get_interfaces()
idx = 0
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
if not name or name == "lo":
continue
idx += 1
net = Network(idx)
networks.append(net)
# 若无真实接口, 回退到内部缓存
if networks:
return networks
except Exception:
pass
return list(self._networks.values())
def getActiveNetwork(self) -> Optional[Network]:
if _USE_LINUX:
try:
interfaces = LinuxNetwork.get_interfaces()
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
state = iface.get("operstate", "")
if name and name != "lo" and state.lower() == "up":
net = Network(1)
self._networks[1] = net
return net
# 回退: 若有外网连通性, 仍返回一个活跃 Network
if LinuxNetwork.get_connectivity():
net = Network(1)
self._networks[1] = net
return net
except Exception:
pass
if self._networks:
nid = next(iter(self._networks))
return self._networks[nid]
+61
View File
@@ -670,12 +670,73 @@ class WifiManager(Stub):
return info
def getScanResults(self) -> List[ScanResult]:
if _USE_LINUX:
results = self._linux_scan_results()
if results is not None:
return results
return list(self._scan_results)
def _linux_scan_results(self) -> Optional[List[ScanResult]]:
"""通过 nmcli 获取 WiFi 扫描结果 (真实实现)"""
import subprocess
import re
try:
proc = subprocess.run(
["nmcli", "-t", "-f", "ssid,bssid,signal,freq,security",
"dev", "wifi", "list"],
capture_output=True, text=True, timeout=15,
)
if proc.returncode != 0:
return None
results: List[ScanResult] = []
seen: Set[str] = set()
for line in proc.stdout.strip().split("\n"):
if not line:
continue
# nmcli -t 模式中值内的 : 被转义为 \:, 按非转义 : 分割
parts = re.split(r'(?<!\\):', line)
parts = [p.replace('\\:', ':') for p in parts]
if len(parts) < 5:
continue
ssid = parts[0]
bssid = parts[1]
if not bssid or bssid in seen:
continue
seen.add(bssid)
sr = ScanResult()
sr.SSID = ssid
sr.BSSID = bssid
try:
sr.level = int(parts[2]) if parts[2] else 0
except ValueError:
sr.level = 0
try:
sr.frequency = int(float(parts[3])) if parts[3] else 0
except ValueError:
sr.frequency = 0
sr.capabilities = parts[4] if len(parts) > 4 else ""
results.append(sr)
return results
except Exception:
return None
def startScan(self) -> bool:
return True
def setWifiEnabled(self, enabled: bool) -> bool:
if _USE_LINUX:
import subprocess
import shutil
try:
cmd = ["nmcli", "radio", "wifi", "on" if enabled else "off"]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
if proc.returncode != 0 and shutil.which("rfkill"):
# 回退到 rfkill
rfkill_cmd = (["rfkill", "unblock", "wifi"] if enabled
else ["rfkill", "block", "wifi"])
subprocess.run(rfkill_cmd, capture_output=True, timeout=5)
except Exception:
pass
self._wifi_enabled = enabled
self._wifi_state = self.WIFI_STATE_ENABLED if enabled else self.WIFI_STATE_DISABLED
return True
+138
View File
@@ -108,6 +108,25 @@ class _SettingsBase(Stub):
data = _SettingsBase._load(name)
return data.get(key, default_value)
@staticmethod
def putBoolean(name: str, key: str, value: bool) -> bool:
data = _SettingsBase._load(name)
data[key] = bool(value)
_SettingsBase._save(name)
return True
@staticmethod
def getBoolean(name: str, key: str, default_value: bool = False) -> bool:
data = _SettingsBase._load(name)
val = data.get(key, default_value)
if isinstance(val, bool):
return val
if isinstance(val, str):
return val.lower() in ("true", "1", "yes")
if isinstance(val, (int, float)):
return bool(val)
return default_value
class SettingsSystem(_SettingsBase):
_NAME = "settings_system"
@@ -144,6 +163,14 @@ class SettingsSystem(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsSystem._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsSystem._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsSystem._NAME, key, default_value)
class SettingsSecure(_SettingsBase):
_NAME = "settings_secure"
@@ -180,6 +207,14 @@ class SettingsSecure(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsSecure._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsSecure._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsSecure._NAME, key, default_value)
class SettingsGlobal(_SettingsBase):
_NAME = "settings_global"
@@ -216,6 +251,14 @@ class SettingsGlobal(_SettingsBase):
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsGlobal._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsGlobal._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsGlobal._NAME, key, default_value)
class Settings(Stub):
"""Settings — 系统设置"""
@@ -236,6 +279,10 @@ class Settings(Stub):
class MediaStore(Stub):
"""MediaStore — 媒体存储"""
VOLUME_INTERNAL = "internal"
VOLUME_EXTERNAL = "external"
VOLUME_EXTERNAL_PRIMARY = "external_primary"
class MediaColumns(Stub):
_ID = 0
DATA = 1
@@ -259,16 +306,35 @@ class MediaStore(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/images/media"
INTERNAL_CONTENT_URI = "content://media/internal/images/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/images/media"
class Thumbnails(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/images/thumbnails"
class Video(Stub):
class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/video/media"
INTERNAL_CONTENT_URI = "content://media/internal/video/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/video/media"
class Audio(Stub):
class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/audio/media"
INTERNAL_CONTENT_URI = "content://media/internal/audio/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/audio/media"
@staticmethod
def getMediaUri(volume_name: str) -> str:
"""返回指定卷的媒体 URI。"""
return f"content://media/{volume_name}"
@staticmethod
def getVersion(context: Any, uri: str) -> str:
@@ -337,22 +403,94 @@ class DocumentsContract(Stub):
class ContactsContract(Stub):
"""ContactsContract — 联系人合约"""
AUTHORITY = "com.android.contacts"
class Contacts(Stub):
CONTENT_URI = "content://com.android.contacts/contacts"
CONTENT_FILTER_URI = "content://com.android.contacts/contacts/filter"
CONTENT_TYPE = "vnd.android.cursor.dir/contact"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact"
DISPLAY_NAME = "display_name"
PHOTO_URI = "photo_uri"
PHOTO_ID = "photo_id"
_ID = "_id"
HAS_PHONE_NUMBER = "has_phone_number"
LOOKUP_KEY = "lookup"
STARRED = "starred"
class CommonDataKinds(Stub):
class Email(Stub):
CONTENT_URI = "content://com.android.contacts/data/emails"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/email_v2"
ADDRESS = "data1"
DISPLAY_NAME = "display_name"
TYPE = "data2"
TYPE_HOME = 1
TYPE_WORK = 2
TYPE_OTHER = 3
TYPE_MOBILE = 4
class Phone(Stub):
CONTENT_URI = "content://com.android.contacts/data/phones"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone_v2"
NUMBER = "data1"
DISPLAY_NAME = "display_name"
TYPE = "data2"
TYPE_HOME = 1
TYPE_MOBILE = 2
TYPE_WORK = 3
TYPE_FAX_WORK = 4
TYPE_FAX_HOME = 5
TYPE_PAGER = 6
TYPE_OTHER = 7
TYPE_CALLBACK = 8
TYPE_CAR = 9
TYPE_COMPANY_MAIN = 10
TYPE_ISDN = 11
TYPE_MAIN = 12
TYPE_OTHER_FAX = 13
TYPE_RADIO = 14
TYPE_TELEX = 15
TYPE_TTY_TDD = 16
TYPE_WORK_MOBILE = 17
TYPE_WORK_PAGER = 18
TYPE_ASSISTANT = 19
class StructuredName(Stub):
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/name"
DISPLAY_NAME = "data1"
GIVEN_NAME = "data2"
FAMILY_NAME = "data3"
PREFIX = "data4"
MIDDLE_NAME = "data5"
SUFFIX = "data6"
class Organization(Stub):
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/organization"
COMPANY = "data1"
TYPE = "data2"
TITLE = "data4"
TYPE_WORK = 1
TYPE_OTHER = 2
class Data(Stub):
CONTENT_URI = "content://com.android.contacts/data"
CONTENT_TYPE = "vnd.android.cursor.dir/data"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/data"
MIMETYPE = "mimetype"
_ID = "_id"
DISPLAY_NAME = "display_name"
RAW_CONTACT_ID = "raw_contact_id"
CONTACT_ID = "contact_id"
class RawContacts(Stub):
CONTENT_URI = "content://com.android.contacts/raw_contacts"
CONTENT_TYPE = "vnd.android.cursor.dir/raw_contact"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/raw_contact"
_ID = "_id"
CONTACT_ID = "contact_id"
ACCOUNT_NAME = "account_name"
ACCOUNT_TYPE = "account_type"
class ContactsContractContacts(ContactsContract.Contacts):
+80 -19
View File
@@ -7,6 +7,7 @@ import html as _html
import re as _re
import sys
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
from urllib.parse import quote_plus as _quote_plus, unquote_plus as _unquote_plus
from .._core import (
Stub, AndroidOnlyError, android_context,
@@ -176,7 +177,9 @@ class TextUtils(Stub):
@staticmethod
def split(text: str, delimiter: str) -> List[str]:
return text.split(delimiter)
if text is None or len(text) == 0:
return []
return _re.split(delimiter, text)
@staticmethod
def htmlEncode(s: str) -> str:
@@ -186,6 +189,48 @@ class TextUtils(Stub):
def htmlDecode(s: str) -> str:
return _html.unescape(s)
@staticmethod
def concat(*texts: Any) -> str:
parts = []
for t in texts:
if t is not None:
parts.append(str(t))
return "".join(parts)
@staticmethod
def indexOf(s: Any, needle: Any, start: int = 0) -> int:
if s is None:
return -1
s = str(s)
needle = str(needle)
if start < 0:
start = 0
return s.find(needle, start)
@staticmethod
def lastIndexOf(s: Any, needle: Any, last: Optional[int] = None) -> int:
if s is None:
return -1
s = str(s)
needle = str(needle)
if last is None:
return s.rfind(needle)
if last < 0:
return -1
return s.rfind(needle, 0, last + 1)
@staticmethod
def urlEncode(s: str) -> str:
if s is None:
return ""
return _quote_plus(str(s))
@staticmethod
def urlDecode(s: str) -> str:
if s is None:
return ""
return _unquote_plus(str(s))
@staticmethod
def getReverse(s: str, start: int, end: int) -> str:
return s[start:end][::-1]
@@ -227,8 +272,13 @@ class TextUtils(Stub):
return text[:max_chars] + ellipsis
@staticmethod
def isDigitsOnly(s: str) -> bool:
return s.isdigit() if s else False
def isDigitsOnly(s: Any) -> bool:
if s is None:
return False
s = str(s)
if len(s) == 0:
return False
return s.isdigit()
@staticmethod
def isGraphical(s: str) -> bool:
@@ -293,12 +343,12 @@ class Spanned(Stub):
def __init__(self, source: str = ""):
super().__init__()
self._source = source
self._spans: Dict[Tuple[type, int], Tuple[int, int, Any]] = {} # (type, id) -> (start, end, span_obj)
self._spans: Dict[Tuple[type, int], Tuple[int, int, int, Any]] = {} # (type, id) -> (start, end, flags, span_obj)
def setSpan(self, span_obj: Any, start: int, end: int, flags: int):
span_id = id(span_obj)
span_type = type(span_obj)
self._spans[(span_type, span_id)] = (start, end, span_obj)
self._spans[(span_type, span_id)] = (start, end, flags, span_obj)
def removeSpan(self, span_obj: Any):
span_id = id(span_obj)
@@ -306,12 +356,13 @@ class Spanned(Stub):
self._spans.pop((span_type, span_id), None)
def getSpans(self, start: int, end: int, span_type: Optional[type] = None) -> List[Any]:
result = []
for (st, sid), (s, e, obj) in self._spans.items():
collected = []
for (st, sid), (s, e, flags, obj) in self._spans.items():
if span_type is None or st == span_type:
if s < end and e > start:
result.append(obj)
return result
collected.append((s, obj))
collected.sort(key=lambda t: t[0])
return [obj for _, obj in collected]
def getSpanStart(self, span_obj: Any) -> int:
span_id = id(span_obj)
@@ -329,10 +380,18 @@ class Spanned(Stub):
span_id = id(span_obj)
span_type = type(span_obj)
data = self._spans.get((span_type, span_id))
return 0 if data is None else 0
return data[2] if data else 0
def nextSpanTransition(self, start: int, end: int, span_type: Optional[type] = None) -> int:
return end
result = end
for (st, sid), (s, e, flags, obj) in self._spans.items():
if span_type is not None and st != span_type:
continue
if start < s < result:
result = s
if start < e < result:
result = e
return result
def toString(self) -> str:
return self._source
@@ -435,15 +494,17 @@ class SpannableStringBuilder(Editable):
else:
super().__init__(str(source))
def append(self, text: Any) -> "SpannableStringBuilder":
super().append(text)
return self
def append(self, text: Any, start: int, end: int) -> "SpannableStringBuilder":
if isinstance(text, str):
self._source += text[start:end]
def append(self, text: Any, start: Optional[int] = None,
end: Optional[int] = None) -> "SpannableStringBuilder":
if text is None:
return self
if start is None and end is None:
self._source += str(text)
else:
self._source += str(text)[start:end]
s = 0 if start is None else start
text_str = str(text)
e = len(text_str) if end is None else end
self._source += text_str[s:e]
return self
def insert(self, where: int, text: Any) -> "SpannableStringBuilder":
+74 -18
View File
@@ -5,6 +5,7 @@ Android Util 组件 — Log, SparseArray, LruCache, DisplayMetrics 等
import math
import re as _re
import struct
import sys
from datetime import datetime
from typing import Any, Callable, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union
@@ -51,19 +52,44 @@ class Log(Stub):
return 0
@staticmethod
def w(tag: str, msg: str) -> int:
def w(tag: str, msg: str = None, tr: Exception = None) -> int:
if Log._min_log_level <= Log.WARN:
# 支持 w(tag, Throwable) 调用形式
if msg is None and tr is not None:
msg = Log.getStackTraceString(tr)
elif tr is not None:
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
sys.stderr.write(f"W/{tag}: {msg}\n")
return 0
@staticmethod
def e(tag: str, msg: str) -> int:
def e(tag: str, msg: str = None, tr: Exception = None) -> int:
if Log._min_log_level <= Log.ERROR:
# 支持 e(tag, Throwable) 调用形式
if msg is None and tr is not None:
msg = Log.getStackTraceString(tr)
elif tr is not None:
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
sys.stderr.write(f"E/{tag}: {msg}\n")
return 0
@staticmethod
def wtf(tag: str, msg: str) -> int:
def wtf(tag: str, msg=None, tr: Exception = None) -> int:
"""What a Terrible Failure。支持以下调用形式:
- wtf(tag, msg)
- wtf(tag, Throwable)
- wtf(tag, msg, Throwable)
"""
# 如果 msg 是异常对象,则当作 tr 处理
if isinstance(msg, BaseException):
tr = msg
msg = None
if msg is None and tr is not None:
msg = Log.getStackTraceString(tr)
elif tr is not None:
msg = f"{msg}\n{Log.getStackTraceString(tr)}"
if msg is None:
msg = ""
sys.stderr.write(f"WTF/{tag}: {msg}\n")
return 0
@@ -88,17 +114,16 @@ class Log(Stub):
@staticmethod
def getStackTraceString(tr: Exception) -> str:
import traceback
if tr is None:
return ""
if isinstance(tr, str):
return tr
return "".join(traceback.format_exception(type(tr), tr, tr.__traceback__))
@staticmethod
def setMinLogLevel(level: int):
Log._min_log_level = level
@staticmethod
def wtf(tag: str, tr: Exception) -> int:
sys.stderr.write(f"WTF/{tag}: {Log.getStackTraceString(tr)}\n")
return 0
# ═══════════════════════════════════════════════════════════════
# SparseArray
@@ -518,6 +543,23 @@ class TypedValue(Stub):
COMPLEX_UNIT_FRACTION = 6
COMPLEX_UNIT_FRACTION_PARENT = 7
COMPLEX_UNIT_SHIFT = 0
COMPLEX_UNIT_MASK = 0xF
COMPLEX_RADIX_SHIFT = 4
COMPLEX_RADIX_MASK = 0x3
COMPLEX_MANTISSA_SHIFT = 8
COMPLEX_MANTISSA_MASK = 0x00ffffff
# 复数值的基数乘数(与 AOSP TypedValue.RADIX_MULTS 一致)
_RADIX_MULTS = (
0.00390625, # 1/256, radix 0
3.0517578e-5, # 1/32768, radix 1
1.1920929e-7, # 1/8388608, radix 2
4.6566129e-10, # 1/2147483648, radix 3
)
TYPE_NULL = 0x00
TYPE_REFERENCE = 0x01
TYPE_ATTRIBUTE = 0x02
@@ -536,6 +578,9 @@ class TypedValue(Stub):
TYPE_LAST_COLOR_INT = 0x1F
TYPE_LAST_INT = 0x1F
DATA_NULL_UNDEFINED = 0
DATA_NULL_EMPTY = 1
def __init__(self):
super().__init__()
self.type = self.TYPE_NULL
@@ -564,18 +609,27 @@ class TypedValue(Stub):
@staticmethod
def complexToDimension(data: int, metrics: DisplayMetrics) -> float:
unit = data & 0xF
value = (data >> 4) & 0xFFFFFF
# Sign-extend
if value & 0x800000:
value = value - 0x1000000
return TypedValue.applyDimension(unit, float(value), metrics)
"""将复数维度值转换为像素值。
复数值格式:bits 0-3=unit, bits 4-5=radix, bits 8-31=mantissa(有符号24位)"""
unit = (data >> TypedValue.COMPLEX_UNIT_SHIFT) & TypedValue.COMPLEX_UNIT_MASK
radix = (data >> TypedValue.COMPLEX_RADIX_SHIFT) & TypedValue.COMPLEX_RADIX_MASK
mantissa = (data >> TypedValue.COMPLEX_MANTISSA_SHIFT) & TypedValue.COMPLEX_MANTISSA_MASK
# 24 位有符号数符号扩展
if mantissa & 0x800000:
mantissa = mantissa - 0x1000000
value = mantissa * TypedValue._RADIX_MULTS[radix]
return TypedValue.applyDimension(unit, value, metrics)
@staticmethod
def complexToFloat(data: int) -> float:
value = (data & 0xFFFFFF00) >> 8
exp = (data & 0xFF) - 150
return float(value) * (10.0 ** exp)
"""将复数值转换为浮点数(与 AOSP complexToFloat 一致)。"""
radix = (data >> TypedValue.COMPLEX_RADIX_SHIFT) & TypedValue.COMPLEX_RADIX_MASK
# mantissa 保持在原始位位置(左移 8 位),与 AOSP 一致
mantissa = data & (TypedValue.COMPLEX_MANTISSA_MASK << TypedValue.COMPLEX_MANTISSA_SHIFT)
# 处理符号(Java int 是有符号的,bit 31 为符号位)
if mantissa & 0x80000000:
mantissa = mantissa - 0x100000000
return float(mantissa) * TypedValue._RADIX_MULTS[radix]
@staticmethod
def complexToDimensionPixelOffset(data: int, metrics: DisplayMetrics) -> int:
@@ -589,7 +643,9 @@ class TypedValue(Stub):
return int(val - 0.5)
def getFloat(self) -> float:
return float(self.data)
"""将 data 字段按 IEEE 754 单精度浮点数解读(与 Float.intBitsToFloat 一致)。"""
bits = self.data & 0xFFFFFFFF
return struct.unpack('f', struct.pack('I', bits))[0]
def toString(self) -> str:
return f"TypedValue(type={self.type}, data={self.data})"
+200 -23
View File
@@ -24,7 +24,7 @@ __all__ = [
"InetAddress", "InetSocketAddress",
"Socket", "ServerSocket", "DatagramSocket", "DatagramPacket",
"Proxy", "ProxySelector", "Authenticator", "PasswordAuthentication",
"NetworkInterface", "URI",
"NetworkInterface", "InterfaceAddress", "URI",
"CookieHandler", "CookieManager", "HttpCookie",
"URLDecoder", "URLEncoder",
"SocketException", "ConnectException", "BindException",
@@ -1289,26 +1289,79 @@ class PasswordAuthentication(Stub):
return self._password
# ============================================================
# InterfaceAddress
# ============================================================
class InterfaceAddress(Stub):
"""java.net.InterfaceAddress — 接口地址信息 (地址/广播/前缀长度)"""
def __init__(self, address=None, broadcast=None, prefix_length=0):
super().__init__()
self._address = address # InetAddress
self._broadcast = broadcast # InetAddress or None
self._prefix_length = prefix_length
def getAddress(self):
return self._address
def getBroadcast(self):
return self._broadcast
def getNetworkPrefixLength(self):
return self._prefix_length
def equals(self, other):
if isinstance(other, InterfaceAddress):
return (self._address == other._address
and self._prefix_length == other._prefix_length)
return False
def hashCode(self):
return hash((repr(self._address), self._prefix_length)) & 0x7FFFFFFF
def toString(self):
addr = self._address.getHostAddress() if self._address else "0.0.0.0"
return f"{addr}/{self._prefix_length}"
# ============================================================
# NetworkInterface
# ============================================================
class NetworkInterface(Stub):
_cache = {}
_initialized = False
def __init__(self, name, display_name, addrs):
super().__init__()
self._name = name
self._display_name = display_name
self._addrs = addrs
self._mac = None # bytes, hardware address
self._mtu = 1500
self._state = "unknown"
self._flags = 0
self._interface_addrs = [] # List[InterfaceAddress]
@staticmethod
def _read_sys_file(path):
try:
with open(path, "r") as f:
return f.read().strip()
except Exception:
return None
@staticmethod
def getByName(name):
if name in NetworkInterface._cache:
return NetworkInterface._cache[name]
if name is None:
return None
NetworkInterface._ensure_init()
return NetworkInterface._cache.get(name)
@staticmethod
def getByInetAddress(addr):
NetworkInterface._ensure_init()
for ni in NetworkInterface._cache.values():
for a in ni._addrs:
if a.equals(addr):
@@ -1317,8 +1370,18 @@ class NetworkInterface(Stub):
@staticmethod
def getNetworkInterfaces():
NetworkInterface._ensure_init()
return list(NetworkInterface._cache.values())
@staticmethod
def _ensure_init():
if not NetworkInterface._initialized:
NetworkInterface._initialized = True
try:
NetworkInterface._init_from_socket()
except Exception:
pass
def getName(self):
return self._name
@@ -1329,25 +1392,25 @@ class NetworkInterface(Stub):
return iter(self._addrs)
def getInterfaceAddresses(self):
return self._addrs
return list(self._interface_addrs)
def isUp(self):
return True
return self._state.lower() == "up" or (self._flags & 0x1) != 0
def isLoopback(self):
return self._name == "lo"
return (self._flags & 0x8) != 0 or self._name == "lo"
def isPointToPoint(self):
return False
return (self._flags & 0x10) != 0
def supportsMulticast(self):
return True
return (self._flags & 0x1000) != 0
def getHardwareAddress(self):
return None
return self._mac
def getMTU(self):
return 1500
return self._mtu
def getSubInterfaces(self):
return iter([])
@@ -1363,25 +1426,139 @@ class NetworkInterface(Stub):
@staticmethod
def _init_from_socket():
import netifaces
for iface_name in netifaces.interfaces():
"""从 /sys/class/net/ 和 socket/fcntl 枚举真实网络接口 (不依赖 netifaces)"""
import os as _os
net_dir = "/sys/class/net"
if not _os.path.isdir(net_dir):
# 非 Linux 环境: 尝试用 socket 获取本机地址
try:
hostname = _socket.gethostname()
addr = InetAddress.getByName(hostname)
ni = NetworkInterface(hostname, hostname, [addr])
NetworkInterface._cache[hostname] = ni
except Exception:
pass
return
# 尝试用 fcntl.ioctl 获取 IPv4 地址/掩码/广播
fcntl_available = True
try:
import fcntl as _fcntl
except ImportError:
fcntl_available = False
for iface_name in _os.listdir(net_dir):
addrs = []
ifaddresses = netifaces.ifaddresses(iface_name)
for family in [netifaces.AF_INET, netifaces.AF_INET6]:
if family in ifaddresses:
for addr_info in ifaddresses[family]:
interface_addrs = []
mac = None
mtu = 1500
state = "unknown"
flags = 0
# --- MAC 地址 ---
mac_str = NetworkInterface._read_sys_file(f"{net_dir}/{iface_name}/address")
if mac_str:
try:
mac = bytes(int(b, 16) for b in mac_str.split(":"))
except Exception:
mac = None
# --- MTU ---
mtu_str = NetworkInterface._read_sys_file(f"{net_dir}/{iface_name}/mtu")
if mtu_str:
try:
mtu = int(mtu_str)
except ValueError:
pass
# --- operstate ---
state = NetworkInterface._read_sys_file(
f"{net_dir}/{iface_name}/operstate") or "unknown"
# --- flags (十六进制字符串, 如 0x1003) ---
flags_str = NetworkInterface._read_sys_file(
f"{net_dir}/{iface_name}/flags")
if flags_str:
try:
flags = int(flags_str, 16)
except ValueError:
pass
# --- IPv4 地址 (fcntl.ioctl) ---
if fcntl_available:
sock = None
try:
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
packed = _struct.pack('256s', iface_name.encode()[:15])
# SIOCGIFADDR = 0x8915
result = _fcntl.ioctl(sock.fileno(), 0x8915, packed)
ip_raw = result[20:24]
inet_addr = InetAddress(iface_name, bytes(ip_raw))
addrs.append(inet_addr)
# SIOCGIFNETMASK = 0x891b
prefix = 24
try:
result = _fcntl.ioctl(sock.fileno(), 0x891b, packed)
netmask_str = _socket.inet_ntoa(result[20:24])
prefix = sum(bin(int(b)).count('1') for b in netmask_str.split('.'))
except Exception:
pass
# SIOCGIFBRDADDR = 0x8919 (广播地址)
broadcast_addr = None
try:
result = _fcntl.ioctl(sock.fileno(), 0x8919, packed)
bcast_raw = result[20:24]
broadcast_addr = InetAddress(iface_name, bytes(bcast_raw))
except Exception:
pass
interface_addrs.append(InterfaceAddress(
address=inet_addr,
broadcast=broadcast_addr,
prefix_length=prefix
))
except OSError:
pass # 接口没有 IPv4 地址
finally:
if sock is not None:
try:
addr = addr_info["addr"]
if "." in addr:
addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET, addr)))
else:
addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET6, addr)))
sock.close()
except Exception:
pass
NetworkInterface._cache[iface_name] = NetworkInterface(iface_name, iface_name, addrs)
# --- IPv6 地址 (/proc/net/if_inet6) ---
try:
with open("/proc/net/if_inet6", "r") as f:
for line in f:
parts = line.split()
if len(parts) >= 6 and parts[5] == iface_name:
hex_addr = parts[0]
raw = bytes.fromhex(hex_addr)
prefix6 = int(parts[2], 16) if parts[2] else 64
inet6_addr = InetAddress(iface_name, raw)
addrs.append(inet6_addr)
interface_addrs.append(InterfaceAddress(
address=inet6_addr,
broadcast=None,
prefix_length=prefix6
))
except Exception:
pass
ni = NetworkInterface(iface_name, iface_name, addrs)
ni._mac = mac
ni._mtu = mtu
ni._state = state
ni._flags = flags
ni._interface_addrs = interface_addrs
NetworkInterface._cache[iface_name] = ni
# 初始化缓存
try:
NetworkInterface._init_from_socket()
NetworkInterface._ensure_init()
except Exception:
pass
+52
View File
@@ -115,6 +115,41 @@ class JSONObject(Stub):
self._map[k] = arr
return self
def append(self, key, value):
"""Append value to the JSONArray under key.
If the key is not present a new JSONArray containing value is created.
If the key maps to a JSONArray the value is appended to it.
Otherwise the existing value and the new value are placed into a new
JSONArray.
"""
k = str(key)
current = self._map.get(k)
if isinstance(current, JSONArray):
current.put(value)
else:
arr = JSONArray()
if current is not None and current is not JSONObject.NULL:
arr.put(current)
arr.put(value)
self._map[k] = arr
return self
def putOnce(self, key, value):
"""Put a key/value pair only if the key is not already present."""
if key is None:
raise JSONException("Null key.")
k = str(key)
if k not in self._map:
self._map[k] = JSONObject._wrap(value)
return self
def putOpt(self, key, value):
"""Put a key/value pair, but only if key and value are both non-null."""
if key is not None and value is not None:
return self.put(key, value)
return self
def get(self, key):
k = str(key)
if k not in self._map:
@@ -239,6 +274,14 @@ class JSONObject(Stub):
def has(self, key):
return str(key) in self._map
def isNull(self, key):
"""Returns true if this object has no mapping for key or if it has a
mapping whose value is JSONObject.NULL."""
k = str(key)
if k not in self._map:
return True
return self._map[k] is JSONObject.NULL
def remove(self, key):
k = str(key)
if k in self._map:
@@ -255,6 +298,15 @@ class JSONObject(Stub):
arr.put(k)
return arr
@staticmethod
def getNames(jsonObject):
"""Returns an array of field names, or None if the object is None/empty."""
if jsonObject is None:
return None
if not jsonObject._map:
return None
return [str(k) for k in jsonObject._map.keys()]
def length(self):
return len(self._map)