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:
1 parent
4235f8692c
commit
fbb8263b13
13 files changed
+1373
-146
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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})"
|
||||
|
||||
Reference in new issue
Block a user