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
@@ -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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user