Files
Android-pyApi/android_pyapi/android/provider_contacts.py
T
shuiyu1123 a51854455b feat: 大规模扩充至212个模块/175K行代码 — 覆盖android/java/javax/org/dalvik全包
- android/ 167个模块 (app/content/os/view/widget/graphics/media/hardware/net/telephony/provider等)
- java/ 35个模块 (lang/io/util/net/nio/security/crypto/sql/beans/rmi/awt等)
- javax/ 4个模块 (net/crypto/security/sql)
- org/ 5个模块 (json/w3c_dom/xml_sax/xml_pull/apache_http)
- dalvik/ 1个模块 (system)
- ~3053个类/接口,方法签名完全对齐Android SDK
- 桌面跨平台运行 (Windows/macOS/Linux)
- 修复所有导入错误和Python关键字冲突
2026-06-24 14:15:56 +00:00

776 lines
26 KiB
Python

"""
Android Provider ContactsContract 扩展组件
方法签名完全对齐 Android SDK
"""
from __future__ import annotations
import os
import json
import time
import threading
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError
from ..java.lang import RuntimeException
# ═══════════════════════════════════════════════════════════════
# 联系人常量
# ═══════════════════════════════════════════════════════════════
TIMES_CONTACTED = "times_contacted"
LAST_TIME_CONTACTED = "last_time_contacted"
STARRED = "starred"
CUSTOM_RINGTONE = "custom_ringtone"
SEND_TO_VOICEMAIL = "send_to_voicemail"
CONTACT_PRESENCE = "contact_presence"
CONTACT_CHAT_CAPABILITY = "contact_chat_capability"
CONTACT_STATUS = "contact_status"
CONTACT_STATUS_TIMESTAMP = "contact_status_ts"
CONTACT_STATUS_RES_PACKAGE = "contact_status_res_package"
CONTACT_STATUS_LABEL = "contact_status_label"
CONTACT_STATUS_ICON = "contact_status_icon"
CONTACT_ID = "_id"
CONTACT_LOOKUP_KEY = "lookup"
CONTACT_DISPLAY_NAME = "display_name"
CONTACT_PHOTO_ID = "photo_id"
CONTACT_PHOTO_URI = "photo_uri"
CONTACT_PHOTO_THUMBNAIL_URI = "photo_thumb_uri"
CONTACT_LAST_TIME_CONTACTED = LAST_TIME_CONTACTED
CONTACT_TIMES_CONTACTED = TIMES_CONTACTED
CONTACT_STARRED = STARRED
CONTACT_HAS_PHONE_NUMBER = "has_phone_number"
CONTACT_IS_USER_PROFILE = "is_user_profile"
CONTACT_IS_PRIVATE = "is_private"
CONTACT_NAME_RAW_CONTACT_ID = "name_raw_contact_id"
CONTACT_DISPLAY_NAME_SOURCE = "display_name_source"
CONTACT_LOOKUP_KEY = CONTACT_LOOKUP_KEY
CONTACT_SORT_KEY_PRIMARY = "sort_key"
CONTACT_SORT_KEY_ALTERNATIVE = "sort_key_alt"
LOOKUP_KEY = CONTACT_LOOKUP_KEY
LOOKUP_ID = "_id"
DATA_ID = "_id"
DATA_CONTACT_ID = "contact_id"
DATA_RAW_CONTACT_ID = "raw_contact_id"
DATA_MIMETYPE = "mimetype"
DATA_IS_PRIMARY = "is_primary"
DATA_IS_SUPER_PRIMARY = "is_super_primary"
DATA_DATA_VERSION = "data_version"
DATA_DELETED = "deleted"
DATA_SYNC1 = "data_sync1"
DATA_SYNC2 = "data_sync2"
DATA_SYNC3 = "data_sync3"
DATA_SYNC4 = "data_sync4"
DATA1 = "data1"
DATA2 = "data2"
DATA3 = "data3"
DATA4 = "data4"
DATA5 = "data5"
DATA6 = "data6"
DATA7 = "data7"
DATA8 = "data8"
DATA9 = "data9"
DATA10 = "data10"
DATA11 = "data11"
DATA12 = "data12"
DATA13 = "data13"
DATA14 = "data14"
DATA15 = "data15"
RAW_CONTACT_ID = "_id"
RAW_CONTACT_CONTACT_ID = "contact_id"
RAW_CONTACT_ACCOUNT_NAME = "account_name"
RAW_CONTACT_ACCOUNT_TYPE = "account_type"
RAW_CONTACT_SOURCE_ID = "sourceid"
RAW_CONTACT_VERSION = "version"
RAW_CONTACT_DELETED = "deleted"
RAW_CONTACT_AGGREGATION_MODE = "aggregation_mode"
RAW_CONTACT_AGGREGATION_NEEDED = 0
RAW_CONTACT_AGGREGATION_SUSPENDED = 1
RAW_CONTACT_AGGREGATION_DEFAULT = 2
RAW_CONTACT_SYNC1 = "sync1"
RAW_CONTACT_SYNC2 = "sync2"
RAW_CONTACT_SYNC3 = "sync3"
RAW_CONTACT_SYNC4 = "sync4"
RAW_CONTACT_METADATA = "metadata"
# MIME 类型
MIMETYPE_EMAIL = "vnd.android.cursor.item/email_v2"
MIMETYPE_PHONE = "vnd.android.cursor.item/phone_v2"
MIMETYPE_NAME = "vnd.android.cursor.item/name"
MIMETYPE_POSTAL = "vnd.android.cursor.item/postal-address_v2"
MIMETYPE_IM = "vnd.android.cursor.item/im"
MIMETYPE_NOTE = "vnd.android.cursor.item/note"
MIMETYPE_ORGANIZATION = "vnd.android.cursor.item/organization"
MIMETYPE_PHOTO = "vnd.android.cursor.item/photo"
MIMETYPE_WEBSITE = "vnd.android.cursor.item/website"
MIMETYPE_SIP_ADDRESS = "vnd.android.cursor.item/sip_address"
MIMETYPE_NICKNAME = "vnd.android.cursor.item/nickname"
MIMETYPE_GROUP_MEMBERSHIP = "vnd.android.cursor.item/group_membership"
MIMETYPE_EVENT = "vnd.android.cursor.item/contact_event"
MIMETYPE_RELATION = "vnd.android.cursor.item/relation"
FULL_NAME_STYLE_UNDEFINED = 0
FULL_NAME_STYLE_WESTERN = 1
FULL_NAME_STYLE_EASTERN = 2
FULL_NAME_STYLE_CHINESE = 3
FULL_NAME_STYLE_JAPANESE = 4
FULL_NAME_STYLE_KOREAN = 5
PHONETIC_NAME_STYLE_UNDEFINED = 0
PHONETIC_NAME_STYLE_JAPANESE = 1
PHONETIC_NAME_STYLE_KOREAN = 2
PHONETIC_NAME_STYLE_PINYIN = 3
PHONETIC_NAME_STYLE_ROMANJI = 4
# ═══════════════════════════════════════════════════════════════
# ContactsContract 命名空间容器
# ═══════════════════════════════════════════════════════════════
class ContactsContractBase(Stub):
"""ContactsContract — 联系人合同基类"""
pass
class SyncColumns(Stub):
"""ContactsContract.SyncColumns — 同步列"""
ACCOUNT_NAME = RAW_CONTACT_ACCOUNT_NAME
ACCOUNT_TYPE = RAW_CONTACT_ACCOUNT_TYPE
SOURCE_ID = RAW_CONTACT_SOURCE_ID
VERSION = RAW_CONTACT_VERSION
DIRTY = "dirty"
SYNC1 = RAW_CONTACT_SYNC1
SYNC2 = RAW_CONTACT_SYNC2
SYNC3 = RAW_CONTACT_SYNC3
SYNC4 = RAW_CONTACT_SYNC4
class PresenceColumns(Stub):
"""ContactsContract.PresenceColumns — 在线状态列"""
DATA_ID = "presence_data_id"
PROTOCOL = "protocol"
CUSTOM_PROTOCOL = "custom_protocol"
IM_HANDLE = "im_handle"
IM_ACCOUNT = "im_account"
STATUS = "mode"
STATUS_LABEL = "status_label"
STATUS_RES_PACKAGE = "status_res_package"
STATUS_ICON = "status_icon"
STATUS_TIMESTAMP = "status_ts"
AVAILABLE = 5
AWAY = 2
BUSY = 4
DO_NOT_DISTURB = 4
IDLE = 3
INVISIBLE = 1
OFFLINE = 0
class Presence(PresenceColumns):
"""ContactsContract.Presence — 在线状态"""
pass
class FullNameStyle(Stub):
"""ContactsContract.FullNameStyle — 全名风格"""
UNDEFINED = FULL_NAME_STYLE_UNDEFINED
WESTERN = FULL_NAME_STYLE_WESTERN
EASTERN = FULL_NAME_STYLE_EASTERN
CHINESE = FULL_NAME_STYLE_CHINESE
JAPANESE = FULL_NAME_STYLE_JAPANESE
KOREAN = FULL_NAME_STYLE_KOREAN
class PhoneticNameStyle(Stub):
"""ContactsContract.PhoneticNameStyle — 拼音名称风格"""
UNDEFINED = PHONETIC_NAME_STYLE_UNDEFINED
JAPANESE = PHONETIC_NAME_STYLE_JAPANESE
KOREAN = PHONETIC_NAME_STYLE_KOREAN
PINYIN = PHONETIC_NAME_STYLE_PINYIN
# ═══════════════════════════════════════════════════════════════
# ContactsContract.Contacts
# ═══════════════════════════════════════════════════════════════
class ContactData(Stub):
"""内部联系人数据存储"""
def __init__(self):
self.id: int = 0
self.lookup_key: str = ""
self.display_name: str = ""
self.photo_id: int = 0
self.photo_uri: Optional[str] = None
self.photo_thumb_uri: Optional[str] = None
self.times_contacted: int = 0
self.last_time_contacted: int = 0
self.starred: bool = False
self.custom_ringtone: Optional[str] = None
self.send_to_voicemail: bool = False
self.has_phone_number: bool = False
self.is_user_profile: bool = False
self.name_raw_contact_id: int = 0
self.display_name_source: int = 0
self.sort_key: str = ""
self.sort_key_alt: str = ""
class AggregationSuggestions(Stub):
"""ContactsContract.Contacts.AggregationSuggestions — 聚合建议"""
class Builder(Stub):
"""聚合建议构建器"""
def __init__(self):
super().__init__()
self._contact_id: int = 0
self._limit: int = 0
self._name: Optional[str] = None
def setContactId(self, contact_id: int) -> "AggregationSuggestions.Builder":
self._contact_id = contact_id
return self
def setLimit(self, limit: int) -> "AggregationSuggestions.Builder":
self._limit = limit
return self
def addNameParameter(self, name: str) -> "AggregationSuggestions.Builder":
self._name = name
return self
def build(self) -> str:
return f"suggestions?contact_id={self._contact_id}&limit={self._limit}"
CONTENT_DIRECTORY = "suggestions"
PARAMETER_MATCH_NAME = "name"
class Photo(Stub):
"""ContactsContract.Contacts.Photo — 联系人照片"""
CONTENT_DIRECTORY = "photo"
PHOTO = "data15"
PHOTO_FILE_ID = "data14"
DISPLAY_PHOTO = "display_photo"
class Contacts(Stub):
"""ContactsContract.Contacts — 联系人表"""
CONTENT_URI = "content://com.android.contacts/contacts"
CONTENT_LOOKUP_URI = "content://com.android.contacts/contacts/lookup"
CONTENT_FILTER_URI = "content://com.android.contacts/contacts/filter"
CONTENT_STREQUENT_URI = "content://com.android.contacts/contacts/strequent"
CONTENT_FREQUENT_URI = "content://com.android.contacts/contacts/frequent"
CONTENT_STREQUENT_EMAIL_URI = "content://com.android.contacts/contacts/strequent/email"
CONTENT_GROUP_URI = "content://com.android.contacts/contacts/group"
AggregationSuggestions = AggregationSuggestions
Photo = Photo
# 列
_ID = "_id"
LOOKUP_KEY = "lookup"
DISPLAY_NAME = "display_name"
PHOTO_ID = "photo_id"
PHOTO_URI = "photo_uri"
PHOTO_THUMBNAIL_URI = "photo_thumb_uri"
TIMES_CONTACTED = TIMES_CONTACTED
LAST_TIME_CONTACTED = LAST_TIME_CONTACTED
STARRED = STARRED
CUSTOM_RINGTONE = CUSTOM_RINGTONE
SEND_TO_VOICEMAIL = SEND_TO_VOICEMAIL
HAS_PHONE_NUMBER = "has_phone_number"
IS_USER_PROFILE = "is_user_profile"
NAME_RAW_CONTACT_ID = "name_raw_contact_id"
DISPLAY_NAME_SOURCE = "display_name_source"
SORT_KEY_PRIMARY = "sort_key"
SORT_KEY_ALTERNATIVE = "sort_key_alt"
CONTACT_LAST_UPDATED_TIMESTAMP = "contact_last_updated_timestamp"
CONTACT_PRESENCE = CONTACT_PRESENCE
CONTACT_CHAT_CAPABILITY = CONTACT_CHAT_CAPABILITY
CONTACT_STATUS = CONTACT_STATUS
CONTACT_STATUS_TIMESTAMP = CONTACT_STATUS_TIMESTAMP
@staticmethod
def getLookupUri(contact_id: int, lookup_key: str) -> str:
return f"{Contacts.CONTENT_LOOKUP_URI}/{lookup_key}/{contact_id}"
@staticmethod
def lookupContact(content_resolver: Any, lookup_uri: str) -> Optional[Dict[str, Any]]:
return None
# ═══════════════════════════════════════════════════════════════
# ContactsContract.Data
# ═══════════════════════════════════════════════════════════════
class Data(Stub):
"""ContactsContract.Data — 数据表"""
CONTENT_URI = "content://com.android.contacts/data"
_ID = DATA_ID
CONTACT_ID = DATA_CONTACT_ID
RAW_CONTACT_ID = DATA_RAW_CONTACT_ID
MIMETYPE = DATA_MIMETYPE
IS_PRIMARY = DATA_IS_PRIMARY
IS_SUPER_PRIMARY = DATA_IS_SUPER_PRIMARY
DATA_VERSION = DATA_DATA_VERSION
DELETED = DATA_DELETED
SYNC1 = DATA_SYNC1
SYNC2 = DATA_SYNC2
SYNC3 = DATA_SYNC3
SYNC4 = DATA_SYNC4
DATA1 = DATA1
DATA2 = DATA2
DATA3 = DATA3
DATA4 = DATA4
DATA5 = DATA5
DATA6 = DATA6
DATA7 = DATA7
DATA8 = DATA8
DATA9 = DATA9
DATA10 = DATA10
DATA11 = DATA11
DATA12 = DATA12
DATA13 = DATA13
DATA14 = DATA14
DATA15 = DATA15
# 偏好常量
CONTACT_PREFERRED_ORDER = "contact_preferred_order"
CONTACT_PREFERRED_PHONE = "contact_preferred_phone"
CONTACT_PREFERRED_EMAIL = "contact_preferred_email"
CONTACT_PREFERRED_PHONE_ORDER = "contact_preferred_phone_order"
CONTACT_PREFERRED_EMAIL_ORDER = "contact_preferred_email_order"
# 可见性
VISIBILITY_VISIBLE = 0
VISIBILITY_HIDDEN = 1
VISIBILITY_DEFAULT = 0
VISIBILITY = "visibility"
@staticmethod
def getContactPreferred(data_kind: str, data_set: List[Dict]) -> Optional[Dict]:
if not data_set:
return None
primaries = [d for d in data_set if d.get("is_super_primary") or d.get("is_primary")]
if primaries:
return primaries[0]
return data_set[0]
# ═══════════════════════════════════════════════════════════════
# ContactsContract.CommonDataKinds
# ═══════════════════════════════════════════════════════════════
class CommonDataKinds(Stub):
"""ContactsContract.CommonDataKinds — 通用数据类型"""
class Email(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_EMAIL
TYPE_HOME = 1
TYPE_WORK = 2
TYPE_OTHER = 3
TYPE_MOBILE = 4
ADDRESS = DATA1
DISPLAY_NAME = DATA4
TYPE = DATA2
LABEL = DATA3
class Phone(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_PHONE
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
TYPE_MMS = 20
NUMBER = DATA1
NUMBER_KEY = "data4"
TYPE = DATA2
LABEL = DATA3
class StructuredName(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_NAME
DISPLAY_NAME = DATA1
GIVEN_NAME = DATA2
FAMILY_NAME = DATA3
PREFIX = DATA4
MIDDLE_NAME = DATA5
SUFFIX = DATA6
PHONETIC_GIVEN_NAME = DATA7
PHONETIC_FAMILY_NAME = DATA8
PHONETIC_MIDDLE_NAME = DATA9
FULL_NAME_STYLE = DATA10
PHONETIC_NAME_STYLE = DATA11
class StructuredPostal(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_POSTAL
FORMATTED_ADDRESS = DATA1
STREET = DATA4
POBOX = DATA5
NEIGHBORHOOD = DATA6
CITY = DATA7
REGION = DATA8
POSTCODE = DATA9
COUNTRY = DATA10
TYPE = DATA2
LABEL = DATA3
TYPE_HOME = 1
TYPE_WORK = 2
TYPE_OTHER = 3
class Im(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_IM
PROTOCOL = DATA5
CUSTOM_PROTOCOL = DATA6
DATA = DATA1
TYPE = DATA2
PROTOCOL_AIM = 0
PROTOCOL_MSN = 1
PROTOCOL_YAHOO = 2
PROTOCOL_SKYPE = 3
PROTOCOL_QQ = 4
PROTOCOL_GTALK = 5
PROTOCOL_ICQ = 6
PROTOCOL_JABBER = 7
PROTOCOL_CUSTOM = -1
class Organization(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_ORGANIZATION
COMPANY = DATA1
TYPE = DATA2
LABEL = DATA3
TITLE = DATA4
DEPARTMENT = DATA5
JOB_DESCRIPTION = DATA6
SYMBOL = DATA7
PHONETIC_NAME = DATA8
OFFICE_LOCATION = DATA9
PHONETIC_NAME_STYLE = DATA10
TYPE_WORK = 1
TYPE_OTHER = 2
class Photo(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_PHOTO
PHOTO = DATA15
PHOTO_FILE_ID = DATA14
class Note(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_NOTE
NOTE = DATA1
class Website(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_WEBSITE
URL = DATA1
TYPE = DATA2
TYPE_HOMEPAGE = 1
TYPE_BLOG = 2
TYPE_PROFILE = 3
TYPE_HOME = 4
TYPE_WORK = 5
TYPE_FTP = 6
TYPE_OTHER = 7
class Event(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_EVENT
START_DATE = DATA1
TYPE = DATA2
TYPE_ANNIVERSARY = 1
TYPE_OTHER = 2
TYPE_BIRTHDAY = 3
class Relation(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_RELATION
NAME = DATA1
TYPE = DATA2
TYPE_ASSISTANT = 1
TYPE_BROTHER = 2
TYPE_CHILD = 3
TYPE_DOMESTIC_PARTNER = 4
TYPE_FATHER = 5
TYPE_FRIEND = 6
TYPE_MANAGER = 7
TYPE_MOTHER = 8
TYPE_PARENT = 9
TYPE_PARTNER = 10
TYPE_REFERRED_BY = 11
TYPE_RELATIVE = 12
TYPE_SISTER = 13
TYPE_SPOUSE = 14
class Nickname(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_NICKNAME
NAME = DATA1
TYPE = DATA2
TYPE_DEFAULT = 1
TYPE_OTHER_NAME = 2
TYPE_MAIDEN_NAME = 3
TYPE_SHORT_NAME = 4
TYPE_INITIALS = 5
class SipAddress(Stub):
CONTENT_ITEM_TYPE = MIMETYPE_SIP_ADDRESS
SIP_ADDRESS = DATA1
TYPE = DATA2
TYPE_HOME = 1
TYPE_WORK = 2
# ═══════════════════════════════════════════════════════════════
# ContactsContract.RawContacts
# ═══════════════════════════════════════════════════════════════
class RawContacts(Stub):
"""ContactsContract.RawContacts — 原始联系人"""
CONTENT_URI = "content://com.android.contacts/raw_contacts"
_ID = RAW_CONTACT_ID
CONTACT_ID = RAW_CONTACT_CONTACT_ID
ACCOUNT_NAME = RAW_CONTACT_ACCOUNT_NAME
ACCOUNT_TYPE = RAW_CONTACT_ACCOUNT_TYPE
SOURCE_ID = RAW_CONTACT_SOURCE_ID
VERSION = RAW_CONTACT_VERSION
DELETED = RAW_CONTACT_DELETED
AGGREGATION_MODE = RAW_CONTACT_AGGREGATION_MODE
SYNC1 = RAW_CONTACT_SYNC1
SYNC2 = RAW_CONTACT_SYNC2
SYNC3 = RAW_CONTACT_SYNC3
SYNC4 = RAW_CONTACT_SYNC4
METADATA = RAW_CONTACT_METADATA
RAW_CONTACT_IS_USER_PROFILE = "raw_contact_is_user_profile"
RAW_CONTACT_IS_PRIVATE = "raw_contact_is_private"
DISPLAY_NAME_SOURCE = "display_name_source"
NAME_VERIFIED = "name_verified"
GEOLOCATION = "geolocation"
AGGREGATION_MODE_DEFAULT = 0
AGGREGATION_MODE_IMMEDIATE = 1
AGGREGATION_MODE_SUSPENDED = 2
AGGREGATION_MODE_DISABLED = 3
@staticmethod
def getContactId(raw_contact_id: int) -> int:
return 0
@staticmethod
def getLookupUri(raw_contact_id: int, lookup_key: str) -> str:
return f"content://com.android.contacts/raw_contacts/{raw_contact_id}"
# ═══════════════════════════════════════════════════════════════
# ContactsContract.StatusUpdates / Profile / Settings
# ═══════════════════════════════════════════════════════════════
class StatusUpdates(PresenceColumns):
"""ContactsContract.StatusUpdates — 状态更新"""
CONTENT_URI = "content://com.android.contacts/status_updates"
class Profile(Stub):
"""ContactsContract.Profile — 用户个人资料"""
CONTENT_URI = "content://com.android.contacts/profile"
CONTENT_VCARD_URI = "content://com.android.contacts/profile/vcard"
CONTENT_RAW_CONTACTS_URI = "content://com.android.contacts/profile/raw_contacts"
MIN_ID = 1000000000
MAX_ID = 2000000000
class Settings(Stub):
"""ContactsContract.Settings — 联系人设置"""
CONTENT_URI = "content://com.android.contacts/settings"
ACCOUNT_NAME = RAW_CONTACT_ACCOUNT_NAME
ACCOUNT_TYPE = RAW_CONTACT_ACCOUNT_TYPE
SHOULD_SYNC = "should_sync"
UNGROUPED_VISIBLE = "ungrouped_visible"
ANY_UNGROUPED = "any_ungrouped"
UNGROUPED_COUNT = "ungrouped_count"
UNGROUPED_WITH_PHONES = "ungrouped_with_phones"
# ═══════════════════════════════════════════════════════════════
# ContactsContract.PhoneLookup / DisplayPhoto / DataUsageFeedback
# ═══════════════════════════════════════════════════════════════
class PhoneLookup(Stub):
"""ContactsContract.PhoneLookup — 电话查找"""
CONTENT_URI = "content://com.android.contacts/phone_lookup"
CONTENT_FILTER_URI = "content://com.android.contacts/phone_lookup/filter"
QUERY_PARAMETER_SIP_ADDRESS = "sip"
NUMBER = "number"
TYPE = "type"
LABEL = "label"
NORMALIZED_NUMBER = "normalized_number"
CONTACT_ID = "contact_id"
LOOKUP_KEY = "lookup"
DISPLAY_NAME = "display_name"
PHOTO_ID = "photo_id"
PHOTO_URI = "photo_uri"
PHOTO_THUMBNAIL_URI = "photo_thumb_uri"
class DisplayPhoto(Stub):
"""ContactsContract.DisplayPhoto — 显示照片"""
CONTENT_URI = "content://com.android.contacts/display_photo"
DISPLAY_MAX_DIM = "display_max_dim"
THUMBNAIL_MAX_DIM = "thumbnail_max_dim"
CONTENT_DIRECTORY = "display_photo"
class DataUsageFeedback(Stub):
"""ContactsContract.DataUsageFeedback — 数据使用反馈"""
FEEDBACK_URI = "content://com.android.contacts/data/usagefeedback"
USAGE_TYPE = "type"
USAGE_TYPE_CALL = "call"
USAGE_TYPE_TEXT = "text"
USAGE_TYPE_EMAIL = "email"
USAGE_TYPE_LONG_TEXT = "long_text"
USAGE_TYPE_VIDEO_CALL = "video_call"
class QuickContact(Stub):
"""ContactsContract.QuickContact — 快速联系人"""
ACTION_QUICK_CONTACT = "android.provider.ContactsContract.QuickContact"
MODE_SMALL = 0
MODE_MEDIUM = 1
MODE_LARGE = 2
EXTRA_EXCLUDE_MIMES = "android.provider.extra.EXCLUDE_MIMES"
EXTRA_MODE = "android.provider.extra.MODE"
EXTRA_PRIORITIZED_MIMETYPE = "android.provider.extra.PRIORITIZED_MIMETYPE"
# ═══════════════════════════════════════════════════════════════
# ContactsContract 控制器 — 模拟 ContentProvider 的数据存储
# ═══════════════════════════════════════════════════════════════
class ContactsContract(ContactsContractBase):
"""ContactsContract — 联系人合同"""
Contacts = Contacts
Data = Data
CommonDataKinds = CommonDataKinds
RawContacts = RawContacts
StatusUpdates = StatusUpdates
Profile = Profile
Settings = Settings
SyncColumns = SyncColumns
Presence = Presence
PhoneLookup = PhoneLookup
DisplayPhoto = DisplayPhoto
FullNameStyle = FullNameStyle
PhoneticNameStyle = PhoneticNameStyle
QuickContact = QuickContact
DataUsageFeedback = DataUsageFeedback
AUTHORITY = "com.android.contacts"
CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"
DEFERRED_SNIPPETING = "deferred_snippeting"
DEFERRED_SNIPPETING_QUERY = "deferred_snippeting_query"
DIRECTORY_CONTENT_URI = "content://com.android.contacts/directories"
LIMIT_PARAM_KEY = "limit"
PRIMARY_ACCOUNT_NAME = "name_forPrimaryAccount"
PRIMARY_ACCOUNT_TYPE = "type_forPrimaryAccount"
PROVIDER_INTERFACE = "android.content.ContactsContract"
REMOVE_DUPLICATE_ENTRIES = "remove_duplicate_entries"
STREQUENT_PHONE_ONLY = "strequent_phone_only"
_store: Dict[str, List[Dict[str, Any]]] = {
"contacts": [],
"raw_contacts": [],
"data": [],
}
_lock = threading.Lock()
@classmethod
def addContact(cls, display_name: str = "",
phone: str = "", email: str = "") -> int:
with cls._lock:
contact_id = len(cls._store["contacts"]) + 1
contact = {
"_id": contact_id,
"display_name": display_name,
"has_phone_number": bool(phone),
"times_contacted": 0,
"last_time_contacted": 0,
"starred": False,
}
cls._store["contacts"].append(contact)
raw_id = len(cls._store["raw_contacts"]) + 1
raw = {
"_id": raw_id,
"contact_id": contact_id,
"aggregation_mode": 0,
"deleted": False,
"account_name": "",
"account_type": "",
}
cls._store["raw_contacts"].append(raw)
if phone:
cls._store["data"].append({
"_id": len(cls._store["data"]) + 1,
"raw_contact_id": raw_id,
"mimetype": MIMETYPE_PHONE,
"data1": phone,
"data2": 2,
"is_primary": True,
})
if email:
cls._store["data"].append({
"_id": len(cls._store["data"]) + 1,
"raw_contact_id": raw_id,
"mimetype": MIMETYPE_EMAIL,
"data1": email,
"data2": 1,
"is_primary": True,
})
return contact_id
@classmethod
def query(cls, table: str, filters: Optional[Dict] = None) -> List[Dict[str, Any]]:
with cls._lock:
results = list(cls._store.get(table, []))
if filters:
filtered = []
for item in results:
match = True
for k, v in filters.items():
if item.get(k) != v:
match = False
break
if match:
filtered.append(item)
return filtered
return results
@classmethod
def toString(cls) -> str:
return (f"ContactsContract(contacts={len(cls._store['contacts'])}, "
f"raw={len(cls._store['raw_contacts'])}, "
f"data={len(cls._store['data'])})")