550 lines
19 KiB
Python
550 lines
19 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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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 — 媒体存储"""
|
|
|
|
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"
|
|
|
|
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"
|
|
|
|
class Audio(Stub):
|
|
class Media(Stub):
|
|
EXTERNAL_CONTENT_URI = "content://media/external/audio/media"
|
|
|
|
@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 — 联系人合约"""
|
|
|
|
class Contacts(Stub):
|
|
CONTENT_URI = "content://com.android.contacts/contacts"
|
|
CONTENT_FILTER_URI = "content://com.android.contacts/contacts/filter"
|
|
DISPLAY_NAME = "display_name"
|
|
PHOTO_URI = "photo_uri"
|
|
_ID = "_id"
|
|
|
|
class CommonDataKinds(Stub):
|
|
class Email(Stub):
|
|
CONTENT_URI = "content://com.android.contacts/data/emails"
|
|
|
|
class Phone(Stub):
|
|
CONTENT_URI = "content://com.android.contacts/data/phones"
|
|
|
|
class Data(Stub):
|
|
CONTENT_URI = "content://com.android.contacts/data"
|
|
|
|
|
|
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",
|
|
] |