Files
Android-pyApi/android_pyapi/android/provider.py
T
shuiyu1123 fbb8263b13 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行
2026-06-25 00:40:59 +00:00

688 lines
23 KiB
Python

"""
Android Provider 组件 — Settings, MediaStore, ContactsContract 等
方法签名完全对齐 Android SDK
"""
import os
import sys
import json
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from .._core import (
Stub, AndroidOnlyError, android_context,
resolve_android_path, to_android_path,
_sdcard_root, _app_data_dir, _package_name,
)
# ═══════════════════════════════════════════════════════════════
# OpenableColumns
# ═══════════════════════════════════════════════════════════════
class OpenableColumns(Stub):
DISPLAY_NAME = "_display_name"
SIZE = "_size"
# ═══════════════════════════════════════════════════════════════
# Settings
# ═══════════════════════════════════════════════════════════════
class _SettingsBase(Stub):
"""Settings 基类 — 存储到 JSON 文件"""
_DATA_DIR = os.path.join(_app_data_dir, "shared_prefs")
_cache: Dict[str, Dict[str, Any]] = {}
@classmethod
def _get_path(cls, name: str) -> str:
os.makedirs(cls._DATA_DIR, exist_ok=True)
return os.path.join(cls._DATA_DIR, f"{name}.json")
@classmethod
def _load(cls, name: str) -> Dict[str, Any]:
if name in cls._cache:
return cls._cache[name]
path = cls._get_path(name)
if os.path.exists(path):
try:
with open(path, "r") as f:
data = json.load(f)
cls._cache[name] = data
return data
except (json.JSONDecodeError, OSError):
pass
cls._cache[name] = {}
return cls._cache[name]
@classmethod
def _save(cls, name: str):
path = cls._get_path(name)
try:
with open(path, "w") as f:
json.dump(cls._cache.get(name, {}), f)
except OSError:
pass
@staticmethod
def putString(name: str, key: str, value: str) -> bool:
data = _SettingsBase._load(name)
data[key] = value
_SettingsBase._save(name)
return True
@staticmethod
def getString(name: str, key: str, default_value: Optional[str] = None) -> Optional[str]:
data = _SettingsBase._load(name)
return data.get(key, default_value)
@staticmethod
def putInt(name: str, key: str, value: int) -> bool:
data = _SettingsBase._load(name)
data[key] = value
_SettingsBase._save(name)
return True
@staticmethod
def getInt(name: str, key: str, default_value: int = 0) -> int:
data = _SettingsBase._load(name)
return data.get(key, default_value)
@staticmethod
def putLong(name: str, key: str, value: int) -> bool:
return _SettingsBase.putInt(name, key, value)
@staticmethod
def getLong(name: str, key: str, default_value: int = 0) -> int:
return _SettingsBase.getInt(name, key, default_value)
@staticmethod
def putFloat(name: str, key: str, value: float) -> bool:
data = _SettingsBase._load(name)
data[key] = value
_SettingsBase._save(name)
return True
@staticmethod
def getFloat(name: str, key: str, default_value: float = 0.0) -> float:
data = _SettingsBase._load(name)
return data.get(key, default_value)
@staticmethod
def putBoolean(name: str, key: str, value: bool) -> bool:
data = _SettingsBase._load(name)
data[key] = bool(value)
_SettingsBase._save(name)
return True
@staticmethod
def getBoolean(name: str, key: str, default_value: bool = False) -> bool:
data = _SettingsBase._load(name)
val = data.get(key, default_value)
if isinstance(val, bool):
return val
if isinstance(val, str):
return val.lower() in ("true", "1", "yes")
if isinstance(val, (int, float)):
return bool(val)
return default_value
class SettingsSystem(_SettingsBase):
_NAME = "settings_system"
@staticmethod
def putInt(key: str, value: int) -> bool:
return _SettingsBase.putInt(SettingsSystem._NAME, key, value)
@staticmethod
def getInt(key: str, default_value: int = 0) -> int:
return _SettingsBase.getInt(SettingsSystem._NAME, key, default_value)
@staticmethod
def putString(key: str, value: str) -> bool:
return _SettingsBase.putString(SettingsSystem._NAME, key, value)
@staticmethod
def getString(key: str, default_value: Optional[str] = None) -> Optional[str]:
return _SettingsBase.getString(SettingsSystem._NAME, key, default_value)
@staticmethod
def putFloat(key: str, value: float) -> bool:
return _SettingsBase.putFloat(SettingsSystem._NAME, key, value)
@staticmethod
def getFloat(key: str, default_value: float = 0.0) -> float:
return _SettingsBase.getFloat(SettingsSystem._NAME, key, default_value)
@staticmethod
def putLong(key: str, value: int) -> bool:
return _SettingsBase.putLong(SettingsSystem._NAME, key, value)
@staticmethod
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsSystem._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsSystem._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsSystem._NAME, key, default_value)
class SettingsSecure(_SettingsBase):
_NAME = "settings_secure"
@staticmethod
def putInt(key: str, value: int) -> bool:
return _SettingsBase.putInt(SettingsSecure._NAME, key, value)
@staticmethod
def getInt(key: str, default_value: int = 0) -> int:
return _SettingsBase.getInt(SettingsSecure._NAME, key, default_value)
@staticmethod
def putString(key: str, value: str) -> bool:
return _SettingsBase.putString(SettingsSecure._NAME, key, value)
@staticmethod
def getString(key: str, default_value: Optional[str] = None) -> Optional[str]:
return _SettingsBase.getString(SettingsSecure._NAME, key, default_value)
@staticmethod
def putFloat(key: str, value: float) -> bool:
return _SettingsBase.putFloat(SettingsSecure._NAME, key, value)
@staticmethod
def getFloat(key: str, default_value: float = 0.0) -> float:
return _SettingsBase.getFloat(SettingsSecure._NAME, key, default_value)
@staticmethod
def putLong(key: str, value: int) -> bool:
return _SettingsBase.putLong(SettingsSecure._NAME, key, value)
@staticmethod
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsSecure._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsSecure._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsSecure._NAME, key, default_value)
class SettingsGlobal(_SettingsBase):
_NAME = "settings_global"
@staticmethod
def putInt(key: str, value: int) -> bool:
return _SettingsBase.putInt(SettingsGlobal._NAME, key, value)
@staticmethod
def getInt(key: str, default_value: int = 0) -> int:
return _SettingsBase.getInt(SettingsGlobal._NAME, key, default_value)
@staticmethod
def putString(key: str, value: str) -> bool:
return _SettingsBase.putString(SettingsGlobal._NAME, key, value)
@staticmethod
def getString(key: str, default_value: Optional[str] = None) -> Optional[str]:
return _SettingsBase.getString(SettingsGlobal._NAME, key, default_value)
@staticmethod
def putFloat(key: str, value: float) -> bool:
return _SettingsBase.putFloat(SettingsGlobal._NAME, key, value)
@staticmethod
def getFloat(key: str, default_value: float = 0.0) -> float:
return _SettingsBase.getFloat(SettingsGlobal._NAME, key, default_value)
@staticmethod
def putLong(key: str, value: int) -> bool:
return _SettingsBase.putLong(SettingsGlobal._NAME, key, value)
@staticmethod
def getLong(key: str, default_value: int = 0) -> int:
return _SettingsBase.getLong(SettingsGlobal._NAME, key, default_value)
@staticmethod
def putBoolean(key: str, value: bool) -> bool:
return _SettingsBase.putBoolean(SettingsGlobal._NAME, key, value)
@staticmethod
def getBoolean(key: str, default_value: bool = False) -> bool:
return _SettingsBase.getBoolean(SettingsGlobal._NAME, key, default_value)
class Settings(Stub):
"""Settings — 系统设置"""
System = SettingsSystem
Secure = SettingsSecure
Global = SettingsGlobal
ACTION_SETTINGS = "android.settings.SETTINGS"
ACTION_APPLICATION_DETAILS_SETTINGS = "android.settings.APPLICATION_DETAILS_SETTINGS"
EXTRA_APP_PACKAGE = "android.intent.extra.PACKAGE_NAME"
# ═══════════════════════════════════════════════════════════════
# MediaStore
# ═══════════════════════════════════════════════════════════════
class MediaStore(Stub):
"""MediaStore — 媒体存储"""
VOLUME_INTERNAL = "internal"
VOLUME_EXTERNAL = "external"
VOLUME_EXTERNAL_PRIMARY = "external_primary"
class MediaColumns(Stub):
_ID = 0
DATA = 1
SIZE = 2
DISPLAY_NAME = 3
MIME_TYPE = 4
TITLE = 5
DATE_ADDED = 6
DATE_MODIFIED = 7
ORIENTATION = 8
WIDTH = 9
HEIGHT = 10
class Files(Stub):
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/file"
class Images(Stub):
class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/images/media"
INTERNAL_CONTENT_URI = "content://media/internal/images/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/images/media"
class Thumbnails(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/images/thumbnails"
class Video(Stub):
class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/video/media"
INTERNAL_CONTENT_URI = "content://media/internal/video/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/video/media"
class Audio(Stub):
class Media(Stub):
EXTERNAL_CONTENT_URI = "content://media/external/audio/media"
INTERNAL_CONTENT_URI = "content://media/internal/audio/media"
@staticmethod
def getContentUri(volume_name: str) -> str:
return f"content://media/{volume_name}/audio/media"
@staticmethod
def getMediaUri(volume_name: str) -> str:
"""返回指定卷的媒体 URI。"""
return f"content://media/{volume_name}"
@staticmethod
def getVersion(context: Any, uri: str) -> str:
return "0"
@staticmethod
def setVersion(context: Any, uri: str, version: str) -> bool:
return True
# ═══════════════════════════════════════════════════════════════
# DocumentsContract
# ═══════════════════════════════════════════════════════════════
class DocumentsContract(Stub):
"""DocumentsContract — 文档合约"""
EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI"
EXTRA_LOADING = "android.provider.extra.LOADING"
EXTRA_INFO = "android.provider.extra.INFO"
EXTRA_PROMPT = "android.provider.extra.PROMPT"
@staticmethod
def buildDocumentUri(authority: str, document_id: str) -> str:
return f"content://{authority}/document/{document_id}"
@staticmethod
def buildDocumentUriUsingTree(tree_uri: str, document_id: str) -> str:
return f"{tree_uri}/document/{document_id}"
@staticmethod
def buildChildDocumentsUri(authority: str, parent_document_id: str) -> str:
return f"content://{authority}/document/{parent_document_id}/children"
@staticmethod
def buildChildDocumentsUriUsingTree(tree_uri: str, parent_document_id: str) -> str:
return f"{tree_uri}/document/{parent_document_id}/children"
@staticmethod
def getDocumentId(document_uri: str) -> str:
return document_uri.rsplit("/", 1)[-1]
@staticmethod
def getTreeDocumentId(document_uri: str) -> str:
parts = document_uri.split("/")
if "document" in parts:
idx = parts.index("document")
if idx + 1 < len(parts):
return parts[idx + 1]
return ""
@staticmethod
def getRootId(root_uri: str) -> str:
parts = root_uri.split("/")
if "root" in parts:
idx = parts.index("root")
if idx + 1 < len(parts):
return parts[idx + 1]
return ""
# ═══════════════════════════════════════════════════════════════
# ContactsContract
# ═══════════════════════════════════════════════════════════════
class ContactsContract(Stub):
"""ContactsContract — 联系人合约"""
AUTHORITY = "com.android.contacts"
class Contacts(Stub):
CONTENT_URI = "content://com.android.contacts/contacts"
CONTENT_FILTER_URI = "content://com.android.contacts/contacts/filter"
CONTENT_TYPE = "vnd.android.cursor.dir/contact"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact"
DISPLAY_NAME = "display_name"
PHOTO_URI = "photo_uri"
PHOTO_ID = "photo_id"
_ID = "_id"
HAS_PHONE_NUMBER = "has_phone_number"
LOOKUP_KEY = "lookup"
STARRED = "starred"
class CommonDataKinds(Stub):
class Email(Stub):
CONTENT_URI = "content://com.android.contacts/data/emails"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/email_v2"
ADDRESS = "data1"
DISPLAY_NAME = "display_name"
TYPE = "data2"
TYPE_HOME = 1
TYPE_WORK = 2
TYPE_OTHER = 3
TYPE_MOBILE = 4
class Phone(Stub):
CONTENT_URI = "content://com.android.contacts/data/phones"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone_v2"
NUMBER = "data1"
DISPLAY_NAME = "display_name"
TYPE = "data2"
TYPE_HOME = 1
TYPE_MOBILE = 2
TYPE_WORK = 3
TYPE_FAX_WORK = 4
TYPE_FAX_HOME = 5
TYPE_PAGER = 6
TYPE_OTHER = 7
TYPE_CALLBACK = 8
TYPE_CAR = 9
TYPE_COMPANY_MAIN = 10
TYPE_ISDN = 11
TYPE_MAIN = 12
TYPE_OTHER_FAX = 13
TYPE_RADIO = 14
TYPE_TELEX = 15
TYPE_TTY_TDD = 16
TYPE_WORK_MOBILE = 17
TYPE_WORK_PAGER = 18
TYPE_ASSISTANT = 19
class StructuredName(Stub):
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/name"
DISPLAY_NAME = "data1"
GIVEN_NAME = "data2"
FAMILY_NAME = "data3"
PREFIX = "data4"
MIDDLE_NAME = "data5"
SUFFIX = "data6"
class Organization(Stub):
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/organization"
COMPANY = "data1"
TYPE = "data2"
TITLE = "data4"
TYPE_WORK = 1
TYPE_OTHER = 2
class Data(Stub):
CONTENT_URI = "content://com.android.contacts/data"
CONTENT_TYPE = "vnd.android.cursor.dir/data"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/data"
MIMETYPE = "mimetype"
_ID = "_id"
DISPLAY_NAME = "display_name"
RAW_CONTACT_ID = "raw_contact_id"
CONTACT_ID = "contact_id"
class RawContacts(Stub):
CONTENT_URI = "content://com.android.contacts/raw_contacts"
CONTENT_TYPE = "vnd.android.cursor.dir/raw_contact"
CONTENT_ITEM_TYPE = "vnd.android.cursor.item/raw_contact"
_ID = "_id"
CONTACT_ID = "contact_id"
ACCOUNT_NAME = "account_name"
ACCOUNT_TYPE = "account_type"
class ContactsContractContacts(ContactsContract.Contacts):
pass
class ContactsContractData(ContactsContract.Data):
pass
# ═══════════════════════════════════════════════════════════════
# CallLog
# ═══════════════════════════════════════════════════════════════
class CallLog(Stub):
"""CallLog — 通话记录"""
class Calls(Stub):
CONTENT_URI = "content://call_log/calls"
_ID = "_id"
NUMBER = "number"
TYPE = "type"
TYPE_INCOMING = 1
TYPE_OUTGOING = 2
TYPE_MISSED = 3
TYPE_VOICEMAIL = 4
TYPE_REJECTED = 5
TYPE_BLOCKED = 6
TYPE_ANSWERED_EXTERNALLY = 7
DATE = "date"
DURATION = "duration"
NEW = "new"
VOICEMAIL_URI = "voicemail_uri"
CACHED_NAME = "cached_name"
CACHED_NUMBER_LABEL = "cached_number_label"
GEOCODED_LOCATION = "geocoded_location"
COUNTRY_ISO = "country_iso"
@staticmethod
def getLastOutgoingCall(context: Any) -> Optional[str]:
return None
# ═══════════════════════════════════════════════════════════════
# CalendarContract
# ═══════════════════════════════════════════════════════════════
class CalendarContract(Stub):
"""CalendarContract — 日历合约"""
class Events(Stub):
CONTENT_URI = "content://com.android.calendar/events"
_ID = "_id"
TITLE = "title"
DESCRIPTION = "description"
EVENT_LOCATION = "event_location"
DTSTART = "dtstart"
DTEND = "dtend"
EVENT_TIMEZONE = "event_timezone"
DURATION = "duration"
ALL_DAY = "allDay"
CALENDAR_ID = "calendar_id"
ORGANIZER = "organizer"
STATUS = "eventStatus"
AVAILABILITY = "availability"
ACCESS_LEVEL = "accessLevel"
class Attendees(Stub):
CONTENT_URI = "content://com.android.calendar/attendees"
class Reminders(Stub):
CONTENT_URI = "content://com.android.calendar/reminders"
# ═══════════════════════════════════════════════════════════════
# Browser / SearchRecentSuggestions / UserDictionary
# ═══════════════════════════════════════════════════════════════
class Browser(Stub):
"""Browser — 浏览器书签"""
class BookmarkColumns(Stub):
TITLE = "title"
URL = "url"
_ID = "_id"
BOOKMARK = "bookmark"
DATE = "date"
CREATED = "created"
VISITS = "visits"
class SearchColumns(Stub):
URL = "url"
SEARCH = "search"
DATE = "date"
HISTORY_PROJECTION = ["_id", "url", "title", "date", "visits", "bookmark", "created"]
SEARCHES_PROJECTION = ["_id", "search", "date"]
class SearchRecentSuggestions(Stub):
"""SearchRecentSuggestions — 搜索建议"""
def __init__(self, context: Any, authority: str, mode: int):
super().__init__()
self._authority = authority
self._mode = mode
def saveRecentQuery(self, query_string: str, line2: str):
pass
def clearHistory(self):
pass
class UserDictionary(Stub):
"""UserDictionary — 用户词典"""
class Words(Stub):
CONTENT_URI = "content://user_dictionary/words"
_ID = "_id"
WORD = "word"
FREQUENCY = "frequency"
LOCALE = "locale"
APP_ID = "app_id"
SHORTCUT = "shortcut"
# ═══════════════════════════════════════════════════════════════
# SyncStateContract / LiveFolders
# ═══════════════════════════════════════════════════════════════
class SyncStateContract(Stub):
"""SyncStateContract — 同步状态合约"""
class Columns(Stub):
_ID = "_id"
ACCOUNT_NAME = "account_name"
ACCOUNT_TYPE = "account_type"
DATA = "data"
class Helpers(Stub):
@staticmethod
def get(cr: Any, provider_uri: str, account: Any) -> Optional[bytes]:
return None
@staticmethod
def set(cr: Any, provider_uri: str, account: Any, data: bytes):
pass
@staticmethod
def insert(cr: Any, provider_uri: str, account: Any, data: bytes):
pass
@staticmethod
def update(cr: Any, provider_uri: str, account: Any, data: bytes):
pass
class LiveFolders(Stub):
"""LiveFolders — 实时文件夹"""
NAME = "name"
DESCRIPTION = "description"
INTENT = "intent"
ICON_BITMAP = "icon_bitmap"
ICON_PACKAGE = "icon_package"
ICON_RESOURCE = "icon_resource"
DISPLAY_MODE = "display_mode"
DISPLAY_MODE_GRID = 1
DISPLAY_MODE_LIST = 2
# ═══════════════════════════════════════════════════════════════
# __all__
# ═══════════════════════════════════════════════════════════════
__all__ = [
"Settings",
"SettingsSystem",
"SettingsSecure",
"SettingsGlobal",
"CallLog",
"ContactsContract",
"ContactsContractContacts",
"ContactsContractData",
"MediaStore",
"DocumentsContract",
"CalendarContract",
"Browser",
"SearchRecentSuggestions",
"UserDictionary",
"SyncStateContract",
"LiveFolders",
"OpenableColumns",
]