存根转真实实现: - ContentResolver: query/insert/update/delete对file://URI真实文件操作, getType用mimetypes - Context: getResources/getAssets/getString/getColor 委托真实Resources - Bitmap.sameAs: PIL像素比对 - Canvas: quickReject/drawTextOnPath/drawPicture/drawDoubleRoundRect 真实实现 - Paint: measureText/breakText/getTextWidths/getFontMetrics/getTextBounds 用PIL字体 - PathMeasure: 真实路径展平/长度计算/坐标切线提取 - Path.op/Region.op: 布尔运算 - Camera: 3D变换矩阵计算 - SystemClock.setCurrentTimeMillis: date -s命令 - Environment.isExternalStorageRemovable: /proc/mounts检查 - Process.myUid: os.getuid() - PowerManager.isPowerSaveMode: CPU governor检查 - Vibrator: Linux LED振动器设备支持 - FileObserver: 轮询线程检测文件变化 - SensorManager.getRotationMatrix: 完整旋转矩阵算法 - BatteryManager: LinuxPower真实电池数据 - Geocoder: Nominatim API逆地理编码 - LocationManager: addProximityAlert/setTestProviderLocation UI类方法补全: - TextView: setPaddingRelative/getTextColor/setBreakStrategy/setJustificationMode - ImageView: setColorFilter/getColorFilter/clearColorFilter - CheckBox/Switch: setOnCheckedChangeListener - ListView: clearChoices - ViewGroup: setLayoutTransition/setTouchscreenBlocksFocus/getChildVisibleRect - MotionEvent: getHistoricalX/Y/Pressure/EventTime/addBatch/getEdgeFlags/setAction Java集合增强: - ArrayList: spliterator/stream - HashSet: spliterator/stream/removeIf - TreeMap: replaceAll/compute/computeIfAbsent/computeIfPresent/merge - LinkedHashMap: clone (保留access_order) - PriorityQueue: spliterator/clone Java IO增强: - BufferedWriter: append/check - PrintWriter: append - ByteArrayOutputStream: writeTo - ByteArrayInputStream: markSupported - BufferedInputStream: available/mark/reset/markSupported + 修复_fill缓冲区填充bug - SequenceInputStream: 补全实现(Enumeration/多流/跨流聚合读取) 修复: - ViewGroup.addView: 合并重复定义为智能参数分派 - view_ext.ViewGroup: 继承view.ViewGroup - view_ext.KeyEvent: 继承view.KeyEvent - hardware_sensor_ext.SensorManager: 继承hardware.SensorManager - app_fragment: 使用java.lang.IllegalStateException - content_intent.Intent: setData/getScheme正确从Uri提取scheme - __init__.py: 补全Fragment/KeyEvent/MotionEvent/TrafficStats/RecyclerView/TreeMap/Vibrator等导出 统计: 221模块, 3662类, 30952方法, 182302行
2054 lines
69 KiB
Python
2054 lines
69 KiB
Python
"""
|
|
Android Content 组件 — Context, ContentResolver, Uri, SharedPreferences 等
|
|
方法签名完全对齐 Android SDK
|
|
"""
|
|
|
|
import json
|
|
import mimetypes
|
|
import os
|
|
import sys
|
|
import threading
|
|
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, quote, unquote
|
|
|
|
from .._core import (
|
|
Stub, AndroidOnlyError, android_context,
|
|
resolve_android_path, to_android_path,
|
|
_sdcard_root, _app_data_dir, _package_name,
|
|
)
|
|
|
|
try:
|
|
from .._linux_impl import IS_LINUX, LinuxClipboard, LinuxNotify, LinuxLocation, LinuxProcess
|
|
_USE_LINUX = IS_LINUX
|
|
except ImportError:
|
|
_USE_LINUX = False
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Uri
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class Uri(Stub):
|
|
"""Android Uri — 统一资源标识符"""
|
|
|
|
def __init__(self, scheme: str = "", authority: str = "", path: str = "",
|
|
query: str = "", fragment: str = "", host: str = "", port: int = -1,
|
|
ssp: str = "", user_info: str = ""):
|
|
super().__init__()
|
|
self._scheme = scheme
|
|
self._authority = authority
|
|
self._path = path
|
|
self._query = query
|
|
self._fragment = fragment
|
|
self._host = host
|
|
self._port = port
|
|
self._ssp = ssp
|
|
self._user_info = user_info
|
|
self._opaque = bool(ssp)
|
|
self._cached_string: Optional[str] = None
|
|
|
|
@staticmethod
|
|
def parse(uri_string: str) -> "Uri":
|
|
parsed = urlparse(uri_string)
|
|
host = parsed.hostname or ""
|
|
port = parsed.port or -1
|
|
return Uri(
|
|
scheme=parsed.scheme,
|
|
authority=parsed.netloc,
|
|
path=parsed.path,
|
|
query=parsed.query,
|
|
fragment=parsed.fragment,
|
|
host=host,
|
|
port=port,
|
|
user_info=parsed.username or "",
|
|
)
|
|
|
|
@staticmethod
|
|
def fromParts(scheme: str, ssp: str, fragment: str) -> "Uri":
|
|
uri = Uri(scheme=scheme, ssp=ssp, fragment=fragment)
|
|
uri._opaque = True
|
|
return uri
|
|
|
|
@staticmethod
|
|
def withAppendedPath(base_uri: "Uri", path_segment: str) -> "Uri":
|
|
new_path = base_uri._path.rstrip("/") + "/" + path_segment
|
|
return Uri(
|
|
scheme=base_uri._scheme,
|
|
authority=base_uri._authority,
|
|
path=new_path,
|
|
query=base_uri._query,
|
|
fragment=base_uri._fragment,
|
|
host=base_uri._host,
|
|
port=base_uri._port,
|
|
)
|
|
|
|
@staticmethod
|
|
def encode(s: str) -> str:
|
|
return quote(s, safe=":/?#[]@!$&'()*+,;=-._~")
|
|
|
|
@staticmethod
|
|
def decode(s: str) -> str:
|
|
return unquote(s)
|
|
|
|
def buildUpon(self) -> "Uri.Builder":
|
|
b = Uri.Builder()
|
|
b._scheme = self._scheme
|
|
b._authority = self._authority
|
|
b._path = self._path
|
|
b._query = self._query
|
|
b._fragment = self._fragment
|
|
return b
|
|
|
|
def getScheme(self) -> Optional[str]:
|
|
return self._scheme or None
|
|
|
|
def getAuthority(self) -> Optional[str]:
|
|
return self._authority or None
|
|
|
|
def getPath(self) -> Optional[str]:
|
|
return self._path or None
|
|
|
|
def getQuery(self) -> Optional[str]:
|
|
return self._query or None
|
|
|
|
def getFragment(self) -> Optional[str]:
|
|
return self._fragment or None
|
|
|
|
def getHost(self) -> Optional[str]:
|
|
return self._host or None
|
|
|
|
def getPort(self) -> int:
|
|
return self._port
|
|
|
|
def getPathSegments(self) -> List[str]:
|
|
return [s for s in self._path.split("/") if s] if self._path else []
|
|
|
|
def getLastPathSegment(self) -> Optional[str]:
|
|
segments = self.getPathSegments()
|
|
return segments[-1] if segments else None
|
|
|
|
def getUserInfo(self) -> Optional[str]:
|
|
return self._user_info or None
|
|
|
|
def isAbsolute(self) -> bool:
|
|
return bool(self._scheme)
|
|
|
|
def isOpaque(self) -> bool:
|
|
return self._opaque
|
|
|
|
def isHierarchical(self) -> bool:
|
|
return not self._opaque
|
|
|
|
def getSchemeSpecificPart(self) -> Optional[str]:
|
|
return self._ssp or None
|
|
|
|
def normalizeScheme(self) -> "Uri":
|
|
if self._scheme:
|
|
self._scheme = self._scheme.lower()
|
|
return self
|
|
|
|
def toString(self) -> str:
|
|
if self._cached_string is not None:
|
|
return self._cached_string
|
|
if self._opaque and self._ssp:
|
|
result = f"{self._scheme}:{self._ssp}"
|
|
if self._fragment:
|
|
result += f"#{self._fragment}"
|
|
else:
|
|
parts = []
|
|
if self._scheme:
|
|
parts.append(f"{self._scheme}:")
|
|
if self._authority:
|
|
parts.append(f"//{self._authority}")
|
|
if self._path:
|
|
parts.append(self._path)
|
|
if self._query:
|
|
parts.append(f"?{self._query}")
|
|
if self._fragment:
|
|
parts.append(f"#{self._fragment}")
|
|
result = "".join(parts)
|
|
self._cached_string = result
|
|
return result
|
|
|
|
def __repr__(self):
|
|
return self.toString()
|
|
|
|
def __str__(self):
|
|
return self.toString()
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, Uri):
|
|
return self.toString() == other.toString()
|
|
return NotImplemented
|
|
|
|
def __hash__(self):
|
|
return hash(self.toString())
|
|
|
|
class Builder(Stub):
|
|
"""Uri builder pattern"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._scheme = ""
|
|
self._authority = ""
|
|
self._path = ""
|
|
self._query = ""
|
|
self._fragment = ""
|
|
self._host = ""
|
|
self._port = -1
|
|
self._segments: List[str] = []
|
|
|
|
def scheme(self, scheme: str) -> "Uri.Builder":
|
|
self._scheme = scheme
|
|
return self
|
|
|
|
def authority(self, authority: str) -> "Uri.Builder":
|
|
self._authority = authority
|
|
return self
|
|
|
|
def path(self, path: str) -> "Uri.Builder":
|
|
self._path = path
|
|
return self
|
|
|
|
def appendPath(self, new_segment: str) -> "Uri.Builder":
|
|
self._segments.append(new_segment)
|
|
return self
|
|
|
|
def query(self, query: str) -> "Uri.Builder":
|
|
self._query = query
|
|
return self
|
|
|
|
def appendQueryParameter(self, key: str, value: str) -> "Uri.Builder":
|
|
params = dict(parse_qs(self._query, keep_blank_values=True)) if self._query else {}
|
|
if key in params:
|
|
params[key].append(value)
|
|
else:
|
|
params[key] = [value]
|
|
self._query = urlencode(params, doseq=True)
|
|
return self
|
|
|
|
def fragment(self, fragment: str) -> "Uri.Builder":
|
|
self._fragment = fragment
|
|
return self
|
|
|
|
def opaquePart(self, opaque_part: str) -> "Uri.Builder":
|
|
return self
|
|
|
|
def build(self) -> "Uri":
|
|
path = self._path
|
|
if self._segments:
|
|
path = (path.rstrip("/") if path else "") + "/" + "/".join(self._segments)
|
|
return Uri(self._scheme, self._authority, path, self._query, self._fragment)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ContentValues
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ContentValues(Stub):
|
|
"""ContentValues — 类似 dict 的内容值容器"""
|
|
|
|
def __init__(self, from_dict: Optional[Dict[str, Any]] = None):
|
|
super().__init__()
|
|
self._values: Dict[str, Any] = dict(from_dict) if from_dict else {}
|
|
|
|
def put(self, key: str, value: Any):
|
|
self._values[key] = value
|
|
|
|
def putNull(self, key: str):
|
|
self._values[key] = None
|
|
|
|
def get(self, key: str) -> Optional[Any]:
|
|
return self._values.get(key)
|
|
|
|
def getAsString(self, key: str) -> Optional[str]:
|
|
v = self._values.get(key)
|
|
return str(v) if v is not None else None
|
|
|
|
def getAsInteger(self, key: str) -> Optional[int]:
|
|
v = self._values.get(key)
|
|
if v is None:
|
|
return None
|
|
try:
|
|
return int(v)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
def getAsLong(self, key: str) -> Optional[int]:
|
|
return self.getAsInteger(key)
|
|
|
|
def getAsFloat(self, key: str) -> Optional[float]:
|
|
v = self._values.get(key)
|
|
if v is None:
|
|
return None
|
|
try:
|
|
return float(v)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
def getAsDouble(self, key: str) -> Optional[float]:
|
|
return self.getAsFloat(key)
|
|
|
|
def getAsBoolean(self, key: str) -> Optional[bool]:
|
|
v = self._values.get(key)
|
|
if v is None:
|
|
return None
|
|
if isinstance(v, bool):
|
|
return v
|
|
if isinstance(v, str):
|
|
return v.lower() in ("true", "1", "yes", "y")
|
|
return bool(v)
|
|
|
|
def getAsByteArray(self, key: str) -> Optional[bytes]:
|
|
v = self._values.get(key)
|
|
if isinstance(v, bytes):
|
|
return v
|
|
if isinstance(v, str):
|
|
return v.encode("utf-8")
|
|
return None
|
|
|
|
def containsKey(self, key: str) -> bool:
|
|
return key in self._values
|
|
|
|
def valueSet(self) -> Set[str]:
|
|
return set(self._values.keys())
|
|
|
|
def keySet(self) -> Set[str]:
|
|
return set(self._values.keys())
|
|
|
|
def size(self) -> int:
|
|
return len(self._values)
|
|
|
|
def isEmpty(self) -> bool:
|
|
return len(self._values) == 0
|
|
|
|
def clear(self):
|
|
self._values.clear()
|
|
|
|
def remove(self, key: str):
|
|
self._values.pop(key, None)
|
|
|
|
def putAll(self, other: "ContentValues"):
|
|
if other:
|
|
self._values.update(other._values)
|
|
|
|
def equals(self, other) -> bool:
|
|
if isinstance(other, ContentValues):
|
|
return self._values == other._values
|
|
return False
|
|
|
|
def hashCode(self) -> int:
|
|
return hash(frozenset(self._values.items())) & 0x7FFFFFFF
|
|
|
|
def toString(self) -> str:
|
|
return f"ContentValues({self._values})"
|
|
|
|
def __getitem__(self, key: str):
|
|
return self._values[key]
|
|
|
|
def __setitem__(self, key: str, value: Any):
|
|
self._values[key] = value
|
|
|
|
def __contains__(self, key: str) -> bool:
|
|
return key in self._values
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._values)
|
|
|
|
def __repr__(self):
|
|
return self.toString()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ContentResolver
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ContentResolver(Stub):
|
|
"""ContentResolver — 内容解析器"""
|
|
|
|
NOTIFY_SYNC_TO_NETWORK = 1
|
|
NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._observers: Dict[str, List[Any]] = {}
|
|
|
|
@staticmethod
|
|
def _uri_to_file_path(uri) -> Optional[str]:
|
|
"""将 file:// 或纯路径 Uri 转换为本地文件系统路径"""
|
|
try:
|
|
uri_str = str(uri)
|
|
except Exception:
|
|
return None
|
|
# 处理字符串输入
|
|
if isinstance(uri, str):
|
|
if uri.startswith("file://"):
|
|
return uri[7:]
|
|
if "://" not in uri:
|
|
return uri if uri else None
|
|
return None
|
|
scheme = getattr(uri, "_scheme", "") or ""
|
|
path = getattr(uri, "_path", "") or ""
|
|
if scheme == "file":
|
|
return path if path else None
|
|
if scheme == "" and path:
|
|
return path
|
|
return None
|
|
|
|
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
|
|
selection_args: Optional[List[str]], sort_order: Optional[str],
|
|
cancellation_signal: Any = None) -> Any:
|
|
"""Query content provider. 对 file:// URI 返回基于目录列表的 Cursor。"""
|
|
path = self._uri_to_file_path(uri)
|
|
if path is not None and os.path.isdir(path):
|
|
try:
|
|
from .database import Cursor, MatrixCursor
|
|
cols = projection or ["_id", "name", "size", "last_modified"]
|
|
cursor = MatrixCursor(cols)
|
|
for idx, name in enumerate(sorted(os.listdir(path))):
|
|
full = os.path.join(path, name)
|
|
try:
|
|
st = os.stat(full)
|
|
size = st.st_size
|
|
mtime = int(st.st_mtime * 1000)
|
|
except OSError:
|
|
size = 0
|
|
mtime = 0
|
|
cursor.addRow([idx, name, size, mtime])
|
|
return cursor
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
|
|
"""Insert. 对 file:// URI 将 ContentValues 写为 JSON 文件。"""
|
|
path = self._uri_to_file_path(uri)
|
|
if path is not None and isinstance(values, ContentValues):
|
|
try:
|
|
os.makedirs(path if os.path.isdir(path) else os.path.dirname(path) or ".", exist_ok=True)
|
|
target = path if not os.path.isdir(path) else os.path.join(path, "insert.json")
|
|
data = values._values if hasattr(values, "_values") else {}
|
|
with open(target, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, ensure_ascii=False, default=str)
|
|
return Uri.withAppendedPath(uri, os.path.basename(target))
|
|
except OSError:
|
|
pass
|
|
return uri
|
|
|
|
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
|
|
selection_args: Optional[List[str]]) -> int:
|
|
"""Update. 对 file:// URI 更新对应 JSON 文件。"""
|
|
path = self._uri_to_file_path(uri)
|
|
if path is not None and isinstance(values, ContentValues) and os.path.isfile(path):
|
|
try:
|
|
data = values._values if hasattr(values, "_values") else {}
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
existing = json.load(f)
|
|
if isinstance(existing, dict):
|
|
existing.update(data)
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(existing, f, ensure_ascii=False, default=str)
|
|
return 1
|
|
except (OSError, ValueError):
|
|
pass
|
|
return 0
|
|
|
|
def delete(self, uri: Uri, selection: Optional[str],
|
|
selection_args: Optional[List[str]]) -> int:
|
|
"""Delete. 对 file:// URI 删除对应文件或目录。"""
|
|
path = self._uri_to_file_path(uri)
|
|
if path is not None:
|
|
try:
|
|
if os.path.isfile(path):
|
|
os.remove(path)
|
|
return 1
|
|
if os.path.isdir(path):
|
|
import shutil
|
|
shutil.rmtree(path)
|
|
return 1
|
|
except OSError:
|
|
pass
|
|
return 0
|
|
|
|
def applyBatch(self, authority: str, operations: List[Any]) -> List[Any]:
|
|
"""Apply batch operations, returning per-op results."""
|
|
results: List[Any] = []
|
|
for op in operations:
|
|
try:
|
|
op_type = getattr(op, "_type", None)
|
|
uri = getattr(op, "_uri", None)
|
|
values = getattr(op, "_values", None)
|
|
if op_type == 1 and uri is not None: # INSERT
|
|
results.append(ContentProviderResult(uri=self.insert(uri, values or ContentValues())))
|
|
elif op_type == 2 and uri is not None: # UPDATE
|
|
count = self.update(uri, values or ContentValues(),
|
|
getattr(op, "_selection", None),
|
|
getattr(op, "_selection_args", None))
|
|
results.append(ContentProviderResult(count=count))
|
|
elif op_type == 3 and uri is not None: # DELETE
|
|
count = self.delete(uri, getattr(op, "_selection", None),
|
|
getattr(op, "_selection_args", None))
|
|
results.append(ContentProviderResult(count=count))
|
|
else:
|
|
results.append(ContentProviderResult(count=0))
|
|
except Exception:
|
|
results.append(ContentProviderResult(count=0))
|
|
return results
|
|
|
|
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
|
|
count = 0
|
|
for v in values or []:
|
|
if self.insert(uri, v) is not None:
|
|
count += 1
|
|
return count
|
|
|
|
def call(self, uri: Uri, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
|
|
return None
|
|
|
|
def notifyChange(self, uri: Uri, observer: Any, sync_to_network: bool = False):
|
|
uri_str = str(uri)
|
|
if uri_str in self._observers:
|
|
for obs in list(self._observers[uri_str]):
|
|
try:
|
|
obs.onChange(True)
|
|
except Exception:
|
|
pass
|
|
|
|
def registerContentObserver(self, uri: Uri, notify_for_descendants: bool, observer: Any):
|
|
uri_str = str(uri)
|
|
if uri_str not in self._observers:
|
|
self._observers[uri_str] = []
|
|
self._observers[uri_str].append(observer)
|
|
|
|
def unregisterContentObserver(self, observer: Any):
|
|
for uri_str in list(self._observers.keys()):
|
|
if observer in self._observers[uri_str]:
|
|
self._observers[uri_str].remove(observer)
|
|
if not self._observers[uri_str]:
|
|
del self._observers[uri_str]
|
|
|
|
def getType(self, uri) -> Optional[str]:
|
|
"""返回 URI 指向资源的 MIME 类型。"""
|
|
path = self._uri_to_file_path(uri)
|
|
if path:
|
|
mime, _ = mimetypes.guess_type(path)
|
|
return mime or "application/octet-stream"
|
|
scheme = getattr(uri, "_scheme", "") or ""
|
|
if isinstance(uri, str) and "://" in uri:
|
|
scheme = uri.split("://")[0]
|
|
if scheme == "content":
|
|
return "vnd.android.cursor.dir/vnd.android.content"
|
|
return None
|
|
|
|
def acquireContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]:
|
|
return ContentProviderClient(uri)
|
|
|
|
def acquireUnstableContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]:
|
|
return ContentProviderClient(uri)
|
|
|
|
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
|
|
"""返回 URI 支持的流 MIME 类型列表。"""
|
|
mime = self.getType(uri)
|
|
if mime is None:
|
|
return None
|
|
if mime_type_filter and mime_type_filter.endswith("/*"):
|
|
prefix = mime_type_filter[:-1]
|
|
if mime.startswith(prefix):
|
|
return [mime]
|
|
return None
|
|
if mime == mime_type_filter:
|
|
return [mime]
|
|
return [mime] if mime else None
|
|
|
|
def openInputStream(self, uri: Uri) -> Any:
|
|
"""打开输入流。对 file:// URI 返回真实文件流。"""
|
|
path = self._uri_to_file_path(uri)
|
|
if path and os.path.isfile(path):
|
|
try:
|
|
return open(path, "rb")
|
|
except OSError:
|
|
return None
|
|
return None
|
|
|
|
def openOutputStream(self, uri: Uri) -> Any:
|
|
"""打开输出流。对 file:// URI 返回真实文件流。"""
|
|
path = self._uri_to_file_path(uri)
|
|
if path:
|
|
try:
|
|
d = os.path.dirname(path)
|
|
if d:
|
|
os.makedirs(d, exist_ok=True)
|
|
return open(path, "wb")
|
|
except OSError:
|
|
return None
|
|
return None
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Context
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class Context(Stub):
|
|
"""Android Context — 应用上下文"""
|
|
|
|
# Service 常量
|
|
POWER_SERVICE = "power"
|
|
WINDOW_SERVICE = "window"
|
|
LAYOUT_INFLATER_SERVICE = "layout_inflater"
|
|
ACTIVITY_SERVICE = "activity"
|
|
ALARM_SERVICE = "alarm"
|
|
NOTIFICATION_SERVICE = "notification"
|
|
ACCESSIBILITY_SERVICE = "accessibility"
|
|
KEYGUARD_SERVICE = "keyguard"
|
|
LOCATION_SERVICE = "location"
|
|
SENSOR_SERVICE = "sensor"
|
|
STORAGE_SERVICE = "storage"
|
|
VIBRATOR_SERVICE = "vibrator"
|
|
CONNECTIVITY_SERVICE = "connectivity"
|
|
WIFI_SERVICE = "wifi"
|
|
AUDIO_SERVICE = "audio"
|
|
TELEPHONY_SERVICE = "telephony"
|
|
INPUT_METHOD_SERVICE = "input_method"
|
|
CAMERA_SERVICE = "camera"
|
|
BATTERY_SERVICE = "battery"
|
|
CLIPBOARD_SERVICE = "clipboard"
|
|
|
|
# Mode 常量
|
|
MODE_PRIVATE = 0x0000
|
|
MODE_WORLD_READABLE = 0x0001
|
|
MODE_WORLD_WRITEABLE = 0x0002
|
|
MODE_APPEND = 0x8000
|
|
MODE_MULTI_PROCESS = 0x0004
|
|
|
|
# Flag 常量
|
|
BIND_AUTO_CREATE = 0x0001
|
|
BIND_DEBUG_UNBIND = 0x0002
|
|
BIND_NOT_FOREGROUND = 0x0004
|
|
BIND_ABOVE_CLIENT = 0x0008
|
|
BIND_ALLOW_OOM_MANAGEMENT = 0x0010
|
|
BIND_WAIVE_PRIORITY = 0x0020
|
|
BIND_IMPORTANT = 0x0040
|
|
BIND_ADJUST_WITH_ACTIVITY = 0x0080
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._package_name = _package_name
|
|
self._services: Dict[str, Any] = {}
|
|
self._shared_prefs: Dict[str, "SharedPreferences"] = {}
|
|
self._registered_receivers: List[Dict] = []
|
|
self._content_resolver = ContentResolver()
|
|
|
|
def getPackageName(self) -> str:
|
|
return self._package_name
|
|
|
|
def getFilesDir(self):
|
|
return os.path.join(_app_data_dir, "files")
|
|
|
|
def getCacheDir(self):
|
|
return os.path.join(_app_data_dir, "cache")
|
|
|
|
def getCodeCacheDir(self):
|
|
return os.path.join(_app_data_dir, "code_cache")
|
|
|
|
def getNoBackupFilesDir(self):
|
|
return os.path.join(_app_data_dir, "no_backup")
|
|
|
|
def getExternalFilesDir(self, type_: Optional[str] = None) -> str:
|
|
base = os.path.join(_sdcard_root, "Android", "data", self._package_name, "files")
|
|
if type_:
|
|
base = os.path.join(base, type_)
|
|
os.makedirs(base, exist_ok=True)
|
|
return base
|
|
|
|
def getExternalCacheDir(self):
|
|
base = os.path.join(_sdcard_root, "Android", "data", self._package_name, "cache")
|
|
os.makedirs(base, exist_ok=True)
|
|
return base
|
|
|
|
def getObbDir(self):
|
|
base = os.path.join(_sdcard_root, "Android", "obb", self._package_name)
|
|
os.makedirs(base, exist_ok=True)
|
|
return base
|
|
|
|
def getDataDir(self):
|
|
return _app_data_dir
|
|
|
|
def getPackageCodePath(self) -> str:
|
|
return os.path.join(_app_data_dir, "base.apk")
|
|
|
|
def getPackageResourcePath(self) -> str:
|
|
return os.path.join(_app_data_dir, "base.apk")
|
|
|
|
def startActivity(self, intent: Any):
|
|
sys.stderr.write(f"[Context] startActivity: {intent}\n")
|
|
|
|
def startActivities(self, intents: List[Any], options: Optional[Any] = None):
|
|
for intent in intents:
|
|
self.startActivity(intent)
|
|
|
|
def sendBroadcast(self, intent: Any):
|
|
sys.stderr.write(f"[Context] sendBroadcast: {intent}\n")
|
|
for receiver in self._registered_receivers:
|
|
if self._matches_intent_filter(intent, receiver.get("filter")):
|
|
try:
|
|
receiver["receiver"].onReceive(self, intent)
|
|
except Exception as e:
|
|
sys.stderr.write(f"[Context] broadcast error: {e}\n")
|
|
|
|
def sendOrderedBroadcast(self, intent: Any, receiver_permission: Optional[str] = None):
|
|
self.sendBroadcast(intent)
|
|
|
|
def sendStickyBroadcast(self, intent: Any):
|
|
self.sendBroadcast(intent)
|
|
|
|
def registerReceiver(self, receiver: Optional[Any], filter: Any) -> Optional[Any]:
|
|
if receiver:
|
|
self._registered_receivers.append({"receiver": receiver, "filter": filter})
|
|
return receiver
|
|
|
|
def unregisterReceiver(self, receiver: Any):
|
|
self._registered_receivers = [
|
|
r for r in self._registered_receivers
|
|
if r["receiver"] is not receiver
|
|
]
|
|
|
|
def _matches_intent_filter(self, intent: Any, intent_filter: Any) -> bool:
|
|
if intent_filter is None:
|
|
return True
|
|
action = getattr(intent, "getAction", lambda: None)()
|
|
if intent_filter._action and action != intent_filter._action:
|
|
return False
|
|
return True
|
|
|
|
def getContentResolver(self) -> ContentResolver:
|
|
return self._content_resolver
|
|
|
|
def getSharedPreferences(self, name: str, mode: int = MODE_PRIVATE) -> "SharedPreferences":
|
|
if name not in self._shared_prefs:
|
|
self._shared_prefs[name] = SharedPreferences(name)
|
|
return self._shared_prefs[name]
|
|
|
|
def getPackageManager(self):
|
|
from .content_pm import PackageManager
|
|
return PackageManager()
|
|
|
|
def getResources(self):
|
|
from .content_res import Resources, AssetManager
|
|
if not hasattr(self, "_resources") or self._resources is None:
|
|
self._resources = Resources(self.getAssets())
|
|
return self._resources
|
|
|
|
def getAssets(self):
|
|
from .content_res import AssetManager
|
|
if not hasattr(self, "_assets") or self._assets is None:
|
|
self._assets = AssetManager()
|
|
return self._assets
|
|
|
|
def getMainLooper(self):
|
|
from .os import Looper
|
|
return Looper.getMainLooper()
|
|
|
|
def getApplicationContext(self):
|
|
return self
|
|
|
|
def getSystemService(self, name: str) -> Any:
|
|
if name == Context.ACTIVITY_SERVICE:
|
|
from .app import ActivityManager
|
|
return ActivityManager()
|
|
if name == Context.ALARM_SERVICE:
|
|
from .app import AlarmManager
|
|
return AlarmManager()
|
|
if name == Context.NOTIFICATION_SERVICE:
|
|
from .app import NotificationManager
|
|
return NotificationManager()
|
|
if name == Context.VIBRATOR_SERVICE:
|
|
from .os import Vibrator
|
|
return Vibrator()
|
|
if name == Context.POWER_SERVICE:
|
|
from .os import PowerManager
|
|
return PowerManager()
|
|
if name == Context.LOCATION_SERVICE:
|
|
from .location import LocationManager
|
|
return LocationManager()
|
|
if name == Context.SENSOR_SERVICE:
|
|
from .hardware import SensorManager
|
|
return SensorManager()
|
|
if name == Context.CONNECTIVITY_SERVICE:
|
|
from .net import ConnectivityManager
|
|
return ConnectivityManager()
|
|
if name == Context.WIFI_SERVICE:
|
|
from .net_wifi import WifiManager
|
|
return WifiManager()
|
|
if name == Context.AUDIO_SERVICE:
|
|
from .media import AudioManager
|
|
return AudioManager()
|
|
if name == Context.TELEPHONY_SERVICE:
|
|
from .telephony import TelephonyManager
|
|
return TelephonyManager()
|
|
if name == Context.INPUT_METHOD_SERVICE:
|
|
from .view_inputmethod import InputMethodManager
|
|
return InputMethodManager()
|
|
if name == Context.CAMERA_SERVICE:
|
|
from .hardware_camera2 import CameraManager
|
|
return CameraManager()
|
|
if name == Context.BATTERY_SERVICE:
|
|
from .os import BatteryManager
|
|
return BatteryManager()
|
|
if name == Context.CLIPBOARD_SERVICE:
|
|
return ClipboardManager()
|
|
return self._services.get(name)
|
|
|
|
def startService(self, intent: Any) -> Optional[Any]:
|
|
sys.stderr.write(f"[Context] startService: {intent}\n")
|
|
return None
|
|
|
|
def stopService(self, intent: Any) -> bool:
|
|
return True
|
|
|
|
def bindService(self, intent: Any, conn: Any, flags: int) -> bool:
|
|
sys.stderr.write(f"[Context] bindService: {intent}\n")
|
|
return True
|
|
|
|
def unbindService(self, conn: Any):
|
|
pass
|
|
|
|
def getString(self, res_id: int, *args) -> str:
|
|
"""返回资源字符串。优先委托给 Resources,否则返回合理默认值。"""
|
|
try:
|
|
res = self.getResources()
|
|
val = res.getString(res_id, *args)
|
|
if val is not None:
|
|
return val
|
|
except Exception:
|
|
pass
|
|
return str(res_id)
|
|
|
|
def getColor(self, color_id: int) -> int:
|
|
"""返回资源颜色。优先委托给 Resources,否则返回不透明黑色。"""
|
|
try:
|
|
res = self.getResources()
|
|
return res.getColor(color_id)
|
|
except Exception:
|
|
return 0xFF000000
|
|
|
|
def getDrawable(self, drawable_id: int):
|
|
"""返回 Drawable。优先委托给 Resources。"""
|
|
try:
|
|
res = self.getResources()
|
|
return res.getDrawable(drawable_id)
|
|
except Exception:
|
|
return None
|
|
|
|
def getWallpaper(self):
|
|
"""返回壁纸 Drawable。桌面端返回 None(无系统壁纸)。"""
|
|
return None
|
|
|
|
def getWallpaperDesiredMinimumWidth(self) -> int:
|
|
return 1080
|
|
|
|
def getWallpaperDesiredMinimumHeight(self) -> int:
|
|
return 1920
|
|
|
|
def checkCallingPermission(self, permission: str) -> int:
|
|
from .content_pm import PackageManager
|
|
return PackageManager.PERMISSION_GRANTED
|
|
|
|
def checkSelfPermission(self, permission: str) -> int:
|
|
from .content_pm import PackageManager
|
|
return PackageManager.PERMISSION_GRANTED
|
|
|
|
def shouldShowRequestPermissionRationale(self, permission: str) -> bool:
|
|
return False
|
|
|
|
def enforcePermission(self, permission: str, pid: int, uid: int, message: Optional[str]):
|
|
pass
|
|
|
|
def createPackageContext(self, package_name: str, flags: int) -> "Context":
|
|
ctx = Context()
|
|
ctx._package_name = package_name
|
|
return ctx
|
|
|
|
def isDeviceProtectedStorage(self) -> bool:
|
|
return False
|
|
|
|
def isCredentialProtectedStorage(self) -> bool:
|
|
return False
|
|
|
|
def deleteFile(self, name: str) -> bool:
|
|
path = os.path.join(self.getFilesDir(), name)
|
|
try:
|
|
os.remove(path)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
def fileList(self) -> List[str]:
|
|
try:
|
|
return os.listdir(self.getFilesDir())
|
|
except OSError:
|
|
return []
|
|
|
|
def getDatabasePath(self, name: str):
|
|
path = os.path.join(_app_data_dir, "databases", name)
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
return path
|
|
|
|
def databaseList(self) -> List[str]:
|
|
db_dir = os.path.join(_app_data_dir, "databases")
|
|
try:
|
|
return [f for f in os.listdir(db_dir) if f.endswith(".db")]
|
|
except OSError:
|
|
return []
|
|
|
|
def deleteDatabase(self, name: str) -> bool:
|
|
db_path = os.path.join(_app_data_dir, "databases", name)
|
|
try:
|
|
os.remove(db_path)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
def getDir(self, name: str, mode: int) -> str:
|
|
path = os.path.join(_app_data_dir, name)
|
|
os.makedirs(path, exist_ok=True)
|
|
return path
|
|
|
|
def toString(self) -> str:
|
|
return f"Context{{{self._package_name}}}"
|
|
|
|
# ─── 文件流 / 权限 (扩展) ─────────────────────────────────
|
|
|
|
def openFileInput(self, name):
|
|
path = os.path.join(self.getFilesDir(), name)
|
|
return open(path, 'rb')
|
|
|
|
def openFileOutput(self, name, mode):
|
|
path = os.path.join(self.getFilesDir(), name)
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
return open(path, 'wb')
|
|
|
|
def getFileStreamPath(self, name):
|
|
from ..java.io import File
|
|
return File(self.getFilesDir(), name)
|
|
|
|
def grantUriPermission(self, toPackage, uri, modeFlags):
|
|
pass
|
|
|
|
def revokeUriPermission(self, uri, modeFlags):
|
|
pass
|
|
|
|
def checkUriPermission(self, uri, pid, uid, modeFlags):
|
|
return 0 # PERMISSION_GRANTED
|
|
|
|
def checkPermission(self, permission, pid, uid):
|
|
return 0 # PERMISSION_GRANTED
|
|
|
|
def enforceUriPermission(self, uri, pid, uid, modeFlags, message):
|
|
pass
|
|
|
|
# ─── 资源 / 主题 (扩展) ───────────────────────────────────
|
|
|
|
def getApplicationInfo(self):
|
|
from .content_pm import ApplicationInfo
|
|
return ApplicationInfo()
|
|
|
|
def getTheme(self):
|
|
return getattr(self, '_theme', 0)
|
|
|
|
def setTheme(self, resid):
|
|
self._theme = resid
|
|
|
|
def getText(self, resId):
|
|
"""返回资源文本。优先委托给 Resources。"""
|
|
try:
|
|
res = self.getResources()
|
|
val = res.getText(resId)
|
|
if val is not None:
|
|
return val
|
|
except Exception:
|
|
pass
|
|
return str(resId)
|
|
|
|
def getQuantityString(self, resId, quantity, *formatArgs):
|
|
"""返回复数字符串。优先委托给 Resources。"""
|
|
try:
|
|
res = self.getResources()
|
|
return res.getQuantityString(resId, quantity, *formatArgs)
|
|
except Exception:
|
|
return str(resId)
|
|
|
|
def getStringArray(self, resId):
|
|
"""返回字符串数组。优先委托给 Resources。"""
|
|
try:
|
|
res = self.getResources()
|
|
val = res.getStringArray(resId)
|
|
if val is not None:
|
|
return val
|
|
except Exception:
|
|
pass
|
|
return []
|
|
|
|
def getTextArray(self, resId):
|
|
"""返回文本数组。优先委托给 Resources。"""
|
|
try:
|
|
res = self.getResources()
|
|
val = res.getTextArray(resId)
|
|
if val is not None:
|
|
return val
|
|
except Exception:
|
|
pass
|
|
return []
|
|
|
|
def getIntArray(self, resId):
|
|
"""返回整型数组。优先委托给 Resources。"""
|
|
try:
|
|
res = self.getResources()
|
|
val = res.getIntArray(resId)
|
|
if val is not None:
|
|
return val
|
|
except Exception:
|
|
pass
|
|
return []
|
|
|
|
# ─── Context 派生 / 回调 (扩展) ───────────────────────────
|
|
|
|
def createConfigurationContext(self, overrideConfiguration):
|
|
return Context()
|
|
|
|
def createDisplayContext(self, display):
|
|
return Context()
|
|
|
|
def registerComponentCallbacks(self, callback):
|
|
pass
|
|
|
|
def unregisterComponentCallbacks(self, callback):
|
|
pass
|
|
|
|
def getExternalStoragePublicDirectory(self, type_):
|
|
from ..java.io import File
|
|
from .os import Environment
|
|
return Environment.getExternalStoragePublicDirectory(type_)
|
|
|
|
def getDisplayMetrics(self):
|
|
from .util import DisplayMetrics
|
|
return DisplayMetrics()
|
|
|
|
|
|
class ContextWrapper(Context):
|
|
"""ContextWrapper — Context 的包装器"""
|
|
|
|
def __init__(self, base: Context):
|
|
super().__init__()
|
|
self._base = base
|
|
|
|
def getBaseContext(self) -> Context:
|
|
return self._base
|
|
|
|
def getPackageName(self) -> str:
|
|
return self._base.getPackageName()
|
|
|
|
def getFilesDir(self):
|
|
return self._base.getFilesDir()
|
|
|
|
def getCacheDir(self):
|
|
return self._base.getCacheDir()
|
|
|
|
def startActivity(self, intent: Any):
|
|
self._base.startActivity(intent)
|
|
|
|
def sendBroadcast(self, intent: Any):
|
|
self._base.sendBroadcast(intent)
|
|
|
|
def getContentResolver(self) -> ContentResolver:
|
|
return self._base.getContentResolver()
|
|
|
|
def getSharedPreferences(self, name: str, mode: int) -> "SharedPreferences":
|
|
return self._base.getSharedPreferences(name, mode)
|
|
|
|
def getSystemService(self, name: str) -> Any:
|
|
return self._base.getSystemService(name)
|
|
|
|
def startService(self, intent: Any) -> Optional[Any]:
|
|
return self._base.startService(intent)
|
|
|
|
def bindService(self, intent: Any, conn: Any, flags: int) -> bool:
|
|
return self._base.bindService(intent, conn, flags)
|
|
|
|
def registerReceiver(self, receiver: Optional[Any], filter: Any) -> Optional[Any]:
|
|
return self._base.registerReceiver(receiver, filter)
|
|
|
|
def unregisterReceiver(self, receiver: Any):
|
|
self._base.unregisterReceiver(receiver)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# IntentFilter
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class IntentFilter(Stub):
|
|
"""IntentFilter — 描述 Intent 匹配规则"""
|
|
|
|
MATCH_CATEGORY_EMPTY = 0x100000
|
|
MATCH_CATEGORY_HOST = 0x200000
|
|
MATCH_CATEGORY_PORT = 0x400000
|
|
MATCH_CATEGORY_PATH = 0x800000
|
|
MATCH_CATEGORY_SCHEME = 0x1000000
|
|
MATCH_CATEGORY_SCHEME_SPECIFIC_PART = 0x2000000
|
|
MATCH_CATEGORY_TYPE = 0x4000000
|
|
NO_MATCH_ACTION = -3
|
|
NO_MATCH_CATEGORY = -4
|
|
NO_MATCH_DATA = -5
|
|
NO_MATCH_TYPE = -6
|
|
|
|
def __init__(self, action: Optional[str] = None):
|
|
super().__init__()
|
|
self._action = action
|
|
self._actions: List[str] = []
|
|
self._categories: List[str] = []
|
|
self._data_schemes: List[str] = []
|
|
self._data_authorities: List[str] = []
|
|
self._data_paths: List[str] = []
|
|
self._data_types: List[str] = []
|
|
self._priority = 0
|
|
if action:
|
|
self.addAction(action)
|
|
|
|
def addAction(self, action: str):
|
|
if action not in self._actions:
|
|
self._actions.append(action)
|
|
|
|
def addCategory(self, category: str):
|
|
if category not in self._categories:
|
|
self._categories.append(category)
|
|
|
|
def addDataScheme(self, scheme: str):
|
|
if scheme not in self._data_schemes:
|
|
self._data_schemes.append(scheme)
|
|
|
|
def addDataAuthority(self, host: str, port: int = -1):
|
|
auth = f"{host}:{port}" if port >= 0 else host
|
|
if auth not in self._data_authorities:
|
|
self._data_authorities.append(auth)
|
|
|
|
def addDataPath(self, path: str, pattern_type: int = 0):
|
|
if path not in self._data_paths:
|
|
self._data_paths.append(path)
|
|
|
|
def addDataType(self, type_: str):
|
|
if type_ not in self._data_types:
|
|
self._data_types.append(type_)
|
|
|
|
def hasAction(self, action: str) -> bool:
|
|
return action in self._actions
|
|
|
|
def hasCategory(self, category: str) -> bool:
|
|
return category in self._categories
|
|
|
|
def hasDataScheme(self, scheme: str) -> bool:
|
|
return scheme in self._data_schemes
|
|
|
|
def countActions(self) -> int:
|
|
return len(self._actions)
|
|
|
|
def getAction(self, index: int) -> str:
|
|
return self._actions[index]
|
|
|
|
def setPriority(self, priority: int):
|
|
self._priority = priority
|
|
|
|
def getPriority(self) -> int:
|
|
return self._priority
|
|
|
|
def match(self, action: str, type_: Optional[str], scheme: Optional[str],
|
|
data: Optional[Any], categories: Optional[Set[str]], tag: str) -> int:
|
|
if action and action not in self._actions:
|
|
return self.NO_MATCH_ACTION
|
|
return self.MATCH_CATEGORY_EMPTY
|
|
|
|
def matchAction(self, action: str) -> bool:
|
|
return action in self._actions
|
|
|
|
def matchCategories(self, categories: Set[str]) -> Optional[str]:
|
|
return None
|
|
|
|
def matchData(self, type_: str, scheme: str, data: Any) -> int:
|
|
return self.MATCH_CATEGORY_EMPTY
|
|
|
|
def dump(self, du: Any, prefix: str):
|
|
pass
|
|
|
|
def describeContents(self) -> int:
|
|
return 0
|
|
|
|
def toString(self) -> str:
|
|
return f"IntentFilter(actions={self._actions})"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# BroadcastReceiver
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class BroadcastReceiver(Stub):
|
|
"""BroadcastReceiver — 广播接收器"""
|
|
|
|
RESULT_OK = -1
|
|
RESULT_CANCELED = 0
|
|
RESULT_REDIRECT = 1
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._result_code = self.RESULT_OK
|
|
self._result_data = ""
|
|
self._result_extras: Optional[ContentValues] = None
|
|
self._ordered = False
|
|
self._initial_sticky = False
|
|
|
|
def onReceive(self, context: Context, intent: Any):
|
|
"""Override this method to handle broadcasts"""
|
|
pass
|
|
|
|
def setResultCode(self, code: int):
|
|
self._result_code = code
|
|
|
|
def getResultCode(self) -> int:
|
|
return self._result_code
|
|
|
|
def setResultData(self, data: str):
|
|
self._result_data = data
|
|
|
|
def getResultData(self) -> str:
|
|
return self._result_data
|
|
|
|
def setResult(self, code: int, data: str, extras: Optional[ContentValues]):
|
|
self._result_code = code
|
|
self._result_data = data
|
|
self._result_extras = extras
|
|
|
|
def setResultExtras(self, extras: ContentValues):
|
|
self._result_extras = extras
|
|
|
|
def getResultExtras(self, make_map: bool) -> Optional[ContentValues]:
|
|
return self._result_extras
|
|
|
|
def abortBroadcast(self):
|
|
self._result_code = self.RESULT_CANCELED
|
|
|
|
def isOrderedBroadcast(self) -> bool:
|
|
return self._ordered
|
|
|
|
def isInitialStickyBroadcast(self) -> bool:
|
|
return self._initial_sticky
|
|
|
|
def getDebugUnregister(self) -> bool:
|
|
return False
|
|
|
|
def toString(self) -> str:
|
|
return f"BroadcastReceiver@{hex(id(self))}"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# SharedPreferences / SharedPreferencesEditor
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
_SHARED_PREFS_DIR = os.path.join(_app_data_dir, "shared_prefs")
|
|
os.makedirs(_SHARED_PREFS_DIR, exist_ok=True)
|
|
|
|
class SharedPreferences(Stub):
|
|
"""SharedPreferences — JSON 文件实现的持久化键值存储"""
|
|
|
|
def __init__(self, name: str):
|
|
super().__init__()
|
|
self._name = name
|
|
self._lock = threading.Lock()
|
|
self._file_path = os.path.join(_SHARED_PREFS_DIR, f"{name}.json")
|
|
self._data: Dict[str, Any] = {}
|
|
self._load()
|
|
|
|
def _load(self):
|
|
try:
|
|
if os.path.exists(self._file_path):
|
|
with open(self._file_path, "r", encoding="utf-8") as f:
|
|
self._data = json.load(f)
|
|
else:
|
|
self._data = {}
|
|
except (json.JSONDecodeError, OSError):
|
|
self._data = {}
|
|
|
|
def _save(self):
|
|
with self._lock:
|
|
os.makedirs(os.path.dirname(self._file_path), exist_ok=True)
|
|
with open(self._file_path, "w", encoding="utf-8") as f:
|
|
json.dump(self._data, f, indent=2, ensure_ascii=False)
|
|
|
|
def getString(self, key: str, def_value: Optional[str] = None) -> Optional[str]:
|
|
v = self._data.get(key)
|
|
return str(v) if v is not None else def_value
|
|
|
|
def getInt(self, key: str, def_value: int = 0) -> int:
|
|
v = self._data.get(key)
|
|
if isinstance(v, int) or isinstance(v, float):
|
|
return int(v)
|
|
return def_value
|
|
|
|
def getLong(self, key: str, def_value: int = 0) -> int:
|
|
return self.getInt(key, def_value)
|
|
|
|
def getFloat(self, key: str, def_value: float = 0.0) -> float:
|
|
v = self._data.get(key)
|
|
if isinstance(v, (int, float)):
|
|
return float(v)
|
|
return def_value
|
|
|
|
def getBoolean(self, key: str, def_value: bool = False) -> bool:
|
|
v = self._data.get(key)
|
|
if isinstance(v, bool):
|
|
return v
|
|
if isinstance(v, str):
|
|
return v.lower() in ("true", "1", "yes")
|
|
return def_value
|
|
|
|
def getAll(self) -> Dict[str, Any]:
|
|
return dict(self._data)
|
|
|
|
def contains(self, key: str) -> bool:
|
|
return key in self._data
|
|
|
|
def edit(self) -> "SharedPreferencesEditor":
|
|
return SharedPreferencesEditor(self)
|
|
|
|
def registerOnSharedPreferenceChangeListener(self, listener):
|
|
if not hasattr(self, '_listeners'):
|
|
self._listeners = []
|
|
self._listeners.append(listener)
|
|
|
|
def unregisterOnSharedPreferenceChangeListener(self, listener):
|
|
if hasattr(self, '_listeners'):
|
|
try:
|
|
self._listeners.remove(listener)
|
|
except ValueError:
|
|
pass
|
|
|
|
def getSharedPrefsFile(self):
|
|
return self._file_path
|
|
|
|
def toString(self) -> str:
|
|
return f"SharedPreferences({self._name})"
|
|
|
|
|
|
class SharedPreferencesEditor(Stub):
|
|
"""SharedPreferences Editor"""
|
|
|
|
def __init__(self, prefs: SharedPreferences):
|
|
super().__init__()
|
|
self._prefs = prefs
|
|
self._modified: Dict[str, Any] = {}
|
|
self._removed: Set[str] = set()
|
|
self._clear_all = False
|
|
|
|
def putString(self, key: str, value: str) -> "SharedPreferencesEditor":
|
|
self._modified[key] = value
|
|
self._removed.discard(key)
|
|
return self
|
|
|
|
def putInt(self, key: str, value: int) -> "SharedPreferencesEditor":
|
|
self._modified[key] = value
|
|
self._removed.discard(key)
|
|
return self
|
|
|
|
def putLong(self, key: str, value: int) -> "SharedPreferencesEditor":
|
|
self._modified[key] = value
|
|
self._removed.discard(key)
|
|
return self
|
|
|
|
def putFloat(self, key: str, value: float) -> "SharedPreferencesEditor":
|
|
self._modified[key] = value
|
|
self._removed.discard(key)
|
|
return self
|
|
|
|
def putBoolean(self, key: str, value: bool) -> "SharedPreferencesEditor":
|
|
self._modified[key] = value
|
|
self._removed.discard(key)
|
|
return self
|
|
|
|
def remove(self, key: str) -> "SharedPreferencesEditor":
|
|
self._removed.add(key)
|
|
self._modified.pop(key, None)
|
|
return self
|
|
|
|
def clear(self) -> "SharedPreferencesEditor":
|
|
self._clear_all = True
|
|
self._modified.clear()
|
|
self._removed.clear()
|
|
return self
|
|
|
|
def apply(self):
|
|
"""异步保存"""
|
|
self.commit()
|
|
|
|
def commit(self) -> bool:
|
|
try:
|
|
if self._clear_all:
|
|
self._prefs._data.clear()
|
|
for key in self._removed:
|
|
self._prefs._data.pop(key, None)
|
|
self._prefs._data.update(self._modified)
|
|
self._prefs._save()
|
|
# 触发监听器
|
|
sp = getattr(self, '_prefs', None)
|
|
if sp:
|
|
for key in self._modified:
|
|
for listener in getattr(sp, '_listeners', []):
|
|
listener.onSharedPreferenceChanged(sp, key)
|
|
self._modified.clear()
|
|
self._removed.clear()
|
|
self._clear_all = False
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ComponentName
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ComponentName(Stub):
|
|
"""ComponentName — 标识包中的特定组件"""
|
|
|
|
def __init__(self, pkg: str, cls: str):
|
|
super().__init__()
|
|
self._package = pkg
|
|
self._classname = cls
|
|
|
|
def getPackageName(self) -> str:
|
|
return self._package
|
|
|
|
def getClassName(self) -> str:
|
|
return self._classname
|
|
|
|
def getShortClassName(self) -> str:
|
|
return self._classname.split(".")[-1] if "." in self._classname else self._classname
|
|
|
|
def flattenToString(self) -> str:
|
|
return f"{self._package}/{self._classname}"
|
|
|
|
def flattenToShortString(self) -> str:
|
|
return f"{self._package}/{self.getShortClassName()}"
|
|
|
|
@staticmethod
|
|
def unflattenFromString(str_: str) -> Optional["ComponentName"]:
|
|
if "/" in str_:
|
|
parts = str_.split("/", 1)
|
|
return ComponentName(parts[0], parts[1])
|
|
return None
|
|
|
|
def toString(self) -> str:
|
|
return f"ComponentInfo{{{self._package}/{self._classname}}}"
|
|
|
|
def __repr__(self):
|
|
return self.toString()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ClipData / ClipDescription
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ClipDescription(Stub):
|
|
"""ClipDescription — 描述剪贴板数据"""
|
|
|
|
MIMETYPE_TEXT_PLAIN = "text/plain"
|
|
MIMETYPE_TEXT_HTML = "text/html"
|
|
MIMETYPE_TEXT_URILIST = "text/uri-list"
|
|
MIMETYPE_TEXT_INTENT = "text/vnd.android.intent"
|
|
|
|
def __init__(self, label: str, mime_types: List[str]):
|
|
super().__init__()
|
|
self._label = label
|
|
self._mime_types = list(mime_types)
|
|
self._timestamp = 0
|
|
|
|
def getLabel(self) -> str:
|
|
return self._label
|
|
|
|
def getMimeType(self, index: int) -> str:
|
|
if 0 <= index < len(self._mime_types):
|
|
return self._mime_types[index]
|
|
return ""
|
|
|
|
def getMimeTypeCount(self) -> int:
|
|
return len(self._mime_types)
|
|
|
|
def filterMimeTypes(self, mime_type: str) -> bool:
|
|
return mime_type in self._mime_types
|
|
|
|
def compareMimeTypes(self, mime_type1: str, mime_type2: str) -> bool:
|
|
return mime_type1 == mime_type2
|
|
|
|
def getTimestamp(self) -> int:
|
|
return self._timestamp
|
|
|
|
def toString(self) -> str:
|
|
return f"ClipDescription({self._label}, {self._mime_types})"
|
|
|
|
|
|
class ClipData(Stub):
|
|
"""ClipData — 剪贴板数据"""
|
|
|
|
def __init__(self, description: ClipDescription, item: "ClipData.Item"):
|
|
super().__init__()
|
|
self._description = description
|
|
self._items: List["ClipData.Item"] = [item] if item else []
|
|
|
|
class Item(Stub):
|
|
"""单个剪贴板条目"""
|
|
def __init__(self, text: Optional[str] = None, intent: Optional[Any] = None, uri: Optional[Uri] = None):
|
|
super().__init__()
|
|
self._text = text
|
|
self._intent = intent
|
|
self._uri = uri
|
|
self._html_text = None
|
|
|
|
def getText(self) -> Optional[str]:
|
|
return self._text
|
|
|
|
def getIntent(self):
|
|
return self._intent
|
|
|
|
def getUri(self) -> Optional[Uri]:
|
|
return self._uri
|
|
|
|
def getHtmlText(self) -> Optional[str]:
|
|
return self._html_text
|
|
|
|
def setHtmlText(self, html_text: str):
|
|
self._html_text = html_text
|
|
|
|
def coerceToText(self, context: Context) -> str:
|
|
if self._text:
|
|
return self._text
|
|
if self._uri:
|
|
return str(self._uri)
|
|
if self._intent:
|
|
return str(self._intent)
|
|
return ""
|
|
|
|
def coerceToHtmlText(self, context: Context) -> Optional[str]:
|
|
return self._html_text
|
|
|
|
def toString(self) -> str:
|
|
return f"ClipData.Item({self._text})"
|
|
|
|
@staticmethod
|
|
def newPlainText(label: str, text: str) -> "ClipData":
|
|
desc = ClipDescription(label, [ClipDescription.MIMETYPE_TEXT_PLAIN])
|
|
return ClipData(desc, ClipData.Item(text=text))
|
|
|
|
@staticmethod
|
|
def newUri(cr: ContentResolver, label: str, uri: Uri) -> "ClipData":
|
|
desc = ClipDescription(label, [ClipDescription.MIMETYPE_TEXT_URILIST])
|
|
return ClipData(desc, ClipData.Item(uri=uri))
|
|
|
|
@staticmethod
|
|
def newIntent(label: str, intent: Any) -> "ClipData":
|
|
desc = ClipDescription(label, [ClipDescription.MIMETYPE_TEXT_INTENT])
|
|
return ClipData(desc, ClipData.Item(intent=intent))
|
|
|
|
def getDescription(self) -> ClipDescription:
|
|
return self._description
|
|
|
|
def addItem(self, item: "ClipData.Item"):
|
|
self._items.append(item)
|
|
|
|
def getItemAt(self, index: int) -> Optional["ClipData.Item"]:
|
|
if 0 <= index < len(self._items):
|
|
return self._items[index]
|
|
return None
|
|
|
|
def getItemCount(self) -> int:
|
|
return len(self._items)
|
|
|
|
def toString(self) -> str:
|
|
return f"ClipData({self._description})"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ContentProvider / ContentProviderClient
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ContentProvider(Stub):
|
|
"""ContentProvider — 内容提供者基类"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._context: Optional[Context] = None
|
|
self._calling_package = ""
|
|
|
|
def onCreate(self) -> bool:
|
|
return True
|
|
|
|
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
|
|
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]:
|
|
"""默认实现:委托给关联 Context 的 ContentResolver。"""
|
|
if self._context is not None:
|
|
return self._context.getContentResolver().query(
|
|
uri, projection, selection, selection_args, sort_order)
|
|
return None
|
|
|
|
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
|
|
if self._context is not None:
|
|
return self._context.getContentResolver().insert(uri, values)
|
|
return None
|
|
|
|
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
|
|
selection_args: Optional[List[str]]) -> int:
|
|
if self._context is not None:
|
|
return self._context.getContentResolver().update(uri, values, selection, selection_args)
|
|
return 0
|
|
|
|
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int:
|
|
if self._context is not None:
|
|
return self._context.getContentResolver().delete(uri, selection, selection_args)
|
|
return 0
|
|
|
|
def getType(self, uri: Uri) -> Optional[str]:
|
|
"""返回 URI 的 MIME 类型。"""
|
|
path = ContentResolver._uri_to_file_path(uri)
|
|
if path:
|
|
mime, _ = mimetypes.guess_type(path)
|
|
return mime or "application/octet-stream"
|
|
return None
|
|
|
|
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
|
|
mime = self.getType(uri)
|
|
if mime is None:
|
|
return None
|
|
return [mime]
|
|
|
|
def openFile(self, uri: Uri, mode: str) -> Any:
|
|
"""打开文件。对 file:// URI 返回真实文件对象。"""
|
|
path = ContentResolver._uri_to_file_path(uri)
|
|
if path and os.path.exists(path):
|
|
try:
|
|
fmode = "rb" if "r" in mode else "wb"
|
|
return open(path, fmode)
|
|
except OSError:
|
|
return None
|
|
return None
|
|
|
|
def openAssetFile(self, uri: Uri, mode: str) -> Any:
|
|
return self.openFile(uri, mode)
|
|
|
|
def attachInfo(self, context: Context, info: Any):
|
|
self._context = context
|
|
|
|
def getContext(self) -> Optional[Context]:
|
|
return self._context
|
|
|
|
def getCallingPackage(self) -> str:
|
|
return self._calling_package
|
|
|
|
def applyBatch(self, operations: List[Any]) -> List[Any]:
|
|
return []
|
|
|
|
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
|
|
return len(values) if values else 0
|
|
|
|
def isTemporary(self) -> bool:
|
|
return False
|
|
|
|
def shutdown(self):
|
|
pass
|
|
|
|
def toString(self) -> str:
|
|
return f"ContentProvider@{hex(id(self))}"
|
|
|
|
|
|
class ContentProviderClient(Stub):
|
|
"""ContentProviderClient — 内容提供者客户端"""
|
|
|
|
def __init__(self, uri: Optional[Uri] = None):
|
|
super().__init__()
|
|
self._uri = uri
|
|
|
|
def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str],
|
|
selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]:
|
|
"""委托给 ContentResolver 处理 file:// URI。"""
|
|
resolver = ContentResolver()
|
|
return resolver.query(uri, projection, selection, selection_args, sort_order)
|
|
|
|
def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]:
|
|
resolver = ContentResolver()
|
|
return resolver.insert(uri, values)
|
|
|
|
def update(self, uri: Uri, values: ContentValues, selection: Optional[str],
|
|
selection_args: Optional[List[str]]) -> int:
|
|
resolver = ContentResolver()
|
|
return resolver.update(uri, values, selection, selection_args)
|
|
|
|
def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int:
|
|
resolver = ContentResolver()
|
|
return resolver.delete(uri, selection, selection_args)
|
|
|
|
def applyBatch(self, operations: List[Any]) -> List[Any]:
|
|
resolver = ContentResolver()
|
|
return resolver.applyBatch("", operations)
|
|
|
|
def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int:
|
|
resolver = ContentResolver()
|
|
return resolver.bulkInsert(uri, values)
|
|
|
|
def getType(self, uri: Uri) -> Optional[str]:
|
|
"""返回 URI 的 MIME 类型。"""
|
|
path = ContentResolver._uri_to_file_path(uri)
|
|
if path:
|
|
mime, _ = mimetypes.guess_type(path)
|
|
return mime or "application/octet-stream"
|
|
return None
|
|
|
|
def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]:
|
|
mime = self.getType(uri)
|
|
if mime is None:
|
|
return None
|
|
return [mime]
|
|
|
|
def call(self, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]:
|
|
return None
|
|
|
|
def release(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def openFile(self, uri: Uri, mode: str) -> Any:
|
|
"""打开文件。对 file:// URI 返回真实文件对象。"""
|
|
path = ContentResolver._uri_to_file_path(uri)
|
|
if path and os.path.exists(path):
|
|
try:
|
|
fmode = "rb" if "r" in mode else "wb"
|
|
return open(path, fmode)
|
|
except OSError:
|
|
return None
|
|
return None
|
|
|
|
def openAssetFile(self, uri: Uri, mode: str) -> Any:
|
|
return self.openFile(uri, mode)
|
|
|
|
def openTypedAssetFile(self, uri: Uri, mime_type: str, opts: Optional[ContentValues]) -> Any:
|
|
return self.openFile(uri, "r")
|
|
|
|
def getLocalContentProvider(self) -> Optional[ContentProvider]:
|
|
return None
|
|
|
|
|
|
class ContentProviderOperation(Stub):
|
|
"""ContentProviderOperation — 批量操作"""
|
|
|
|
TYPE_INSERT = 1
|
|
TYPE_UPDATE = 2
|
|
TYPE_DELETE = 3
|
|
TYPE_ASSERT = 4
|
|
|
|
class Builder(Stub):
|
|
def __init__(self, type_: int, uri: Uri):
|
|
super().__init__()
|
|
self._type = type_
|
|
self._uri = uri
|
|
self._values: Optional[ContentValues] = None
|
|
self._selection: Optional[str] = None
|
|
self._selection_args: Optional[List[str]] = None
|
|
|
|
def withValue(self, key: str, value: Any) -> "ContentProviderOperation.Builder":
|
|
if self._values is None:
|
|
self._values = ContentValues()
|
|
self._values.put(key, value)
|
|
return self
|
|
|
|
def withValues(self, values: ContentValues) -> "ContentProviderOperation.Builder":
|
|
self._values = values
|
|
return self
|
|
|
|
def withSelection(self, selection: str, selection_args: List[str]) -> "ContentProviderOperation.Builder":
|
|
self._selection = selection
|
|
self._selection_args = selection_args
|
|
return self
|
|
|
|
def build(self) -> "ContentProviderOperation":
|
|
op = ContentProviderOperation()
|
|
op._type = self._type
|
|
op._uri = self._uri
|
|
op._values = self._values
|
|
op._selection = self._selection
|
|
op._selection_args = self._selection_args
|
|
return op
|
|
|
|
@staticmethod
|
|
def newInsert(uri: Uri) -> "ContentProviderOperation.Builder":
|
|
return ContentProviderOperation.Builder(ContentProviderOperation.TYPE_INSERT, uri)
|
|
|
|
@staticmethod
|
|
def newUpdate(uri: Uri) -> "ContentProviderOperation.Builder":
|
|
return ContentProviderOperation.Builder(ContentProviderOperation.TYPE_UPDATE, uri)
|
|
|
|
@staticmethod
|
|
def newDelete(uri: Uri) -> "ContentProviderOperation.Builder":
|
|
return ContentProviderOperation.Builder(ContentProviderOperation.TYPE_DELETE, uri)
|
|
|
|
@staticmethod
|
|
def newAssert(uri: Uri) -> "ContentProviderOperation.Builder":
|
|
return ContentProviderOperation.Builder(ContentProviderOperation.TYPE_ASSERT, uri)
|
|
|
|
|
|
class ContentProviderResult(Stub):
|
|
"""ContentProviderResult — 操作结果"""
|
|
|
|
def __init__(self, uri: Optional[Uri] = None, count: Optional[int] = None):
|
|
super().__init__()
|
|
self.uri = uri
|
|
self.count = count
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Loader / CursorLoader / DialogInterface
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class Loader(Stub):
|
|
"""Loader — 异步数据加载器基类"""
|
|
|
|
def __init__(self, context: Context):
|
|
super().__init__()
|
|
self._context = context
|
|
self._started = False
|
|
self._abandoned = False
|
|
self._listeners: List = []
|
|
|
|
def deliverResult(self, data: Any):
|
|
for listener in self._listeners:
|
|
try:
|
|
listener.onLoadFinished(self, data)
|
|
except Exception:
|
|
pass
|
|
|
|
def startLoading(self):
|
|
self._started = True
|
|
|
|
def forceLoad(self):
|
|
self.deliverResult(None)
|
|
|
|
def stopLoading(self):
|
|
self._started = False
|
|
|
|
def abandon(self):
|
|
self._abandoned = True
|
|
|
|
def isStarted(self) -> bool:
|
|
return self._started
|
|
|
|
def isAbandoned(self) -> bool:
|
|
return self._abandoned
|
|
|
|
def reset(self):
|
|
self._started = False
|
|
self._abandoned = False
|
|
|
|
def registerListener(self, listener):
|
|
if listener not in self._listeners:
|
|
self._listeners.append(listener)
|
|
|
|
def unregisterListener(self, listener):
|
|
if listener in self._listeners:
|
|
self._listeners.remove(listener)
|
|
|
|
def dataToString(self, data: Any) -> str:
|
|
return str(data)
|
|
|
|
def toString(self) -> str:
|
|
return f"Loader@{hex(id(self))}"
|
|
|
|
|
|
class CursorLoader(Loader):
|
|
"""CursorLoader — 游标加载器"""
|
|
|
|
def __init__(self, context: Context, uri: Uri, projection: Optional[List[str]],
|
|
selection: Optional[str], selection_args: Optional[List[str]], sort_order: Optional[str]):
|
|
super().__init__(context)
|
|
self._uri = uri
|
|
self._projection = projection
|
|
self._selection = selection
|
|
self._selection_args = selection_args
|
|
self._sort_order = sort_order
|
|
|
|
def getUri(self) -> Uri:
|
|
return self._uri
|
|
|
|
def setUri(self, uri: Uri):
|
|
self._uri = uri
|
|
|
|
def getProjection(self) -> Optional[List[str]]:
|
|
return self._projection
|
|
|
|
def setProjection(self, projection: Optional[List[str]]):
|
|
self._projection = projection
|
|
|
|
def getSelection(self) -> Optional[str]:
|
|
return self._selection
|
|
|
|
def setSelection(self, selection: Optional[str]):
|
|
self._selection = selection
|
|
|
|
def getSelectionArgs(self) -> Optional[List[str]]:
|
|
return self._selection_args
|
|
|
|
def setSelectionArgs(self, args: Optional[List[str]]):
|
|
self._selection_args = args
|
|
|
|
def getSortOrder(self) -> Optional[str]:
|
|
return self._sort_order
|
|
|
|
def setSortOrder(self, sort_order: Optional[str]):
|
|
self._sort_order = sort_order
|
|
|
|
def loadInBackground(self):
|
|
"""在后台加载数据,委托给 ContentResolver.query。"""
|
|
if self._context is not None:
|
|
try:
|
|
resolver = self._context.getContentResolver()
|
|
return resolver.query(self._uri, self._projection, self._selection,
|
|
self._selection_args, self._sort_order)
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
class DialogInterface(Stub):
|
|
"""DialogInterface — 对话框接口常量"""
|
|
|
|
BUTTON_POSITIVE = -1
|
|
BUTTON_NEGATIVE = -2
|
|
BUTTON_NEUTRAL = -3
|
|
|
|
def cancel(self):
|
|
pass
|
|
|
|
def dismiss(self):
|
|
pass
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# SyncAdapter / SyncResult
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class SyncAdapter(Stub):
|
|
"""SyncAdapter — 同步适配器"""
|
|
|
|
def __init__(self, context: Context, auto_initialize: bool, allow_parallel_syncs: bool):
|
|
super().__init__()
|
|
self._context = context
|
|
|
|
def onPerformSync(self, account: Any, extras: ContentValues, authority: str,
|
|
provider: ContentProviderClient, sync_result: "SyncResult"):
|
|
pass
|
|
|
|
def onSecurityException(self, account: Any, extras: ContentValues,
|
|
authority: str, sync_result: "SyncResult"):
|
|
pass
|
|
|
|
|
|
class SyncResult(Stub):
|
|
"""SyncResult — 同步结果"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.stats = self._Stats()
|
|
|
|
class _Stats:
|
|
def __init__(self):
|
|
self.numEntries = 0
|
|
self.numUpdates = 0
|
|
self.numDeletes = 0
|
|
self.numInserts = 0
|
|
self.numSkippedEntries = 0
|
|
|
|
def clear(self):
|
|
self.stats = self._Stats()
|
|
|
|
def hasError(self) -> bool:
|
|
return False
|
|
|
|
def hasSoftError(self) -> bool:
|
|
return False
|
|
|
|
def hasHardError(self) -> bool:
|
|
return False
|
|
|
|
def madeSomeProgress(self) -> bool:
|
|
return self.stats.numEntries > 0
|
|
|
|
def toString(self) -> str:
|
|
return f"SyncResult({self.stats.numEntries} entries)"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ClipboardManager
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ClipboardManager(Stub):
|
|
"""ClipboardManager — 剪贴板管理器"""
|
|
|
|
def getText(self):
|
|
if _USE_LINUX:
|
|
return LinuxClipboard.read()
|
|
return ""
|
|
|
|
def setText(self, text):
|
|
if _USE_LINUX:
|
|
return LinuxClipboard.write(text)
|
|
return False
|
|
|
|
def hasText(self):
|
|
if _USE_LINUX:
|
|
return len(LinuxClipboard.read()) > 0
|
|
return False
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 替换 _core.py 中的 _GlobalContext
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
_real_context = Context()
|
|
# 将 _core 中的 android_context 替换为真实 Context
|
|
import importlib
|
|
try:
|
|
_core_mod = importlib.import_module(".._core", __package__)
|
|
_core_mod.android_context.__class__ = Context
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# __all__
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
__all__ = [
|
|
"Uri",
|
|
"ContentValues",
|
|
"ContentResolver",
|
|
"Context",
|
|
"ContextWrapper",
|
|
"IntentFilter",
|
|
"BroadcastReceiver",
|
|
"SharedPreferences",
|
|
"SharedPreferencesEditor",
|
|
"ComponentName",
|
|
"ClipData",
|
|
"ClipDescription",
|
|
"ContentProvider",
|
|
"ContentProviderClient",
|
|
"ContentProviderOperation",
|
|
"ContentProviderResult",
|
|
"CursorLoader",
|
|
"Loader",
|
|
"DialogInterface",
|
|
"SyncAdapter",
|
|
"SyncResult",
|
|
"ClipboardManager",
|
|
] |