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