diff --git a/android_pyapi/__init__.py b/android_pyapi/__init__.py index 7101a52..4fef857 100644 --- a/android_pyapi/__init__.py +++ b/android_pyapi/__init__.py @@ -59,6 +59,48 @@ from .android.webkit import * from .android.nfc import * from .android.speech_tts import * from .android.inputmethod import * +from .android.app_admin import * +from .android.app_backup import * +from .android.app_job import * +from .android.app_people import * +from .android.app_role import * +from .android.app_usage import * +from .android.appwidget import * +from .android.companion import * +from .android.credentials import * +from .android.hardware_display import * +from .android.hardware_fingerprint import * +from .android.hardware_input import * +from .android.hardware_usb import * +from .android.media_audiofx import * +from .android.media_browse import * +from .android.media_midi import * +from .android.media_projection import * +from .android.media_session import * +from .android.media_tv import * +from .android.mtp import * +from .android.opengl import * +from .android.print import * +from .android.renderscript import * +from .android.se import * +from .android.security import * +from .android.service_autofill import * +from .android.service_controls import * +from .android.service_dreams import * +from .android.service_notification import * +from .android.service_quicksettings import * +from .android.service_vr import * +from .android.service_wallpaper import * +from .android.sip import * +from .android.speech import * +from .android.system import * +from .android.telecom import * +from .android.test import * +from .android.transition import * +from .android.uwb import * +from .android.view_autofill import * +from .android.view_textclassifier import * +from .android.view_translation import * # java.* 包 from .java.io import * @@ -206,32 +248,111 @@ class TimerTask: __version__ = "1.0.0" __all__ = [ # android - "Intent", "Context", "Uri", "ContentValues", "ContentResolver", - "BroadcastReceiver", "IntentFilter", "SharedPreferences", - "PackageManager", "PackageInfo", "ActivityManager", - "Build", "Environment", "PowerManager", "BatteryManager", - "Bundle", "Handler", "Looper", "Message", - "View", "ViewGroup", "TextView", "EditText", "Button", - "ImageView", "ScrollView", "ListView", "Spinner", - "CheckBox", "Switch", "ProgressBar", "Toast", - "Bitmap", "BitmapFactory", "Canvas", "Color", "Paint", - "Rect", "RectF", "Matrix", "Typeface", - "PdfRenderer", "Drawable", "BitmapDrawable", - "ConnectivityManager", "NetworkInfo", "WifiManager", - "MediaPlayer", "AudioManager", "MediaRecorder", - "SensorManager", "CameraManager", "BiometricPrompt", - "LocationManager", "Location", "Geocoder", - "SQLiteDatabase", "SQLiteOpenHelper", "Cursor", - "Settings", "MediaStore", "ContactsContract", "CalendarContract", - "TelephonyManager", "SmsManager", - "BluetoothAdapter", "BluetoothDevice", - "TextUtils", "Html", "SpannableString", - "Log", "Base64", "TypedValue", "AccessibilityNodeInfo", "AccessibilityService", - "Animation", "AlphaAnimation", "TranslateAnimation", + "ActivityManager", + "AlphaAnimation", + "Animation", + "AppWidgetManager", "AppWidgetProvider", "AppWidgetProviderInfo", + "AudioManager", + "AutofillManager", "AutofillValue", + "BackupAgent", "BackupManager", + "Base64", + "BatteryManager", + "BiometricPrompt", + "Bitmap", "BitmapDrawable", "BitmapFactory", + "BluetoothAdapter", "BluetoothDevice", + "BroadcastReceiver", + "Build", + "Bundle", + "Button", + "CalendarContract", + "Call", + "CameraManager", + "Canvas", + "CheckBox", + "Color", + "CompanionDeviceManager", + "ConnectivityManager", + "ContentResolver", + "ContentValues", + "ContactsContract", + "Context", + "CredentialManager", + "Cursor", + "DevicePolicyManager", + "DisplayManager", + "Drawable", + "EditText", + "Environment", + "FingerprintManager", + "Geocoder", + "Handler", + "Html", + "ImageView", + "InputDevice", + "InputManager", "InputMethodManager", - "WebView", + "Intent", + "IntentFilter", + "JobInfo", "JobScheduler", "JobService", + "KeyChain", + "ListView", + "Location", + "LocationManager", + "Log", + "Looper", + "Matrix", + "MediaBrowser", "MediaController", + "MediaPlayer", + "MediaProjectionManager", + "MediaRecorder", + "MediaSession", + "MediaStore", + "Message", + "MidiDevice", "MidiManager", + "MtpDevice", + "NetworkInfo", + "Os", "OsConstants", + "PackageInfo", "PackageManager", + "Paint", + "PdfRenderer", + "PeopleManager", + "PowerManager", + "PrintAttributes", "PrintDocumentAdapter", "PrintManager", + "ProgressBar", + "Rect", "RectF", + "RoleManager", + "ScrollView", + "SensorManager", + "Settings", + "SharedPreferences", + "SmsManager", + "SpannableString", + "SpeechRecognizer", + "Spinner", + "SQLiteDatabase", "SQLiteOpenHelper", + "StructStat", + "Switch", + "TelecomManager", + "TelephonyManager", + "TextClassifier", "TextToSpeech", + "TextUtils", + "TextView", + "Toast", + "TranslateAnimation", + "TranslationManager", + "TvInputManager", + "Typeface", + "TypedValue", + "Uri", + "UsageStatsManager", + "UsbDevice", "UsbManager", + "UwbManager", + "View", "ViewGroup", + "VirtualDisplay", + "WebView", + "WifiManager", # java "File", "FileInputStream", "FileOutputStream", "BufferedReader", "BufferedWriter", diff --git a/android_pyapi/android/app_admin.py b/android_pyapi/android/app_admin.py new file mode 100644 index 0000000..6727e86 --- /dev/null +++ b/android_pyapi/android/app_admin.py @@ -0,0 +1,424 @@ +""" +Android Device Administration API — DevicePolicyManager, DeviceAdminInfo, DeviceAdminReceiver, SystemUpdatePolicy +方法签名完全对齐 Android SDK +""" + +import os as _os +import sys +import tempfile +import shutil +from typing import Any, Dict, List, Optional, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "DevicePolicyManager", + "DeviceAdminInfo", + "DeviceAdminReceiver", + "SystemUpdatePolicy", +] + + +# ═══════════════════════════════════════════════════════════════ +# DevicePolicyManager 常量 +# ═══════════════════════════════════════════════════════════════ + +class DevicePolicyManager(Stub): + """DevicePolicyManager — 设备策略管理器 (桌面存根)""" + + # --- Action 常量 --- + ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN" + ACTION_DEVICE_ADMIN_SET_DOWNLOAD_URL = "android.app.action.DEVICE_ADMIN_SET_DOWNLOAD_URL" + ACTION_ADMIN_POLICY_COMPLIANCE = "android.app.action.ADMIN_POLICY_COMPLIANCE" + ACTION_PROVISION_MANAGED_PROFILE = "android.app.action.PROVISION_MANAGED_PROFILE" + ACTION_PROVISION_FINANCED_DEVICE = "android.app.action.PROVISION_FINANCED_DEVICE" + ACTION_STATE_USER_SETUP_COMPLETE = "android.app.action.STATE_USER_SETUP_COMPLETE" + ACTION_DEVICE_OWNER_CHANGED = "android.app.action.DEVICE_OWNER_CHANGED" + ACTION_PROFILE_OWNER_CHANGED = "android.app.action.PROFILE_OWNER_CHANGED" + ACTION_SET_NEW_PARENT_PROFILE_PASSWORD = "android.app.action.SET_NEW_PARENT_PROFILE_PASSWORD" + ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD" + ACTION_START_ENCRYPTION = "android.app.action.START_ENCRYPTION" + ACTION_SYSTEM_UPDATE_POLICY_CHANGED = "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED" + ACTION_APPLICATION_VERB = "android.app.action.APPLICATION_VERB" + ACTION_MANAGED_PROFILE_PROVISIONED = "android.app.action.MANAGED_PROFILE_PROVISIONED" + ACTION_ORG_OWNED_PROVISIONING = "android.app.action.ORG_OWNED_PROVISIONING" + ACTION_GET_PROVISIONING_MODE = "android.app.action.GET_PROVISIONING_MODE" + ACTION_ADMIN_SERVICE = "android.app.action.ADMIN_SERVICE" + + # --- Password quality constants --- + PASSWORD_QUALITY_UNSPECIFIED = 0 + PASSWORD_QUALITY_BIOMETRIC_WEAK = 0x8000 + PASSWORD_QUALITY_SOMETHING = 0x10000 + PASSWORD_QUALITY_NUMERIC = 0x20000 + PASSWORD_QUALITY_NUMERIC_COMPLEX = 0x30000 + PASSWORD_QUALITY_ALPHABETIC = 0x40000 + PASSWORD_QUALITY_ALPHANUMERIC = 0x50000 + PASSWORD_QUALITY_COMPLEX = 0x60000 + PASSWORD_QUALITY_MANAGED = 0x80000 + + # --- Encryption status --- + ENCRYPTION_STATUS_UNSUPPORTED = 0 + ENCRYPTION_STATUS_INACTIVE = 1 + ENCRYPTION_STATUS_ACTIVATING = 2 + ENCRYPTION_STATUS_ACTIVE = 3 + ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY = 4 + + # --- Keyguard disable flags --- + KEYGUARD_DISABLE_FEATURES_NONE = 0 + KEYGUARD_DISABLE_FEATURES_ALL = 0x7FFFFFFF + KEYGUARD_DISABLE_SECURE_CAMERA = 2 + KEYGUARD_DISABLE_SECURE_NOTIFICATIONS = 4 + KEYGUARD_DISABLE_TRUST_AGENTS = 8 + KEYGUARD_DISABLE_FINGERPRINT = 16 + KEYGUARD_DISABLE_REMOTE_INPUT = 32 + KEYGUARD_DISABLE_FACE = 64 + KEYGUARD_DISABLE_IRIS = 128 + KEYGUARD_DISABLE_PASSCODE = 256 + + # --- Lock screen info flags --- + FLAG_EVOLVING_DEVICE_ADMIN = 1 << 0 + FLAG_PARENT_CAN_ACCESS_LOCK_SCREEN = 1 << 1 + FLAG_SELF_PROFILE_DISABLED = 1 << 2 + + # --- Other constants --- + PERMISSION_GRANT_STATE_DEFAULT = 0 + PERMISSION_GRANT_STATE_GRANTED = 1 + PERMISSION_GRANT_STATE_DENIED = 2 + STATE_USER_SETUP_COMPLETE = 1 + STATE_USER_UNMANAGED = 0 + STATE_USER_MANAGED = 2 + LEAVE_ALL_KEYS = 0xFFFFFFFF + WIPE_EXTERNAL_STORAGE = 1 + WIPE_RESET_PROTECTION_DATA = 2 + WIPE_SILENTLY = 4 + SKIP_SETUP_WIZARD = 1 + SETTINGS_HOME = "settings_home" + SETTINGS_GLOBAL = "settings_global" + SETTINGS_SECURE = "settings_secure" + SETTINGS_SYSTEM = "settings_system" + LOCK_TASK_FEATURE_NONE = 0 + LOCK_TASK_FEATURE_SYSTEM_INFO = 1 + LOCK_TASK_FEATURE_KEYGUARD = 2 + LOCK_TASK_FEATURE_HOME = 4 + LOCK_TASK_FEATURE_GLOBAL_ACTIONS = 8 + LOCK_TASK_FEATURE_NOTIFICATIONS = 16 + LOCK_TASK_FEATURE_OVERVIEW = 32 + LOCK_TASK_FEATURE_CREDENTIALS = 64 + LOCK_TASK_FEATURE_ACCESSIBILITY = 128 + LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START = 256 + + def __init__(self): + super().__init__() + self._active_admins: List[Dict[str, Any]] = [] + self._password_quality = self.PASSWORD_QUALITY_UNSPECIFIED + self._camera_disabled = False + self._keyguard_disabled = False + + def isAdminActive(self, admin: "DeviceAdminInfo") -> bool: + """检查设备管理器是否已激活""" + return any(a.getComponent() == admin.getComponent() for a in self._active_admins) + + def isDeviceOwnerApp(self, packageName: str) -> bool: + """检查应用是否为设备所有者""" + return False + + def isProfileOwnerApp(self, packageName: str) -> bool: + """检查应用是否为配置文件所有者""" + return False + + def resetPassword(self, password: str, flags: int = 0) -> bool: + """重置设备密码 (桌面存根)""" + raise AndroidOnlyError("DevicePolicyManager.resetPassword() 仅在 Android 设备上可用") + + def lockNow(self): + """立即锁定设备""" + raise AndroidOnlyError("DevicePolicyManager.lockNow() 仅在 Android 设备上可用") + + def wipeData(self, flags: int = 0): + """擦除设备数据""" + raise AndroidOnlyError("DevicePolicyManager.wipeData() 仅在 Android 设备上可用") + + def setCameraDisabled(self, admin: "DeviceAdminInfo", disabled: bool): + """设置相机禁用状态""" + self._camera_disabled = disabled + + def setKeyguardDisabled(self, admin: "DeviceAdminInfo", disabled: bool) -> bool: + """设置锁屏禁用状态""" + self._keyguard_disabled = disabled + return True + + def installCaCert(self, admin: "DeviceAdminInfo", certBuffer: bytes) -> bool: + """安装 CA 证书 (桌面存根)""" + raise AndroidOnlyError("DevicePolicyManager.installCaCert() 仅在 Android 设备上可用") + + def uninstallCaCert(self, admin: "DeviceAdminInfo", certBuffer: bytes): + """卸载 CA 证书 (桌面存根)""" + raise AndroidOnlyError("DevicePolicyManager.uninstallCaCert() 仅在 Android 设备上可用") + + def getActiveAdmins(self) -> Optional[List["DeviceAdminInfo"]]: + """获取活跃的管理器列表""" + return list(self._active_admins) if self._active_admins else None + + def createAdminProfileInfo( + self, + packageName: str, + localizedName: str, + parentComponent: Any = None, + adminInfo: Any = None, + profileOwner: bool = False, + ) -> "DeviceAdminInfo": + """创建管理员配置文件信息""" + return DeviceAdminInfo( + packageName=packageName, + localizedName=localizedName, + parentComponent=parentComponent, + adminInfo=adminInfo, + profileOwner=profileOwner, + ) + + def toString(self) -> str: + return f"DevicePolicyManager@active_admins={len(self._active_admins)}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# DeviceAdminInfo +# ═══════════════════════════════════════════════════════════════ + +class DeviceAdminInfo(Stub): + """DeviceAdminInfo — 设备管理员信息""" + + USES_POLICY_WATCH_LOG = "uses_policy_watch_log" + USES_POLICY_DISABLE_KEYGUARD_FEATURES = "uses_policy_disable_keyguard_features" + USES_POLICY_DISABLE_CAMERA = "uses_policy_disable_camera" + USES_POLICY_RESET_PASSWORD = "uses_policy_reset_password" + USES_POLICY_FORCE_LOCK = "uses_policy_force_lock" + USES_POLICY_WIPE_DATA = "uses_policy_wipe_data" + USES_POLICY_EXPIRE_PASSWORD = "uses_policy_expire_password" + USES_POLICY_LIMIT_PASSWORD = "uses_policy_limit_password" + USES_ENCRYPTED_STORAGE = "uses_encrypted_storage" + USES_POLICY_SETS_DEVICE_OWNER = "uses_policy_sets_device_owner" + USES_POLICY_SETS_PROFILE_OWNER = "uses_policy_sets_profile_owner" + USES_POLICY_INSTALL_CERT = "uses_policy_install_cert" + USES_POLICY_UNINSTALL_CERT = "uses_policy_uninstall_cert" + + def __init__( + self, + packageName: str = "", + localizedName: str = "", + parentComponent: Any = None, + adminInfo: Any = None, + profileOwner: bool = False, + ): + super().__init__() + self._packageName = packageName + self._localizedName = localizedName + self._parentComponent = parentComponent + self._adminInfo = adminInfo + self._profileOwner = profileOwner + + def getComponent(self) -> Optional[str]: + """获取组件标识""" + return self._parentComponent + + def getPackageName(self) -> str: + return self._packageName + + def isVisible(self) -> bool: + return True + + def isProfileOwner(self) -> bool: + return self._profileOwner + + def toString(self) -> str: + return f"DeviceAdminInfo(package={self._packageName})" + + def equals(self, other) -> bool: + if not isinstance(other, DeviceAdminInfo): + return False + return self._packageName == other._packageName and self._parentComponent == other._parentComponent + + def hashCode(self) -> int: + return hash(self._packageName) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# DeviceAdminReceiver +# ═══════════════════════════════════════════════════════════════ + +class DeviceAdminReceiver(Stub): + """DeviceAdminReceiver — 设备管理员广播接收器""" + + ACTION_DEVICE_ADMIN_ENABLED = "android.app.action.DEVICE_ADMIN_ENABLED" + ACTION_DEVICE_ADMIN_DISABLED = "android.app.action.DEVICE_ADMIN_DISABLED" + ACTION_DEVICE_ADMIN_DISABLE_REQUESTED = "android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED" + ACTION_PASSWORD_CHANGED = "android.app.action.ACTION_PASSWORD_CHANGED" + ACTION_PASSWORD_FAILED = "android.app.action.ACTION_PASSWORD_FAILED" + ACTION_PASSWORD_SUCCEEDED = "android.app.action.ACTION_PASSWORD_SUCCEEDED" + ACTION_PASSWORD_EXPIRING = "android.app.action.ACTION_PASSWORD_EXPIRING" + ACTION_PROFILE_PROVISIONING_COMPLETE = "android.app.action.PROFILE_PROVISIONING_COMPLETE" + ACTION_BUGREPORT_SHARING_ACCEPTED = "android.app.action.BUGREPORT_SHARING_ACCEPTED" + ACTION_BUGREPORT_SHARING_DECLINED = "android.app.action.BUGREPORT_SHARING_DECLINED" + ACTION_BUGREPORT_FAILED = "android.app.action.BUGREPORT_FAILED" + ACTION_NOTIFY_PENDING_SYSTEM_UPDATE = "android.app.action.NOTIFY_PENDING_SYSTEM_UPDATE" + ACTION_CHOOSE_PRIVATE_KEY_ALIAS = "android.app.action.CHOOSE_PRIVATE_KEY_ALIAS" + ACTION_NETWORK_LOGS_AVAILABLE = "android.app.action.NETWORK_LOGS_AVAILABLE" + ACTION_SECURITY_LOGS_AVAILABLE = "android.app.action.SECURITY_LOGS_AVAILABLE" + ACTION_USER_ADDED = "android.app.action.USER_ADDED" + ACTION_USER_REMOVED = "android.app.action.USER_REMOVED" + ACTION_USER_STARTED = "android.app.action.USER_STARTED" + ACTION_USER_STOPPED = "android.app.action.USER_STOPPED" + ACTION_USER_SWITCHED = "android.app.action.USER_SWITCHED" + ACTION_LOCK_TASK_ENTERING = "android.app.action.LOCK_TASK_ENTERING" + ACTION_LOCK_TASK_EXITING = "android.app.action.LOCK_TASK_EXITING" + + def __init__(self): + super().__init__() + + def onEnabled(self, context: Any, intent: Any): + """管理员启用回调""" + pass + + def onDisabled(self, context: Any, intent: Any): + """管理员禁用回调""" + pass + + def onPasswordChanged(self, context: Any, intent: Any): + """密码更改回调""" + pass + + def onPasswordExpiring(self, context: Any, intent: Any): + """密码过期回调""" + pass + + def onPasswordFailed(self, context: Any, intent: Any): + """密码失败回调""" + pass + + def onPasswordSucceeded(self, context: Any, intent: Any): + """密码成功回调""" + pass + + def onProfileProvisioningComplete(self, context: Any, intent: Any): + """配置文件配置完成回调""" + pass + + def onBugreportSharingAccepted(self, context: Any, intent: Any, bugreportHash: bytes): + """错误报告分享接受回调""" + pass + + def onBugreportSharingDeclined(self, context: Any, intent: Any): + """错误报告分享拒绝回调""" + pass + + def onBugreportFailed(self, context: Any, intent: Any, bugreportHash: bytes): + """错误报告失败回调""" + pass + + def onNotifyPendingSystemUpdate(self, context: Any, intent: Any, updateReceivedTime: int): + """系统更新待处理通知回调""" + pass + + def onNetworkLogsAvailable(self, context: Any, intent: Any, batchToken: int, networkLogsCount: int): + """网络日志可用回调""" + pass + + def onSecurityLogsAvailable(self, context: Any, intent: Any): + """安全日志可用回调""" + pass + + def onUserAdded(self, context: Any, intent: Any, newUserHandle: Any): + """用户添加回调""" + pass + + def onUserRemoved(self, context: Any, intent: Any, removedUserHandle: Any): + """用户移除回调""" + pass + + def onUserStarted(self, context: Any, intent: Any, userHandle: Any): + """用户启动回调""" + pass + + def onUserStopped(self, context: Any, intent: Any, userHandle: Any): + """用户停止回调""" + pass + + def onUserSwitched(self, context: Any, intent: Any, switchedUserHandle: Any): + """用户切换回调""" + pass + + def onLockTaskModeEntering(self, context: Any, intent: Any, pkg: str): + """锁任务模式进入回调""" + pass + + def onLockTaskModeExiting(self, context: Any, intent: Any): + """锁任务模式退出回调""" + pass + + def toString(self) -> str: + return f"DeviceAdminReceiver@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# SystemUpdatePolicy +# ═══════════════════════════════════════════════════════════════ + +class SystemUpdatePolicy(Stub): + """SystemUpdatePolicy — 系统更新策略""" + + TYPE_INSTALL_AUTOMATIC = 1 + TYPE_INSTALL_WINDOWED = 2 + TYPE_POSTPONE = 3 + + def __init__(self, policyType: int = TYPE_INSTALL_AUTOMATIC): + super().__init__() + self._policyType = policyType + self._startTime: Optional[str] = None + self._endTime: Optional[str] = None + + def getPolicyType(self) -> int: + return self._policyType + + def setPolicyType(self, policyType: int): + self._policyType = policyType + + def setInstallWindow(self, startTime: str, endTime: str): + """设置安装窗口时间段 (格式 HH:mm)""" + self._startTime = startTime + self._endTime = endTime + + def getStartTime(self) -> Optional[str]: + return self._startTime + + def getEndTime(self) -> Optional[str]: + return self._endTime + + @staticmethod + def createPolicy(policyType: int) -> "SystemUpdatePolicy": + return SystemUpdatePolicy(policyType=policyType) + + def toString(self) -> str: + return f"SystemUpdatePolicy(type={self._policyType})" + + def equals(self, other) -> bool: + if not isinstance(other, SystemUpdatePolicy): + return False + return self._policyType == other._policyType + + def hashCode(self) -> int: + return self._policyType & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/app_backup.py b/android_pyapi/android/app_backup.py new file mode 100644 index 0000000..5e23961 --- /dev/null +++ b/android_pyapi/android/app_backup.py @@ -0,0 +1,477 @@ +""" +Android Backup API — BackupManager, BackupAgent, BackupDataInput/Output, RestoreObserver 等 +方法签名完全对齐 Android SDK +""" + +import json +import os as _os +import sys +import tempfile +import shutil +import time +from datetime import datetime +from typing import Any, Dict, List, Optional, Set, Tuple, Callable, BinaryIO + +from .._core import Stub, AndroidOnlyError, resolve_android_path, _app_data_dir +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "BackupManager", + "BackupAgent", + "BackupDataInput", + "BackupDataOutput", + "FullBackupDataOutput", + "BackupHelper", + "FileBackupHelper", + "SharedPreferencesBackupHelper", + "RestoreObserver", + "BackupDataInputStream", +] + + +# ═══════════════════════════════════════════════════════════════ +# 备份存储路径 +# ═══════════════════════════════════════════════════════════════ + +_BACKUP_DIR = _os.path.join(_app_data_dir, "backups") + + +def _ensure_backup_dir(): + _os.makedirs(_BACKUP_DIR, exist_ok=True) + + +_ensure_backup_dir() + + +# ═══════════════════════════════════════════════════════════════ +# BackupManager +# ═══════════════════════════════════════════════════════════════ + +class BackupManager(Stub): + """BackupManager — 备份管理器 (桌面文件备份模拟)""" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._backup_enabled = True + + def dataChanged(self, packageName: str = None): + """通知系统数据已更改,需要备份""" + if packageName: + flag_path = _os.path.join(_BACKUP_DIR, f"{packageName}.pending") + elif self._context: + flag_path = _os.path.join(_BACKUP_DIR, f"{self._context.getPackageName()}.pending") + else: + flag_path = _os.path.join(_BACKUP_DIR, "default.pending") + with open(flag_path, "w") as f: + f.write(time.time()) + + def requestRestore(self, observer: "RestoreObserver") -> int: + """请求恢复备份数据""" + raise AndroidOnlyError("BackupManager.requestRestore() 仅在 Android 设备上可用") + + def getCurrentTransport(self) -> Optional[str]: + """获取当前备份传输名称""" + return "com.android.localtransport.LocalTransport" + + def listAllTransports(self) -> List[str]: + """列出所有可用备份传输""" + return ["com.android.localtransport.LocalTransport"] + + def isBackupEnabled(self) -> bool: + """检查备份是否启用""" + return self._backup_enabled + + def setBackupEnabled(self, isEnabled: bool): + """设置备份启用状态""" + self._backup_enabled = isEnabled + + def toString(self) -> str: + return f"BackupManager(backup_enabled={self._backup_enabled})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# BackupAgent +# ═══════════════════════════════════════════════════════════════ + +class BackupAgent(Stub): + """BackupAgent — 备份代理基类""" + + def __init__(self): + super().__init__() + + def onCreate(self): + """创建备份代理时调用""" + pass + + def onBackup( + self, + oldState: "BackupDataInput", + data: "BackupDataOutput", + newState: "BackupDataOutput", + ): + """执行备份操作""" + pass + + def onRestore( + self, + data: "BackupDataInput", + appVersionCode: int, + newState: "BackupDataOutput", + ): + """执行恢复操作""" + pass + + def toString(self) -> str: + return f"BackupAgent@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# BackupDataInput +# ═══════════════════════════════════════════════════════════════ + +class BackupDataInput(Stub): + """BackupDataInput — 备份数据输入流""" + + def __init__(self, filepath: str = ""): + super().__init__() + self._filepath = filepath + self._data: Dict[str, Any] = {} + self._keys: List[str] = [] + self._index = 0 + if filepath and _os.path.exists(filepath): + with open(filepath, "r") as f: + self._data = json.load(f) + self._keys = list(self._data.keys()) + + def readNextHeader(self) -> bool: + """读取下一个数据头""" + if self._index < len(self._keys): + return True + return False + + def getKey(self) -> str: + """获取当前键名""" + if self._index < len(self._keys): + return self._keys[self._index] + return "" + + def getDataSize(self) -> int: + """获取当前数据大小""" + if self._index < len(self._keys): + val = self._data[self._keys[self._index]] + return len(str(val)) + return 0 + + def readEntityData(self, data: bytearray, offset: int, size: int) -> int: + """读取实体数据""" + if self._index < len(self._keys): + val = str(self._data[self._keys[self._index]]) + raw = val.encode("utf-8") + copy_len = min(size, len(raw) - offset) + for i in range(copy_len): + data[offset + i] = raw[i] + self._index += 1 + return copy_len + return -1 + + def skipEntityData(self): + """跳过当前实体数据""" + self._index += 1 + + def toString(self) -> str: + return f"BackupDataInput(file={self._filepath})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# BackupDataOutput +# ═══════════════════════════════════════════════════════════════ + +class BackupDataOutput(Stub): + """BackupDataOutput — 备份数据输出流""" + + def __init__(self, filepath: str = ""): + super().__init__() + self._filepath = filepath + self._data: Dict[str, str] = {} + + def writeEntityHeader(self, key: str, dataSize: int): + """写入实体数据头""" + pass + + def writeEntityData(self, data: bytes, size: int): + """写入实体数据""" + key = f"entity_{len(self._data)}" + self._data[key] = data[:size].decode("utf-8", errors="replace") + if self._filepath: + _os.makedirs(_os.path.dirname(self._filepath), exist_ok=True) + with open(self._filepath, "w") as f: + json.dump(self._data, f) + + def toString(self) -> str: + return f"BackupDataOutput(file={self._filepath})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# FullBackupDataOutput +# ═══════════════════════════════════════════════════════════════ + +class FullBackupDataOutput(Stub): + """FullBackupDataOutput — 完整备份数据输出""" + + def __init__(self, quota: int = 0, transportFlags: int = 0): + super().__init__() + self._quota = quota + self._transportFlags = transportFlags + + def getQuota(self) -> int: + return self._quota + + def getTransportFlags(self) -> int: + return self._transportFlags + + def toString(self) -> str: + return f"FullBackupDataOutput(quota={self._quota})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# BackupHelper +# ═══════════════════════════════════════════════════════════════ + +class BackupHelper(Stub): + """BackupHelper — 备份辅助接口""" + + def __init__(self): + super().__init__() + + def performBackup( + self, + oldState: "BackupDataInput", + data: "BackupDataOutput", + newState: "BackupDataOutput", + ): + """执行备份""" + raise AndroidOnlyError("BackupHelper.performBackup() 仅在 Android 设备上可用") + + def restoreEntity(self, data: "BackupDataInput", appVersionCode: int): + """恢复实体""" + raise AndroidOnlyError("BackupHelper.restoreEntity() 仅在 Android 设备上可用") + + def toString(self) -> str: + return f"BackupHelper@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# FileBackupHelper +# ═══════════════════════════════════════════════════════════════ + +class FileBackupHelper(BackupHelper): + """FileBackupHelper — 文件备份辅助""" + + def __init__(self, context: Any, files: List[str]): + super().__init__() + self._context = context + self._files = list(files) + + def performBackup( + self, + oldState: "BackupDataInput", + data: "BackupDataOutput", + newState: "BackupDataOutput", + ): + """执行文件备份""" + for fname in self._files: + if self._context: + filepath = _os.path.join(self._context.getFilesDir(), fname) + if _os.path.exists(filepath): + with open(filepath, "rb") as f: + content = f.read() + data.writeEntityHeader(fname, len(content)) + data.writeEntityData(content, len(content)) + + def restoreEntity(self, data: "BackupDataInput", appVersionCode: int): + """恢复文件实体""" + key = data.getKey() + if self._context and key in self._files: + filepath = _os.path.join(self._context.getFilesDir(), key) + size = data.getDataSize() + buf = bytearray(size) + data.readEntityData(buf, 0, size) + _os.makedirs(_os.path.dirname(filepath), exist_ok=True) + with open(filepath, "wb") as f: + f.write(buf) + + def toString(self) -> str: + return f"FileBackupHelper(files={self._files})" + + def equals(self, other) -> bool: + if not isinstance(other, FileBackupHelper): + return False + return self._files == other._files + + def hashCode(self) -> int: + return hash(tuple(self._files)) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# SharedPreferencesBackupHelper +# ═══════════════════════════════════════════════════════════════ + +class SharedPreferencesBackupHelper(BackupHelper): + """SharedPreferencesBackupHelper — SharedPreferences 备份辅助""" + + def __init__(self, context: Any, prefNames: List[str]): + super().__init__() + self._context = context + self._prefNames = list(prefNames) + + def performBackup( + self, + oldState: "BackupDataInput", + data: "BackupDataOutput", + newState: "BackupDataOutput", + ): + """执行 SharedPreferences 备份""" + if not self._context: + return + prefs_dir = _os.path.join(_app_data_dir, "shared_prefs") + for name in self._prefNames: + filepath = _os.path.join(prefs_dir, f"{name}.xml") + if _os.path.exists(filepath): + with open(filepath, "rb") as f: + content = f.read() + data.writeEntityHeader(name, len(content)) + data.writeEntityData(content, len(content)) + + def restoreEntity(self, data: "BackupDataInput", appVersionCode: int): + """恢复 SharedPreferences""" + key = data.getKey() + if self._context and key in self._prefNames: + prefs_dir = _os.path.join(_app_data_dir, "shared_prefs") + filepath = _os.path.join(prefs_dir, f"{key}.xml") + size = data.getDataSize() + buf = bytearray(size) + data.readEntityData(buf, 0, size) + _os.makedirs(prefs_dir, exist_ok=True) + with open(filepath, "wb") as f: + f.write(buf) + + def toString(self) -> str: + return f"SharedPreferencesBackupHelper(prefs={self._prefNames})" + + def equals(self, other) -> bool: + if not isinstance(other, SharedPreferencesBackupHelper): + return False + return self._prefNames == other._prefNames + + def hashCode(self) -> int: + return hash(tuple(self._prefNames)) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# RestoreObserver +# ═══════════════════════════════════════════════════════════════ + +class RestoreObserver(Stub): + """RestoreObserver — 恢复过程观察者""" + + def __init__(self): + super().__init__() + + def onUpdate(self, nowBeingRestored: int, currentPackage: str): + """恢复进度更新回调""" + pass + + def onRestoreStarting(self, numPackages: int): + """恢复开始回调""" + pass + + def onRestoreFinished(self, error: int): + """恢复完成回调""" + pass + + def toString(self) -> str: + return f"RestoreObserver@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# BackupDataInputStream +# ═══════════════════════════════════════════════════════════════ + +class BackupDataInputStream(Stub): + """BackupDataInputStream — 备份数据输入流包装器""" + + def __init__(self, dataInput: "BackupDataInput" = None): + super().__init__() + self._input = dataInput + self._pos = 0 + self._size = 0 + + def read(self) -> int: + """读取一个字节""" + if self._input and self._pos < self._size: + self._pos += 1 + return 0 + return -1 + + def readFully(self, data: bytearray, offset: int, length: int): + """读取完整数据块""" + if self._input: + self._input.readEntityData(data, offset, length) + + def size(self) -> int: + return self._size + + def toString(self) -> str: + return f"BackupDataInputStream(size={self._size})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/app_job.py b/android_pyapi/android/app_job.py new file mode 100644 index 0000000..6422700 --- /dev/null +++ b/android_pyapi/android/app_job.py @@ -0,0 +1,444 @@ +""" +Android Job Scheduler API — JobScheduler, JobInfo, JobService, JobParameters, JobWorkItem +方法签名完全对齐 Android SDK +""" + +import heapq +import threading +import time +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "JobScheduler", + "JobInfo", + "JobService", + "JobParameters", + "JobWorkItem", +] + + +# ═══════════════════════════════════════════════════════════════ +# JobScheduler +# ═══════════════════════════════════════════════════════════════ + +class JobScheduler(Stub): + """JobScheduler — 作业调度器 (桌面线程模拟)""" + + def __init__(self): + super().__init__() + self._lock = threading.Lock() + self._jobs: Dict[int, "JobInfo"] = {} + self._next_id = 1 + self._worker_thread: Optional[threading.Thread] = None + self._running = False + self._start_worker() + + def _start_worker(self): + """启动后台工作线程""" + if self._worker_thread is None or not self._worker_thread.is_alive(): + self._running = True + self._worker_thread = threading.Thread(target=self._run_loop, daemon=True) + self._worker_thread.start() + + def _run_loop(self): + """运行作业调度循环""" + while self._running: + with self._lock: + now = time.time() + for job_id, info in list(self._jobs.items()): + if info._deadline and now >= info._deadline: + self._jobs.pop(job_id, None) + + time.sleep(1.0) + + def schedule(self, job: "JobInfo") -> int: + """调度一个作业""" + with self._lock: + job_id = self._next_id + self._next_id += 1 + job._id = job_id + if job._periodic: + job._deadline = time.time() + job._period_ms / 1000.0 + elif job._deadline_ms: + job._deadline = time.time() + job._deadline_ms / 1000.0 + else: + job._deadline = None + self._jobs[job_id] = job + return job_id + + def enqueue(self, job: "JobInfo", workItem: "JobWorkItem") -> int: + """将作业加入队列""" + return self.schedule(job) + + def cancel(self, jobId: int): + """取消指定作业""" + with self._lock: + self._jobs.pop(jobId, None) + + def cancelAll(self): + """取消所有作业""" + with self._lock: + self._jobs.clear() + + def getAllPendingJobs(self) -> List["JobInfo"]: + """获取所有待处理作业""" + with self._lock: + return list(self._jobs.values()) + + def getPendingJob(self, jobId: int) -> Optional["JobInfo"]: + """获取指定待处理作业""" + with self._lock: + return self._jobs.get(jobId) + + def getTransientNetwork(self) -> Optional[Any]: + """获取瞬时网络""" + return None + + def getStartedJobs(self) -> List["JobInfo"]: + """获取已开始的作业""" + return [] + + def toString(self) -> str: + with self._lock: + return f"JobScheduler(pending={len(self._jobs)})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# JobInfo +# ═══════════════════════════════════════════════════════════════ + +class JobInfo(Stub): + """JobInfo — 作业信息""" + + # --- NetworkType 常量 --- + NETWORK_TYPE_NONE = 0 + NETWORK_TYPE_ANY = 1 + NETWORK_TYPE_UNMETERED = 2 + NETWORK_TYPE_NOT_ROAMING = 3 + NETWORK_TYPE_CELLULAR = 4 + + # --- Backoff 策略 --- + BACKOFF_POLICY_LINEAR = 0 + BACKOFF_POLICY_EXPONENTIAL = 1 + + def __init__(self): + super().__init__() + self._id = 0 + self._service = None + self._required_network_type = self.NETWORK_TYPE_NONE + self._requires_charging = False + self._requires_device_idle = False + self._deadline_ms = 0 + self._min_latency_ms = 0 + self._period_ms = 0 + self._periodic = False + self._backoff_policy = self.BACKOFF_POLICY_EXPONENTIAL + self._backoff_delay_ms = 30000 + self._extras = None + self._transient_network = False + self._estimated_network_bytes = -1 + self._prefetch_network_bytes = -1 + self._requires_storage_not_low = False + + def getId(self) -> int: + return self._id + + def getService(self) -> Optional[Any]: + return self._service + + def getRequiredNetworkType(self) -> int: + return self._required_network_type + + def isRequiresCharging(self) -> bool: + return self._requires_charging + + def isRequiresDeviceIdle(self) -> bool: + return self._requires_device_idle + + def isRequiresStorageNotLow(self) -> bool: + return self._requires_storage_not_low + + def getMinLatencyMillis(self) -> int: + return self._min_latency_ms + + def getMaxExecutionDelayMillis(self) -> int: + return self._deadline_ms + + def isPeriodic(self) -> bool: + return self._periodic + + def getBackoffPolicy(self) -> int: + return self._backoff_policy + + def getInitialBackoffMillis(self) -> int: + return self._backoff_delay_ms + + def getEstimatedNetworkBytes(self) -> int: + return self._estimated_network_bytes + + def getPrefetchNetworkBytes(self) -> int: + return self._prefetch_network_bytes + + def getTransientNetwork(self) -> Optional[Any]: + return None + + def isPersisted(self) -> bool: + return False + + def getExtras(self) -> Optional[Any]: + return self._extras + + # ═══════════════════════════════════════════════════════════ + # Builder + # ═══════════════════════════════════════════════════════════ + + class Builder(Stub): + """JobInfo.Builder — 作业构建器""" + + def __init__(self, jobId: int, jobService: Any): + super().__init__() + self._info = JobInfo() + self._info._id = jobId + self._info._service = jobService + + def setRequiredNetworkType(self, networkType: int) -> "JobInfo.Builder": + self._info._required_network_type = networkType + return self + + def setRequiresCharging(self, requiresCharging: bool) -> "JobInfo.Builder": + self._info._requires_charging = requiresCharging + return self + + def setRequiresDeviceIdle(self, requiresDeviceIdle: bool) -> "JobInfo.Builder": + self._info._requires_device_idle = requiresDeviceIdle + return self + + def setRequiresStorageNotLow(self, requiresStorageNotLow: bool) -> "JobInfo.Builder": + self._info._requires_storage_not_low = requiresStorageNotLow + return self + + def setOverrideDeadline(self, deadlineMillis: int) -> "JobInfo.Builder": + self._info._deadline_ms = deadlineMillis + return self + + def setMinimumLatency(self, minLatencyMillis: int) -> "JobInfo.Builder": + self._info._min_latency_ms = minLatencyMillis + return self + + def setBackoffCriteria( + self, initialBackoffMillis: int, backoffPolicy: int + ) -> "JobInfo.Builder": + self._info._backoff_delay_ms = initialBackoffMillis + self._info._backoff_policy = backoffPolicy + return self + + def setPeriodic(self, intervalMillis: int) -> "JobInfo.Builder": + self._info._periodic = True + self._info._period_ms = intervalMillis + return self + + def setTransientNetwork(self, transient: bool) -> "JobInfo.Builder": + self._info._transient_network = transient + return self + + def setEstimatedNetworkBytes(self, estimatedBytes: int) -> "JobInfo.Builder": + self._info._estimated_network_bytes = estimatedBytes + return self + + def setPrefetchNetworkBytes(self, prefetchBytes: int) -> "JobInfo.Builder": + self._info._prefetch_network_bytes = prefetchBytes + return self + + def setExtras(self, extras: Any) -> "JobInfo.Builder": + self._info._extras = extras + return self + + def build(self) -> "JobInfo": + return self._info + + def toString(self) -> str: + return f"Builder(jobId={self._info._id})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def toString(self) -> str: + return f"JobInfo(id={self._id}, network={self._required_network_type})" + + def equals(self, other) -> bool: + if not isinstance(other, JobInfo): + return False + return self._id == other._id + + def hashCode(self) -> int: + return self._id & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# JobService +# ═══════════════════════════════════════════════════════════════ + +class JobService(Stub): + """JobService — 作业服务基类""" + + def __init__(self): + super().__init__() + + def onStartJob(self, params: "JobParameters") -> bool: + """作业开始时调用。返回 True 表示需要后台线程处理。""" + return False + + def onStopJob(self, params: "JobParameters") -> bool: + """作业停止时调用。返回 True 表示应重试。""" + return False + + def jobFinished(self, params: "JobParameters", needsReschedule: bool): + """通知系统作业已完成""" + pass + + def toString(self) -> str: + return f"JobService@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# JobParameters +# ═══════════════════════════════════════════════════════════════ + +class JobParameters(Stub): + """JobParameters — 作业参数""" + + # --- StopReason 常量 --- + STOP_REASON_UNDEFINED = 0 + STOP_REASON_DATA_CLEARED = 1 + STOP_REASON_APP_BACKGROUND = 2 + STOP_REASON_APP_STANDBY = 3 + STOP_REASON_DEVICE_IDLE = 4 + STOP_REASON_PREEXISTING = 5 + STOP_REASON_ESTIMATED_NETWORK_BYTES_EXCEEDED = 6 + STOP_REASON_QUOTA = 7 + STOP_REASON_TAPPED_CANCEL = 8 + STOP_REASON_USER = 9 + STOP_REASON_CONSTRAINT_BATTERY_NOT_OK = 10 + STOP_REASON_CONSTRAINT_NETWORK_NOT_OK = 11 + STOP_REASON_CONSTRAINT_CONNECTIVITY_NOT_OK = 12 + STOP_REASON_CONSTRAINT_STORAGE_NOT_OK = 13 + STOP_REASON_CONSTRAINT_CHARGING_NOT_OK = 14 + STOP_REASON_CONSTRAINT_IDLE_NOT_OK = 15 + STOP_REASON_CANCELED = 16 + STOP_REASON_CANCELLED_BY_APP = 17 + STOP_REASON_DEVICE_STATE = 18 + STOP_REASON_SYSTEM_PROCESSING = 19 + STOP_REASON_WORK_POLICY = 20 + STOP_REASON_SYSTEM_EXECUTION = 21 + STOP_REASON_RESTRICTED_BACKGROUND = 22 + STOP_REASON_TIMEOUT = 23 + + def __init__(self, jobId: int = 0): + super().__init__() + self._job_id = jobId + self._extras = None + self._network = None + self._transient_network = None + self._stop_reason = 0 + self._override_deadline_expired = False + self._requires_charging = False + self._requires_device_idle = False + self._requires_storage_not_low = False + self._required_network_type = JobInfo.NETWORK_TYPE_NONE + + def getJobId(self) -> int: + return self._job_id + + def getExtras(self) -> Optional[Any]: + return self._extras + + def getNetwork(self) -> Optional[Any]: + return self._network + + def getTransientNetwork(self) -> Optional[Any]: + return self._transient_network + + def getStopReason(self) -> int: + return self._stop_reason + + def isOverrideDeadlineExpired(self) -> bool: + return self._override_deadline_expired + + def isRequiresCharging(self) -> bool: + return self._requires_charging + + def isRequiresDeviceIdle(self) -> bool: + return self._requires_device_idle + + def isRequiresStorageNotLow(self) -> bool: + return self._requires_storage_not_low + + def getRequiredNetworkType(self) -> int: + return self._required_network_type + + def toString(self) -> str: + return f"JobParameters(jobId={self._job_id})" + + def equals(self, other) -> bool: + if not isinstance(other, JobParameters): + return False + return self._job_id == other._job_id + + def hashCode(self) -> int: + return self._job_id & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# JobWorkItem +# ═══════════════════════════════════════════════════════════════ + +class JobWorkItem(Stub): + """JobWorkItem — 作业工作项""" + + def __init__(self, intent: Any = None): + super().__init__() + self._intent = intent + self._delivery_count = 0 + self._latest_delivery_intent = None + + def getIntent(self) -> Optional[Any]: + return self._intent + + def getDeliveryCount(self) -> int: + return self._delivery_count + + def getLatestDeliveryIntent(self) -> Optional[Any]: + return self._latest_delivery_intent + + def toString(self) -> str: + return f"JobWorkItem(deliveryCount={self._delivery_count})" + + def equals(self, other) -> bool: + if not isinstance(other, JobWorkItem): + return False + return self._intent == other._intent + + def hashCode(self) -> int: + return hash(str(self._intent)) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/app_people.py b/android_pyapi/android/app_people.py new file mode 100644 index 0000000..c139a83 --- /dev/null +++ b/android_pyapi/android/app_people.py @@ -0,0 +1,268 @@ +""" +Android People API — PeopleManager, ConversationStatus, PeopleManager.Person, PeopleManager.Conversation +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "PeopleManager", + "ConversationStatus", +] + + +# ═══════════════════════════════════════════════════════════════ +# PeopleManager +# ═══════════════════════════════════════════════════════════════ + +class PeopleManager(Stub): + """PeopleManager — 人物管理器 (桌面存根)""" + + ACTION_PEOPLE_CONVERSATION = "android.app.people.action.PEOPLE_CONVERSATION" + ACTION_REMOVE_STATUS = "android.app.people.action.REMOVE_STATUS" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._statuses: Dict[str, "ConversationStatus"] = {} + self._people: Dict[str, "PeopleManager.Person"] = {} + self._conversations: Dict[str, "PeopleManager.Conversation"] = {} + self._listeners: List[Callable] = [] + + def getStatus(self, packageName: str, personUri: str) -> Optional["ConversationStatus"]: + """获取指定人的状态""" + key = f"{packageName}:{personUri}" + return self._statuses.get(key) + + def getDataIdFromPhoneNumber(self, phoneNumber: str) -> Optional[str]: + """根据电话号码获取数据 ID""" + return None + + def addOrUpdateStatus( + self, packageName: str, status: "ConversationStatus" + ): + """添加或更新状态""" + key = f"{packageName}:{status.getPersonUri()}" + self._statuses[key] = status + + def removeStatus(self, packageName: str, personUri: str): + """移除状态""" + key = f"{packageName}:{personUri}" + self._statuses.pop(key, None) + + def getStoredPeople(self) -> List["PeopleManager.Person"]: + """获取已存储的人物列表""" + return list(self._people.values()) + + def isConversation(self, packageName: str, personUri: str) -> bool: + """检查是否为对话""" + key = f"{packageName}:{personUri}" + return key in self._conversations + + def getConversation( + self, packageName: str, personUri: str + ) -> Optional["PeopleManager.Conversation"]: + """获取对话信息""" + key = f"{packageName}:{personUri}" + return self._conversations.get(key) + + # ═══════════════════════════════════════════════════════════ + # Status - 状态内部类 + # ═══════════════════════════════════════════════════════════ + + class Status(Stub): + """PeopleManager.Status — 状态基类""" + + def __init__(self): + super().__init__() + + def toString(self) -> str: + return f"Status@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + # ═══════════════════════════════════════════════════════════ + # Person - 人物内部类 + # ═══════════════════════════════════════════════════════════ + + class Person(Stub): + """PeopleManager.Person — 人物信息""" + + def __init__( + self, + name: str = "", + key: str = "", + uri: str = "", + importantForPeople: bool = False, + ): + super().__init__() + self._name = name + self._key = key + self._uri = uri + self._important_for_people = importantForPeople + + def getName(self) -> str: + return self._name + + def getKey(self) -> str: + return self._key + + def getUri(self) -> str: + return self._uri + + def getImportantForPeople(self) -> bool: + return self._important_for_people + + def toString(self) -> str: + return f"Person(name={self._name}, key={self._key})" + + def equals(self, other) -> bool: + if not isinstance(other, PeopleManager.Person): + return False + return self._key == other._key + + def hashCode(self) -> int: + return hash(self._key) & 0x7FFFFFFF + + # ═══════════════════════════════════════════════════════════ + # Conversation - 对话内部类 + # ═══════════════════════════════════════════════════════════ + + class Conversation(Stub): + """PeopleManager.Conversation — 对话信息""" + + def __init__( + self, + conv_id: str = "", + person: "PeopleManager.Person" = None, + notificationChannelId: str = "", + shortcutInfo: Any = None, + parentUserHandle: Any = None, + ): + super().__init__() + self._id = conv_id + self._person = person + self._notification_channel_id = notificationChannelId + self._shortcut_info = shortcutInfo + self._parent_user_handle = parentUserHandle + + def getId(self) -> str: + return self._id + + def getPerson(self) -> Optional["PeopleManager.Person"]: + return self._person + + def getNotificationChannelId(self) -> str: + return self._notification_channel_id + + def getShortcutInfo(self) -> Optional[Any]: + return self._shortcut_info + + def getParentUserHandle(self) -> Optional[Any]: + return self._parent_user_handle + + def toString(self) -> str: + return f"Conversation(id={self._id})" + + def equals(self, other) -> bool: + if not isinstance(other, PeopleManager.Conversation): + return False + return self._id == other._id + + def hashCode(self) -> int: + return hash(self._id) & 0x7FFFFFFF + + def toString(self) -> str: + return f"PeopleManager@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# ConversationStatus +# ═══════════════════════════════════════════════════════════════ + +class ConversationStatus(Stub): + """ConversationStatus — 对话状态""" + + def __init__( + self, + userId: int = 0, + packageName: str = "", + personUri: str = "", + status: int = 0, + statusIcon: int = 0, + notificationKey: str = "", + notificationContentDescription: str = "", + ): + super().__init__() + self._user_id = userId + self._package_name = packageName + self._person_uri = personUri + self._status = status + self._status_icon = statusIcon + self._notification_key = notificationKey + self._notification_content_description = notificationContentDescription + self._active = False + self._muted = False + + def getUserId(self) -> int: + return self._user_id + + def getPackageName(self) -> str: + return self._package_name + + def getPersonUri(self) -> str: + return self._person_uri + + def getStatus(self) -> int: + return self._status + + def getStatusIcon(self) -> int: + return self._status_icon + + def getNotificationKey(self) -> str: + return self._notification_key + + def getNotificationContentDescription(self) -> str: + return self._notification_content_description + + def isActive(self) -> bool: + return self._active + + def isMuted(self) -> bool: + return self._muted + + def setActive(self, active: bool): + self._active = active + + def setMuted(self, muted: bool): + self._muted = muted + + def toString(self) -> str: + return (f"ConversationStatus(pkg={self._package_name}, " + f"uri={self._person_uri}, active={self._active})") + + def equals(self, other) -> bool: + if not isinstance(other, ConversationStatus): + return False + return (self._package_name == other._package_name + and self._person_uri == other._person_uri) + + def hashCode(self) -> int: + return hash((self._package_name, self._person_uri)) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/app_role.py b/android_pyapi/android/app_role.py new file mode 100644 index 0000000..cb276a1 --- /dev/null +++ b/android_pyapi/android/app_role.py @@ -0,0 +1,117 @@ +""" +Android Role API — RoleManager +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "RoleManager", +] + + +# ═══════════════════════════════════════════════════════════════ +# RoleManager +# ═══════════════════════════════════════════════════════════════ + +class RoleManager(Stub): + """RoleManager — 角色管理器 (桌面存根)""" + + ACTION_REQUEST_ROLE = "android.app.role.action.REQUEST_ROLE" + + # --- 角色常量 --- + ROLE_ASSISTANT = "android.app.role.ASSISTANT" + ROLE_BROWSER = "android.app.role.BROWSER" + ROLE_DIALER = "android.app.role.DIALER" + ROLE_SMS = "android.app.role.SMS" + ROLE_HOME = "android.app.role.HOME" + ROLE_EMERGENCY = "android.app.role.EMERGENCY" + ROLE_CALL_REDIRECTION = "android.app.role.CALL_REDIRECTION" + ROLE_SYSTEM_EXCLUSIVE = "android.app.role.SYSTEM_EXCLUSIVE" + ROLE_WALLPAPER = "android.app.role.WALLPAPER" + ROLE_NOTIFICATION_ACCESS = "android.app.role.NOTIFICATION_ACCESS" + ROLE_FOREGROUND_SERVICE = "android.app.role.FOREGROUND_SERVICE" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + # 模拟桌面角色持有者 (key=角色名, value=持有者包名列表) + self._role_holders: Dict[str, List[str]] = { + self.ROLE_HOME: ["com.example.launcher"], + self.ROLE_BROWSER: ["org.mozilla.firefox"], + self.ROLE_DIALER: ["com.google.android.dialer"], + self.ROLE_SMS: ["com.google.android.messaging"], + } + self._listeners: List[Callable] = [] + + def isRoleAvailable(self, roleName: str) -> bool: + """检查角色是否可用""" + available_roles = [ + self.ROLE_ASSISTANT, self.ROLE_BROWSER, self.ROLE_DIALER, + self.ROLE_SMS, self.ROLE_HOME, self.ROLE_EMERGENCY, + self.ROLE_CALL_REDIRECTION, self.ROLE_SYSTEM_EXCLUSIVE, + self.ROLE_WALLPAPER, self.ROLE_NOTIFICATION_ACCESS, + self.ROLE_FOREGROUND_SERVICE, + ] + return roleName in available_roles + + def isRoleHeld(self, roleName: str) -> bool: + """检查当前应用是否持有指定角色""" + if not self._context: + return False + pkg = self._context.getPackageName() if hasattr(self._context, "getPackageName") else "" + holders = self._role_holders.get(roleName, []) + return pkg in holders + + def createRequestRoleIntent(self, roleName: str) -> Any: + """创建请求角色的 Intent""" + from .app import Intent + intent = Intent(self.ACTION_REQUEST_ROLE) + intent.putExtra("android.app.role.extra.ROLE_NAME", roleName) + return intent + + def getRoleHolders(self, roleName: str) -> List[str]: + """获取角色持有者列表""" + return list(self._role_holders.get(roleName, [])) + + def addOnRoleHoldersChangedListener( + self, executor: Any, listener: Callable + ): + """添加角色持有者变更监听器""" + if listener not in self._listeners: + self._listeners.append(listener) + + def removeOnRoleHoldersChangedListener(self, listener: Callable): + """移除角色持有者变更监听器""" + if listener in self._listeners: + self._listeners.remove(listener) + + def getRoleNamesFromController(self) -> List[str]: + """获取控制器管理的角色名列表""" + return [ + self.ROLE_ASSISTANT, self.ROLE_BROWSER, self.ROLE_DIALER, + self.ROLE_SMS, self.ROLE_HOME, self.ROLE_EMERGENCY, + self.ROLE_CALL_REDIRECTION, self.ROLE_SYSTEM_EXCLUSIVE, + self.ROLE_WALLPAPER, self.ROLE_NOTIFICATION_ACCESS, + self.ROLE_FOREGROUND_SERVICE, + ] + + def getApplicationVisibleRoleNames(self) -> List[str]: + """获取应用可见的角色名列表""" + return self.getRoleNamesFromController() + + def toString(self) -> str: + return f"RoleManager@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/app_usage.py b/android_pyapi/android/app_usage.py new file mode 100644 index 0000000..8d6b150 --- /dev/null +++ b/android_pyapi/android/app_usage.py @@ -0,0 +1,527 @@ +""" +Android Usage Stats API — UsageStatsManager, UsageStats, UsageEvents, ConfigurationStats, StorageStatsManager +方法签名完全对齐 Android SDK +""" + +import json +import os as _os +import sys +import time +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional, Set, Tuple, Callable +from collections import defaultdict + +from .._core import Stub, AndroidOnlyError, resolve_android_path, _app_data_dir, _sdcard_root +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "UsageStatsManager", + "UsageStats", + "UsageEvents", + "ConfigurationStats", + "CacheUsageStats", + "NetworkUsageStats", + "UsageStatsManagerInternal", + "StorageStatsManager", + "StorageStats", +] + + +# ═══════════════════════════════════════════════════════════════ +# UsageStatsManager +# ═══════════════════════════════════════════════════════════════ + +class UsageStatsManager(Stub): + """UsageStatsManager — 使用统计管理器 (桌面模拟)""" + + INTERVAL_DAILY = 0 + INTERVAL_WEEKLY = 1 + INTERVAL_MONTHLY = 2 + INTERVAL_YEARLY = 3 + INTERVAL_BEST = 4 + INTERVAL_COUNT = 5 + + USAGE_SOURCE_UNSPECIFIED = 0 + USAGE_SOURCE_CURRENT_ACTIVITY = 1 + USAGE_SOURCE_TASK_ROOT_ACTIVITY = 2 + + def __init__(self): + super().__init__() + self._app_inactive: Dict[str, bool] = {} + + def queryUsageStats( + self, intervalType: int, beginTime: int, endTime: int + ) -> List["UsageStats"]: + """查询使用统计信息""" + result: List[UsageStats] = [] + stats_dir = _os.path.join(_app_data_dir, "usagestats") + if _os.path.exists(stats_dir): + for fname in _os.listdir(stats_dir): + if fname.endswith(".json"): + filepath = _os.path.join(stats_dir, fname) + try: + with open(filepath, "r") as f: + data = json.load(f) + stats = UsageStats() + stats._package_name = data.get("package", fname[:-5]) + stats._first_time = data.get("first_time", beginTime) + stats._last_time = data.get("last_time", endTime) + stats._last_used = data.get("last_used", beginTime) + stats._total_foreground = data.get("total_foreground", 0) + stats._launch_count = data.get("launch_count", 0) + if stats._last_time >= beginTime and stats._last_time <= endTime: + result.append(stats) + except (json.JSONDecodeError, IOError): + pass + return result + + def queryConfigurations( + self, intervalType: int, beginTime: int, endTime: int + ) -> List["ConfigurationStats"]: + """查询配置统计""" + return [] + + def queryEvents( + self, beginTime: int, endTime: int + ) -> "UsageEvents": + """查询使用事件""" + return UsageEvents() + + def isAppInactive(self, packageName: str) -> bool: + """检查应用是否处于非活跃状态""" + return self._app_inactive.get(packageName, False) + + def setAppInactive(self, packageName: str, inactive: bool): + """设置应用非活跃状态""" + self._app_inactive[packageName] = inactive + + def onCarrierPrivilegedAppsChanged(self): + """运营商特权应用变更 (桌面存根)""" + pass + + def toString(self) -> str: + return f"UsageStatsManager@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# UsageStats +# ═══════════════════════════════════════════════════════════════ + +class UsageStats(Stub): + """UsageStats — 应用使用统计""" + + def __init__(self): + super().__init__() + self._package_name = "" + self._first_time = 0 + self._last_time = 0 + self._last_used = 0 + self._total_foreground = 0 + self._launch_count = 0 + + def getPackageName(self) -> str: + return self._package_name + + def getFirstTimeStamp(self) -> int: + return self._first_time + + def getLastTimeStamp(self) -> int: + return self._last_time + + def getLastUsedTime(self) -> int: + return self._last_used + + def getTotalTimeInForeground(self) -> int: + return self._total_foreground + + def getAppLaunchCount(self) -> int: + return self._launch_count + + def toString(self) -> str: + return (f"UsageStats(pkg={self._package_name}, " + f"foreground={self._total_foreground}ms, " + f"launches={self._launch_count})") + + def equals(self, other) -> bool: + if not isinstance(other, UsageStats): + return False + return self._package_name == other._package_name + + def hashCode(self) -> int: + return hash(self._package_name) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# UsageEvents +# ═══════════════════════════════════════════════════════════════ + +class UsageEvents(Stub): + """UsageEvents — 使用事件列表""" + + def __init__(self): + super().__init__() + self._events: List["UsageEvents.Event"] = [] + self._index = 0 + + def getNextEvent(self, eventOut: "UsageEvents.Event") -> bool: + """获取下一个事件""" + if self._index < len(self._events): + ev = self._events[self._index] + eventOut._package_name = ev._package_name + eventOut._class_name = ev._class_name + eventOut._action = ev._action + eventOut._time_stamp = ev._time_stamp + self._index += 1 + return True + return False + + def hasNextEvent(self) -> bool: + """检查是否还有下一个事件""" + return self._index < len(self._events) + + def resetToStart(self): + """重置到起始位置""" + self._index = 0 + + # ═══════════════════════════════════════════════════════════ + # Event — 内部事件类 + # ═══════════════════════════════════════════════════════════ + + class Event(Stub): + """UsageEvents.Event — 使用事件""" + + # --- Event action constants --- + MOVE_TO_FOREGROUND = 1 + MOVE_TO_BACKGROUND = 2 + CONFIGURATION_CHANGE = 3 + SHORTCUT_INVOCATION = 4 + STANDBY_BUDGET_CHANGED = 5 + USER_INTERACTION = 6 + NOTIFICATION_SEEN = 7 + NOTIFICATION_INTERRUPTION = 8 + CHOOSER_ACTION = 9 + APP_COMPONENT_USED = 10 + ACTIVITY_STOPPED = 11 + SYSTEM_INTERACTION = 12 + FLAG_IS_PACKAGE_INSTALL = 13 + FLAG_NO_INTERRUPTION = 14 + + def __init__(self): + super().__init__() + self._package_name = "" + self._class_name = "" + self._action = 0 + self._time_stamp = 0 + + def getPackageName(self) -> str: + return self._package_name + + def getClassName(self) -> str: + return self._class_name + + def getAction(self) -> int: + return self._action + + def getTimeStamp(self) -> int: + return self._time_stamp + + def toString(self) -> str: + return (f"Event(pkg={self._package_name}, " + f"action={self._action}, time={self._time_stamp})") + + def equals(self, other) -> bool: + if not isinstance(other, UsageEvents.Event): + return False + return (self._package_name == other._package_name + and self._action == other._action + and self._time_stamp == other._time_stamp) + + def hashCode(self) -> int: + return hash((self._package_name, self._action, self._time_stamp)) & 0x7FFFFFFF + + def toString(self) -> str: + return f"UsageEvents(count={len(self._events)})" + + def equals(self, other) -> bool: + if not isinstance(other, UsageEvents): + return False + return self._events == other._events + + def hashCode(self) -> int: + return hash(len(self._events)) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# ConfigurationStats +# ═══════════════════════════════════════════════════════════════ + +class ConfigurationStats(Stub): + """ConfigurationStats — 配置统计""" + + def __init__(self): + super().__init__() + self._first_time = 0 + self._last_time = 0 + self._last_active = 0 + self._total_active = 0 + self._activation_count = 0 + + def getFirstTimeStamp(self) -> int: + return self._first_time + + def getLastTimeStamp(self) -> int: + return self._last_time + + def getLastTimeActive(self) -> int: + return self._last_active + + def getTotalTimeActive(self) -> int: + return self._total_active + + def getActivationCount(self) -> int: + return self._activation_count + + def toString(self) -> str: + return f"ConfigurationStats(active={self._total_active}ms, count={self._activation_count})" + + def equals(self, other) -> bool: + if not isinstance(other, ConfigurationStats): + return False + return (self._first_time == other._first_time + and self._last_time == other._last_time) + + def hashCode(self) -> int: + return hash((self._first_time, self._last_time)) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# CacheUsageStats +# ═══════════════════════════════════════════════════════════════ + +class CacheUsageStats(Stub): + """CacheUsageStats — 缓存使用统计""" + + def __init__(self): + super().__init__() + self._cache_size = 0 + self._total_size = 0 + + def getCacheSize(self) -> int: + return self._cache_size + + def getTotalSize(self) -> int: + return self._total_size + + def toString(self) -> str: + return f"CacheUsageStats(cache={self._cache_size}B)" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# NetworkUsageStats +# ═══════════════════════════════════════════════════════════════ + +class NetworkUsageStats(Stub): + """NetworkUsageStats — 网络使用统计""" + + def __init__(self): + super().__init__() + self._rx_bytes = 0 + self._tx_bytes = 0 + self._rx_packets = 0 + self._tx_packets = 0 + + def getRxBytes(self) -> int: + return self._rx_bytes + + def getTxBytes(self) -> int: + return self._tx_bytes + + def getRxPackets(self) -> int: + return self._rx_packets + + def getTxPackets(self) -> int: + return self._tx_packets + + def toString(self) -> str: + return f"NetworkUsageStats(rx={self._rx_bytes}B, tx={self._tx_bytes}B)" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# UsageStatsManagerInternal +# ═══════════════════════════════════════════════════════════════ + +class UsageStatsManagerInternal(Stub): + """UsageStatsManagerInternal — 内部使用统计管理器""" + + def __init__(self): + super().__init__() + + def reportEvent(self, event: Any): + """报告使用事件""" + pass + + def toString(self) -> str: + return f"UsageStatsManagerInternal@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# StorageStatsManager +# ═══════════════════════════════════════════════════════════════ + +class StorageStatsManager(Stub): + """StorageStatsManager — 存储统计管理器""" + + def __init__(self): + super().__init__() + + def queryStatsForPackage( + self, storageUuid: Any, packageName: str, userHandle: Any + ) -> "StorageStats": + """查询指定应用存储统计""" + return self._query_stats(storageUuid, packageName) + + def queryStatsForUid( + self, storageUuid: Any, uid: int + ) -> "StorageStats": + """查询指定 UID 存储统计""" + return self._query_stats(storageUuid, f"uid_{uid}") + + def queryStatsForUser( + self, storageUuid: Any, userHandle: Any + ) -> "StorageStats": + """查询指定用户存储统计""" + return self._query_stats(storageUuid, f"user_{userHandle}") + + def queryExternalStatsForUser( + self, storageUuid: Any, userHandle: Any + ) -> "StorageStats": + """查询外部用户存储统计""" + return self._query_stats(storageUuid, f"external_user_{userHandle}") + + def getFreeBytes(self, storageUuid: Any) -> int: + """获取空闲字节数""" + try: + st = _os.statvfs(_sdcard_root) + return st.f_bavail * st.f_frsize + except (OSError, AttributeError): + return 1024 * 1024 * 1024 # 默认 1GB + + def getTotalBytes(self, storageUuid: Any) -> int: + """获取总字节数""" + try: + st = _os.statvfs(_sdcard_root) + return st.f_blocks * st.f_frsize + except (OSError, AttributeError): + return 64 * 1024 * 1024 * 1024 # 默认 64GB + + def _query_stats(self, storage_uuid: Any, tag: str) -> "StorageStats": + """内部查询存储统计""" + stats = StorageStats() + pkg_dir = _os.path.join(_app_data_dir, "files") + if _os.path.exists(pkg_dir): + for dirpath, dirnames, filenames in _os.walk(pkg_dir): + for fname in filenames: + fpath = _os.path.join(dirpath, fname) + try: + fsize = _os.path.getsize(fpath) + ext = _os.path.splitext(fname)[1].lower() + if ext in (".jpg", ".png", ".gif", ".webp", ".bmp"): + stats._image_bytes += fsize + elif ext in (".mp3", ".wav", ".flac", ".ogg", ".aac"): + stats._music_bytes += fsize + elif ext in (".mp4", ".avi", ".mkv", ".mov", ".webm"): + stats._video_bytes += fsize + else: + stats._data_bytes += fsize + except OSError: + pass + return stats + + def toString(self) -> str: + return f"StorageStatsManager@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# StorageStats +# ═══════════════════════════════════════════════════════════════ + +class StorageStats(Stub): + """StorageStats — 存储统计""" + + def __init__(self): + super().__init__() + self._app_bytes = 0 + self._data_bytes = 0 + self._cache_bytes = 0 + self._music_bytes = 0 + self._video_bytes = 0 + self._image_bytes = 0 + self._game_bytes = 0 + + def getAppBytes(self) -> int: + return self._app_bytes + + def getDataBytes(self) -> int: + return self._data_bytes + + def getCacheBytes(self) -> int: + return self._cache_bytes + + def getMusicBytes(self) -> int: + return self._music_bytes + + def getVideoBytes(self) -> int: + return self._video_bytes + + def getImageBytes(self) -> int: + return self._image_bytes + + def getGameBytes(self) -> int: + return self._game_bytes + + def toString(self) -> str: + return (f"StorageStats(app={self._app_bytes}B, data={self._data_bytes}B, " + f"cache={self._cache_bytes}B, music={self._music_bytes}B, " + f"video={self._video_bytes}B, image={self._image_bytes}B)") + + def equals(self, other) -> bool: + if not isinstance(other, StorageStats): + return False + return (self._app_bytes == other._app_bytes + and self._data_bytes == other._data_bytes) + + def hashCode(self) -> int: + return hash((self._app_bytes, self._data_bytes)) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/appwidget.py b/android_pyapi/android/appwidget.py new file mode 100644 index 0000000..91fda3e --- /dev/null +++ b/android_pyapi/android/appwidget.py @@ -0,0 +1,360 @@ +""" +Android App Widget API — AppWidgetProvider, AppWidgetManager, AppWidgetProviderInfo, AppWidgetHost, AppWidgetHostView +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "AppWidgetProvider", + "AppWidgetManager", + "AppWidgetProviderInfo", + "AppWidgetHost", + "AppWidgetHostView", +] + + +# ═══════════════════════════════════════════════════════════════ +# AppWidgetProvider +# ═══════════════════════════════════════════════════════════════ + +class AppWidgetProvider(Stub): + """AppWidgetProvider — 应用小部件提供者""" + + def __init__(self): + super().__init__() + + def onUpdate(self, context: Any, appWidgetManager: "AppWidgetManager", appWidgetIds: List[int]): + """小部件更新回调""" + pass + + def onDeleted(self, context: Any, appWidgetIds: List[int]): + """小部件删除回调""" + pass + + def onEnabled(self, context: Any): + """首个小部件启用回调""" + pass + + def onDisabled(self, context: Any): + """最后一个小部件禁用回调""" + pass + + def onAppWidgetOptionsChanged( + self, context: Any, appWidgetManager: "AppWidgetManager", + appWidgetId: int, newOptions: Dict[str, Any] + ): + """小部件选项变更回调""" + pass + + def onReceive(self, context: Any, intent: Any): + """接收广播回调""" + pass + + def onRestored(self, context: Any, oldWidgetIds: List[int], newWidgetId: int): + """小部件恢复回调""" + pass + + def onProviderChanged( + self, context: Any, appWidgetManager: "AppWidgetManager", + appWidgetId: int, newInfo: "AppWidgetProviderInfo" + ): + """提供者变更回调""" + pass + + def toString(self) -> str: + return f"AppWidgetProvider@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# AppWidgetManager +# ═══════════════════════════════════════════════════════════════ + +class AppWidgetManager(Stub): + """AppWidgetManager — 应用小部件管理器 (桌面存根)""" + + ACTION_APPWIDGET_UPDATE = "android.appwidget.action.APPWIDGET_UPDATE" + ACTION_APPWIDGET_DELETED = "android.appwidget.action.APPWIDGET_DELETED" + ACTION_APPWIDGET_DISABLED = "android.appwidget.action.APPWIDGET_DISABLED" + ACTION_APPWIDGET_ENABLED = "android.appwidget.action.APPWIDGET_ENABLED" + ACTION_APPWIDGET_OPTIONS_CHANGED = "android.appwidget.action.APPWIDGET_OPTIONS_CHANGED" + ACTION_APPWIDGET_RESTORED = "android.appwidget.action.APPWIDGET_RESTORED" + ACTION_APPWIDGET_HOST_RESTORED = "android.appwidget.action.APPWIDGET_HOST_RESTORED" + EXTRA_APPWIDGET_ID = "appWidgetId" + EXTRA_APPWIDGET_IDS = "appWidgetIds" + EXTRA_APPWIDGET_OLD_IDS = "appWidgetOldIds" + EXTRA_APPWIDGET_OPTIONS = "appWidgetOptions" + EXTRA_APPWIDGET_PROVIDER = "appWidgetProvider" + EXTRA_APPWIDGET_PROVIDER_PROFILE = "appWidgetProviderProfile" + WIDGET_CATEGORY_HOME_SCREEN = 1 + WIDGET_CATEGORY_KEYGUARD = 2 + WIDGET_CATEGORY_SEARCHBOX = 4 + OPTION_APPWIDGET_MIN_WIDTH = "appWidgetMinWidth" + OPTION_APPWIDGET_MIN_HEIGHT = "appWidgetMinHeight" + OPTION_APPWIDGET_MAX_WIDTH = "appWidgetMaxWidth" + OPTION_APPWIDGET_MAX_HEIGHT = "appWidgetMaxHeight" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._widget_views: Dict[int, Any] = {} + self._widget_options: Dict[int, Dict[str, Any]] = {} + self._providers: List["AppWidgetProviderInfo"] = [] + self._next_widget_id = 1 + + @staticmethod + def getInstance(context: Any) -> "AppWidgetManager": + """获取 AppWidgetManager 实例""" + return AppWidgetManager(context) + + def getAppWidgetIds(self, info: "AppWidgetProviderInfo") -> List[int]: + """获取指定提供者的小部件 ID 列表""" + return list(self._widget_views.keys()) + + def updateAppWidget(self, appWidgetId: int, view: Any): + """更新小部件视图""" + self._widget_views[appWidgetId] = view + + def updateAppWidgetOptions(self, appWidgetId: int, options: Dict[str, Any]): + """更新小部件选项""" + if appWidgetId in self._widget_options: + self._widget_options[appWidgetId].update(options) + else: + self._widget_options[appWidgetId] = dict(options) + + def getAppWidgetOptions(self, appWidgetId: int) -> Dict[str, Any]: + """获取小部件选项""" + return self._widget_options.get(appWidgetId, {}) + + def bindAppWidgetIdIfAllowed( + self, appWidgetId: int, info: "AppWidgetProviderInfo" + ) -> bool: + """绑定小部件 ID(如允许)""" + return True + + def getAppWidgetInfo(self, appWidgetId: int) -> Optional["AppWidgetProviderInfo"]: + """获取小部件信息""" + if appWidgetId in self._widget_views: + return self._providers[0] if self._providers else None + return None + + def getInstalledProviders(self) -> List["AppWidgetProviderInfo"]: + """获取已安装的提供者列表""" + return list(self._providers) + + def isBindAppWidgetIdAllowed(self, appWidgetId: int, info: "AppWidgetProviderInfo") -> bool: + """检查是否允许绑定小部件 ID""" + return True + + def createAppWidgetConfigIntentSender( + self, info: "AppWidgetProviderInfo", appWidgetId: int + ) -> Optional[Any]: + """创建小部件配置 IntentSender""" + return None + + def requestPinAppWidget( + self, info: "AppWidgetProviderInfo", successCallback: Any = None + ) -> bool: + """请求固定小部件""" + return True + + def getAppWidgetHostViews(self, host: "AppWidgetHost") -> List["AppWidgetHostView"]: + """获取主机视图列表""" + return [] + + def toString(self) -> str: + return f"AppWidgetManager(widgets={len(self._widget_views)})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# AppWidgetProviderInfo +# ═══════════════════════════════════════════════════════════════ + +class AppWidgetProviderInfo(Stub): + """AppWidgetProviderInfo — 小部件提供者信息""" + + # --- ResizeMode --- + RESIZE_MODE_NONE = 0 + RESIZE_MODE_HORIZONTAL = 1 + RESIZE_MODE_VERTICAL = 2 + RESIZE_MODE_RESIZE_ALL = 3 + + # --- WidgetCategory --- + WIDGET_CATEGORY_HOME_SCREEN = 1 + WIDGET_CATEGORY_KEYGUARD = 2 + WIDGET_CATEGORY_SEARCHBOX = 4 + + # --- WidgetFeatures --- + WIDGET_FEATURE_HIDE_FROM_PICKER = 1 + WIDGET_FEATURE_CONFIGURATION_OPTIONAL = 2 + WIDGET_FEATURE_RECONFIGURABLE = 4 + + def __init__(self): + super().__init__() + self.provider: Any = None + self.configure: Any = None + self.icon: int = 0 + self.label: str = "" + self.previewImage: int = 0 + self.previewLayout: int = 0 + self.autoAdvanceViewId: int = -1 + self.resizeMode: int = self.RESIZE_MODE_NONE + self.widgetCategory: int = self.WIDGET_CATEGORY_HOME_SCREEN + self.widgetFeatures: int = 0 + self.description: str = "" + self.targetCellWidth: int = 0 + self.targetCellHeight: int = 0 + self.minWidth: int = 0 + self.minHeight: int = 0 + self.maxWidth: int = 0 + self.maxHeight: int = 0 + self.minResizeWidth: int = 0 + self.minResizeHeight: int = 0 + self.updatePeriodMillis: int = 0 + self.initialLayout: int = 0 + + def getProfile(self) -> Optional[Any]: + """获取配置文件""" + return None + + def getActivityInfo(self) -> Optional[Any]: + """获取 Activity 信息""" + return None + + def getIcon(self) -> int: + return self.icon + + def getLabel(self) -> str: + return self.label + + def loadPreviewImage(self, context: Any, density: int) -> Any: + """加载预览图像""" + return None + + def toString(self) -> str: + return f"AppWidgetProviderInfo(label={self.label}, icon={self.icon})" + + def equals(self, other) -> bool: + if not isinstance(other, AppWidgetProviderInfo): + return False + return self.provider == other.provider and self.label == other.label + + def hashCode(self) -> int: + return hash((str(self.provider), self.label)) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# AppWidgetHost +# ═══════════════════════════════════════════════════════════════ + +class AppWidgetHost(Stub): + """AppWidgetHost — 小部件主机""" + + def __init__(self, context: Any, hostId: int): + super().__init__() + self._context = context + self._host_id = hostId + self._widget_ids: List[int] = [] + self._listening = False + + def createView( + self, context: Any, appWidgetId: int, info: "AppWidgetProviderInfo" + ) -> "AppWidgetHostView": + """创建小部件视图""" + view = AppWidgetHostView(context) + view.setAppWidget(appWidgetId, info) + return view + + def startListening(self): + """开始监听""" + self._listening = True + + def stopListening(self): + """停止监听""" + self._listening = False + + def allocateAppWidgetId(self) -> int: + """分配小部件 ID""" + widget_id = len(self._widget_ids) + 1000 + self._host_id + self._widget_ids.append(widget_id) + return widget_id + + def deleteAppWidgetId(self, appWidgetId: int): + """删除小部件 ID""" + if appWidgetId in self._widget_ids: + self._widget_ids.remove(appWidgetId) + + def deleteHost(self): + """删除主机""" + self._widget_ids.clear() + + def clearViews(self): + """清除所有视图""" + self._widget_ids.clear() + + def toString(self) -> str: + return f"AppWidgetHost(hostId={self._host_id}, widgets={len(self._widget_ids)})" + + def equals(self, other) -> bool: + if not isinstance(other, AppWidgetHost): + return False + return self._host_id == other._host_id + + def hashCode(self) -> int: + return self._host_id & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# AppWidgetHostView +# ═══════════════════════════════════════════════════════════════ + +class AppWidgetHostView(Stub): + """AppWidgetHostView — 小部件主机视图""" + + def __init__(self, context: Any): + super().__init__() + self._context = context + self._app_widget_id = -1 + self._info: Optional["AppWidgetProviderInfo"] = None + + def setAppWidget(self, appWidgetId: int, info: "AppWidgetProviderInfo"): + """设置小部件""" + self._app_widget_id = appWidgetId + self._info = info + + def updateAppWidget(self, info: "AppWidgetProviderInfo"): + """更新小部件""" + self._info = info + + def getAppWidgetInfo(self) -> Optional["AppWidgetProviderInfo"]: + return self._info + + def toString(self) -> str: + return f"AppWidgetHostView(widgetId={self._app_widget_id})" + + def equals(self, other) -> bool: + if not isinstance(other, AppWidgetHostView): + return False + return self._app_widget_id == other._app_widget_id + + def hashCode(self) -> int: + return self._app_widget_id & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/companion.py b/android_pyapi/android/companion.py new file mode 100644 index 0000000..569f8a3 --- /dev/null +++ b/android_pyapi/android/companion.py @@ -0,0 +1,415 @@ +""" +Android Companion Device API — CompanionDeviceManager, AssociationInfo, DeviceFilters +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "CompanionDeviceManager", + "AssociationInfo", + "WifiDeviceFilter", + "BluetoothDeviceFilter", + "BluetoothLeDeviceFilter", + "CommsDevice", + "DeviceActivityCallback", + "CompanionService", +] + + +# ═══════════════════════════════════════════════════════════════ +# CompanionDeviceManager +# ═══════════════════════════════════════════════════════════════ + +class CompanionDeviceManager(Stub): + """CompanionDeviceManager — 配套设备管理器 (桌面存根)""" + + EXTRA_DEVICE = "android.companion.extra.DEVICE" + EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._associations: Dict[str, "AssociationInfo"] = {} + self._callbacks: List[Callable] = [] + + def associate( + self, + request: Any, + callback: "CompanionDeviceManager.Callback", + handler: Any = None, + ): + """发起设备关联请求""" + raise AndroidOnlyError("CompanionDeviceManager.associate() 仅在 Android 设备上可用") + + def associateCallback( + self, + request: Any, + callback: Callable, + executor: Any = None, + ): + """使用回调方式发起设备关联""" + raise AndroidOnlyError("CompanionDeviceManager.associateCallback() 仅在 Android 设备上可用") + + def requestNotificationAccess(self, pairingDevice: "AssociationInfo"): + """请求通知访问权限""" + raise AndroidOnlyError("CompanionDeviceManager.requestNotificationAccess() 仅在 Android 设备上可用") + + def requestSystemAppStore(self, pairingDevice: "AssociationInfo"): + """请求系统应用商店""" + pass + + def startObservingDevicePresence(self, deviceAddress: str): + """开始监听设备存在""" + pass + + def stopObservingDevicePresence(self, deviceAddress: str): + """停止监听设备存在""" + pass + + def isDeviceAssociated(self, deviceAddress: str) -> bool: + """检查设备是否已关联""" + return deviceAddress in self._associations + + def getDeviceId(self, deviceAddress: str) -> Optional[str]: + """获取设备 ID""" + info = self._associations.get(deviceAddress) + return info.getSelfManagedDeviceId() if info else None + + def getAssociations(self) -> List["AssociationInfo"]: + """获取所有关联信息""" + return list(self._associations.values()) + + def removeAssociation(self, deviceAddress: str): + """移除设备关联""" + self._associations.pop(deviceAddress, None) + + # ═══════════════════════════════════════════════════════════ + # Callback - 回调类 + # ═══════════════════════════════════════════════════════════ + + class Callback(Stub): + """CompanionDeviceManager.Callback — 设备发现回调""" + + def __init__(self): + super().__init__() + + def onDeviceFound(self, intent: Any): + """设备找到回调""" + pass + + def onFailure(self, error: str): + """设备发现失败回调""" + pass + + def toString(self) -> str: + return f"Callback@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def toString(self) -> str: + return f"CompanionDeviceManager(associations={len(self._associations)})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# AssociationInfo +# ═══════════════════════════════════════════════════════════════ + +class AssociationInfo(Stub): + """AssociationInfo — 设备关联信息""" + + def __init__( + self, + userId: int = 0, + deviceProfile: str = "", + selfManagedDeviceId: str = "", + associatedDevice: Any = None, + isSelfManaged: bool = False, + isNotAssociated: bool = False, + ): + super().__init__() + self._user_id = userId + self._device_profile = deviceProfile + self._self_managed_device_id = selfManagedDeviceId + self._associated_device = associatedDevice + self._self_managed = isSelfManaged + self._not_associated = isNotAssociated + + def getUserId(self) -> int: + return self._user_id + + def getDeviceProfile(self) -> str: + return self._device_profile + + def getSelfManagedDeviceId(self) -> str: + return self._self_managed_device_id + + def getAssociatedDevice(self) -> Optional[Any]: + return self._associated_device + + def isSelfManaged(self) -> bool: + return self._self_managed + + def isNotAssociated(self) -> bool: + return self._not_associated + + def toString(self) -> str: + return (f"AssociationInfo(deviceId={self._self_managed_device_id}, " + f"profile={self._device_profile})") + + def equals(self, other) -> bool: + if not isinstance(other, AssociationInfo): + return False + return self._self_managed_device_id == other._self_managed_device_id + + def hashCode(self) -> int: + return hash(self._self_managed_device_id) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# WifiDeviceFilter +# ═══════════════════════════════════════════════════════════════ + +class WifiDeviceFilter(Stub): + """WifiDeviceFilter — WiFi 设备过滤器""" + + def __init__(self, namePattern: str = ""): + super().__init__() + self._name_pattern = namePattern + + def getNamePattern(self) -> str: + return self._name_pattern + + def toString(self) -> str: + return f"WifiDeviceFilter(pattern={self._name_pattern})" + + def equals(self, other) -> bool: + if not isinstance(other, WifiDeviceFilter): + return False + return self._name_pattern == other._name_pattern + + def hashCode(self) -> int: + return hash(self._name_pattern) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothDeviceFilter +# ═══════════════════════════════════════════════════════════════ + +class BluetoothDeviceFilter(Stub): + """BluetoothDeviceFilter — 蓝牙设备过滤器""" + + class Builder(Stub): + """BluetoothDeviceFilter.Builder — 蓝牙过滤器构建器""" + + def __init__(self): + super().__init__() + self._name_pattern = "" + self._address = "" + + def setNamePattern(self, pattern: str) -> "BluetoothDeviceFilter.Builder": + self._name_pattern = pattern + return self + + def setAddress(self, address: str) -> "BluetoothDeviceFilter.Builder": + self._address = address + return self + + def build(self) -> "BluetoothDeviceFilter": + return BluetoothDeviceFilter( + namePattern=self._name_pattern, + address=self._address, + ) + + def toString(self) -> str: + return f"Builder(pattern={self._name_pattern})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def __init__(self, namePattern: str = "", address: str = ""): + super().__init__() + self._name_pattern = namePattern + self._address = address + + def getNamePattern(self) -> str: + return self._name_pattern + + def getAddress(self) -> str: + return self._address + + def toString(self) -> str: + return f"BluetoothDeviceFilter(address={self._address})" + + def equals(self, other) -> bool: + if not isinstance(other, BluetoothDeviceFilter): + return False + return self._address == other._address + + def hashCode(self) -> int: + return hash(self._address) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothLeDeviceFilter +# ═══════════════════════════════════════════════════════════════ + +class BluetoothLeDeviceFilter(Stub): + """BluetoothLeDeviceFilter — BLE 设备过滤器""" + + class Builder(Stub): + """BluetoothLeDeviceFilter.Builder — BLE 过滤器构建器""" + + def __init__(self): + super().__init__() + self._name_pattern = "" + self._scan_filter = None + + def setNamePattern(self, pattern: str) -> "BluetoothLeDeviceFilter.Builder": + self._name_pattern = pattern + return self + + def setScanFilter(self, scanFilter: Any) -> "BluetoothLeDeviceFilter.Builder": + self._scan_filter = scanFilter + return self + + def build(self) -> "BluetoothLeDeviceFilter": + return BluetoothLeDeviceFilter( + namePattern=self._name_pattern, + scanFilter=self._scan_filter, + ) + + def toString(self) -> str: + return f"BLEBuilder(pattern={self._name_pattern})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def __init__(self, namePattern: str = "", scanFilter: Any = None): + super().__init__() + self._name_pattern = namePattern + self._scan_filter = scanFilter + + def getNamePattern(self) -> str: + return self._name_pattern + + def getScanFilter(self) -> Optional[Any]: + return self._scan_filter + + def toString(self) -> str: + return f"BluetoothLeDeviceFilter(pattern={self._name_pattern})" + + def equals(self, other) -> bool: + if not isinstance(other, BluetoothLeDeviceFilter): + return False + return self._name_pattern == other._name_pattern + + def hashCode(self) -> int: + return hash(self._name_pattern) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# CommsDevice +# ═══════════════════════════════════════════════════════════════ + +class CommsDevice(Stub): + """CommsDevice — 通信设备信息""" + + def __init__(self, deviceId: str = "", displayName: str = ""): + super().__init__() + self._device_id = deviceId + self._display_name = displayName + + def getDeviceId(self) -> str: + return self._device_id + + def getDisplayName(self) -> str: + return self._display_name + + def toString(self) -> str: + return f"CommsDevice(id={self._device_id}, name={self._display_name})" + + def equals(self, other) -> bool: + if not isinstance(other, CommsDevice): + return False + return self._device_id == other._device_id + + def hashCode(self) -> int: + return hash(self._device_id) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# DeviceActivityCallback +# ═══════════════════════════════════════════════════════════════ + +class DeviceActivityCallback(Stub): + """DeviceActivityCallback — 设备活动回调""" + + def __init__(self): + super().__init__() + + def onDeviceActivityChanged(self, associationInfo: "AssociationInfo", active: bool): + """设备活动状态变更回调""" + pass + + def toString(self) -> str: + return f"DeviceActivityCallback@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# CompanionService +# ═══════════════════════════════════════════════════════════════ + +class CompanionService(Stub): + """CompanionService — 配套服务""" + + SERVICE_INTERFACE = "android.companion.CompanionDeviceService" + + def __init__(self): + super().__init__() + + def onConnected(self, deviceInfo: Any): + """已连接回调""" + pass + + def onDisconnected(self, deviceInfo: Any): + """已断开回调""" + pass + + def toString(self) -> str: + return f"CompanionService@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/credentials.py b/android_pyapi/android/credentials.py new file mode 100644 index 0000000..8445bba --- /dev/null +++ b/android_pyapi/android/credentials.py @@ -0,0 +1,418 @@ +""" +Android Credentials API — CredentialManager, CreateCredentialRequest, GetCredentialRequest 等 +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "CredentialManager", + "CreateCredentialRequest", + "CreateCredentialResponse", + "GetCredentialRequest", + "GetCredentialResponse", + "CredentialDescription", + "CredentialManagerState", + "PrepareGetCredentialResponse", +] + + +# ═══════════════════════════════════════════════════════════════ +# CredentialManager +# ═══════════════════════════════════════════════════════════════ + +class CredentialManager(Stub): + """CredentialManager — 凭据管理器 (桌面存根)""" + + ACTION_CREATE_CREDENTIAL = "android.credentials.action.CREATE_CREDENTIAL" + ACTION_GET_CREDENTIAL = "android.credentials.action.GET_CREDENTIAL" + ACTION_CLEAR_CREDENTIAL = "android.credentials.action.CLEAR_CREDENTIAL" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._descriptions: List["CredentialDescription"] = [] + + def createCredential( + self, + request: "CreateCredentialRequest", + cancellationSignal: Any = None, + executor: Any = None, + callback: Callable = None, + ): + """创建凭据""" + raise AndroidOnlyError("CredentialManager.createCredential() 仅在 Android 设备上可用") + + def getCredential( + self, + request: "GetCredentialRequest", + cancellationSignal: Any = None, + executor: Any = None, + callback: Callable = None, + ): + """获取凭据""" + raise AndroidOnlyError("CredentialManager.getCredential() 仅在 Android 设备上可用") + + def clearCredentialState( + self, + request: Any = None, + cancellationSignal: Any = None, + executor: Any = None, + callback: Callable = None, + ): + """清除凭据状态""" + raise AndroidOnlyError("CredentialManager.clearCredentialState() 仅在 Android 设备上可用") + + def registerCredentialDescription( + self, + credentialDescription: "CredentialDescription", + cancellationSignal: Any = None, + executor: Any = None, + callback: Callable = None, + ): + """注册凭据描述""" + if credentialDescription not in self._descriptions: + self._descriptions.append(credentialDescription) + + def unregisterCredentialDescription( + self, + credentialDescription: "CredentialDescription", + ): + """注销凭据描述""" + if credentialDescription in self._descriptions: + self._descriptions.remove(credentialDescription) + + def getCredentialManagerState(self) -> "CredentialManagerState": + """获取凭据管理器状态""" + return CredentialManagerState() + + def toString(self) -> str: + return f"CredentialManager@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# CreateCredentialRequest +# ═══════════════════════════════════════════════════════════════ + +class CreateCredentialRequest(Stub): + """CreateCredentialRequest — 创建凭据请求""" + + class Builder(Stub): + """CreateCredentialRequest.Builder — 创建凭据请求构建器""" + + def __init__(self, credentialType: str = ""): + super().__init__() + self._credential_type = credentialType + self._credential_data = None + self._candidate_query_data = None + self._always_send_app_uri = False + self._origin = "" + + def setCredentialData(self, data: Any) -> "CreateCredentialRequest.Builder": + self._credential_data = data + return self + + def setCandidateQueryData( + self, data: Any + ) -> "CreateCredentialRequest.Builder": + self._candidate_query_data = data + return self + + def setAlwaysSendAppUri( + self, alwaysSend: bool + ) -> "CreateCredentialRequest.Builder": + self._always_send_app_uri = alwaysSend + return self + + def setOrigin(self, origin: str) -> "CreateCredentialRequest.Builder": + self._origin = origin + return self + + def build(self) -> "CreateCredentialRequest": + req = CreateCredentialRequest.__new__(CreateCredentialRequest) + req._credential_type = self._credential_type + req._credential_data = self._credential_data + req._candidate_query_data = self._candidate_query_data + req._always_send_app_uri = self._always_send_app_uri + req._origin = self._origin + return req + + def toString(self) -> str: + return f"CreateCredentialRequest.Builder(type={self._credential_type})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def __init__(self): + super().__init__() + self._credential_type = "" + self._credential_data = None + self._candidate_query_data = None + self._always_send_app_uri = False + self._origin = "" + + def getCredentialData(self) -> Optional[Any]: + return self._credential_data + + def getCredentialType(self) -> str: + return self._credential_type + + def getCandidateQueryData(self) -> Optional[Any]: + return self._candidate_query_data + + def alwaysSendAppUri(self) -> bool: + return self._always_send_app_uri + + def getOrigin(self) -> str: + return self._origin + + def toString(self) -> str: + return f"CreateCredentialRequest(type={self._credential_type})" + + def equals(self, other) -> bool: + if not isinstance(other, CreateCredentialRequest): + return False + return self._credential_type == other._credential_type + + def hashCode(self) -> int: + return hash(self._credential_type) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# CreateCredentialResponse +# ═══════════════════════════════════════════════════════════════ + +class CreateCredentialResponse(Stub): + """CreateCredentialResponse — 创建凭据响应""" + + def __init__(self): + super().__init__() + self._credential_data = None + self._credential_data_result = None + + def getCredentialData(self) -> Optional[Any]: + return self._credential_data + + def getCredentialDataResult(self) -> Optional[Any]: + return self._credential_data_result + + def toString(self) -> str: + return f"CreateCredentialResponse@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# GetCredentialRequest +# ═══════════════════════════════════════════════════════════════ + +class GetCredentialRequest(Stub): + """GetCredentialRequest — 获取凭据请求""" + + class Builder(Stub): + """GetCredentialRequest.Builder — 获取凭据请求构建器""" + + def __init__(self): + super().__init__() + self._credential_data = None + self._origin = "" + self._always_send_app_uri = False + self._candidate_query_data = None + + def setCredentialData( + self, data: Any + ) -> "GetCredentialRequest.Builder": + self._credential_data = data + return self + + def setOrigin(self, origin: str) -> "GetCredentialRequest.Builder": + self._origin = origin + return self + + def setAlwaysSendAppUri( + self, alwaysSend: bool + ) -> "GetCredentialRequest.Builder": + self._always_send_app_uri = alwaysSend + return self + + def setCandidateQueryData( + self, data: Any + ) -> "GetCredentialRequest.Builder": + self._candidate_query_data = data + return self + + def build(self) -> "GetCredentialRequest": + req = GetCredentialRequest.__new__(GetCredentialRequest) + req._credential_data = self._credential_data + req._origin = self._origin + req._always_send_app_uri = self._always_send_app_uri + req._candidate_query_data = self._candidate_query_data + return req + + def toString(self) -> str: + return f"GetCredentialRequest.Builder@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def __init__(self): + super().__init__() + self._credential_data = None + self._origin = "" + self._always_send_app_uri = False + self._candidate_query_data = None + + def getCredentialData(self) -> Optional[Any]: + return self._credential_data + + def getOrigin(self) -> str: + return self._origin + + def alwaysSendAppUri(self) -> bool: + return self._always_send_app_uri + + def getCandidateQueryData(self) -> Optional[Any]: + return self._candidate_query_data + + def toString(self) -> str: + return f"GetCredentialRequest@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# GetCredentialResponse +# ═══════════════════════════════════════════════════════════════ + +class GetCredentialResponse(Stub): + """GetCredentialResponse — 获取凭据响应""" + + def __init__(self): + super().__init__() + self._credential_data_results: List[Any] = [] + + def getCredentialDataResults(self) -> List[Any]: + return list(self._credential_data_results) + + def toString(self) -> str: + return f"GetCredentialResponse(results={len(self._credential_data_results)})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# CredentialDescription +# ═══════════════════════════════════════════════════════════════ + +class CredentialDescription(Stub): + """CredentialDescription — 凭据描述""" + + def __init__(self, credentialType: str = "", origin: str = ""): + super().__init__() + self._credential_type = credentialType + self._origin = origin + + def getCredentialType(self) -> str: + return self._credential_type + + def getOrigin(self) -> str: + return self._origin + + def toString(self) -> str: + return f"CredentialDescription(type={self._credential_type})" + + def equals(self, other) -> bool: + if not isinstance(other, CredentialDescription): + return False + return (self._credential_type == other._credential_type + and self._origin == other._origin) + + def hashCode(self) -> int: + return hash((self._credential_type, self._origin)) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# CredentialManagerState +# ═══════════════════════════════════════════════════════════════ + +class CredentialManagerState(Stub): + """CredentialManagerState — 凭据管理器状态""" + + def __init__(self): + super().__init__() + self._registered_descriptions: List["CredentialDescription"] = [] + self._is_enabled = True + + def getRegisteredCredentialDescriptions(self) -> List["CredentialDescription"]: + return list(self._registered_descriptions) + + def isEnabled(self) -> bool: + return self._is_enabled + + def toString(self) -> str: + return f"CredentialManagerState(enabled={self._is_enabled})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# PrepareGetCredentialResponse +# ═══════════════════════════════════════════════════════════════ + +class PrepareGetCredentialResponse(Stub): + """PrepareGetCredentialResponse — 预备获取凭据响应""" + + def __init__(self): + super().__init__() + self._has_results = False + self._pending_credential = None + + def hasResults(self) -> bool: + return self._has_results + + def getPendingGetCredentialResponse(self) -> Optional["GetCredentialResponse"]: + return self._pending_credential + + def toString(self) -> str: + return f"PrepareGetCredentialResponse(hasResults={self._has_results})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/hardware_display.py b/android_pyapi/android/hardware_display.py new file mode 100644 index 0000000..d199a51 --- /dev/null +++ b/android_pyapi/android/hardware_display.py @@ -0,0 +1,299 @@ +""" +Android hardware.display 组件 — DisplayManager, VirtualDisplay, WifiDisplay 等 +方法签名完全对齐 Android SDK +""" + +import time +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# DisplayManager +# ═══════════════════════════════════════════════════════════════ + +class DisplayManager(Stub): + """DisplayManager — 显示管理器""" + + VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR = 1 + VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = 4 + VIRTUAL_DISPLAY_FLAG_PUBLIC_DISPLAY = 8 + VIRTUAL_DISPLAY_FLAG_PRESENTATION = 2 + VIRTUAL_DISPLAY_FLAG_SECURE = 16 + VIRTUAL_DISPLAY_FLAG_SUPPORTS_HDR = 32 + + MATCH_DEFAULT_DISPLAY = -1 + MATCH_ALL_DISPLAYS = 0 + + # ═══════════════════════════════════════════════════════════ + # DisplayListener + # ═══════════════════════════════════════════════════════════ + + class DisplayListener(Stub): + """DisplayManager.DisplayListener — 显示监听器""" + + def onDisplayAdded(self, displayId: int): + pass + + def onDisplayRemoved(self, displayId: int): + pass + + def onDisplayChanged(self, displayId: int): + pass + + def __init__(self): + super().__init__() + self._displays: Dict[int, Display] = {} + self._listeners: Dict[int, DisplayManager.DisplayListener] = {} + self._next_display_id = 1 + # Create default display + self._displays[0] = Display(0, "Built-in Display") + + def getDisplay(self, displayId: int) -> Optional["Display"]: + return self._displays.get(displayId) + + def getDisplays(self) -> List["Display"]: + if DisplayManager.MATCH_ALL_DISPLAYS: + return list(self._displays.values()) + return [] + + def registerDisplayListener(self, listener: DisplayListener, handler: Any = None): + lid = id(listener) + self._listeners[lid] = listener + + def unregisterDisplayListener(self, listener: DisplayListener): + lid = id(listener) + self._listeners.pop(lid, None) + + def createVirtualDisplay(self, name: str, width: int, height: int, densityDpi: int, + surface: Any, flags: int, callback: Callable = None, + handler: Any = None) -> "VirtualDisplay": + display_id = self._next_display_id + self._next_display_id += 1 + vd = VirtualDisplay(display_id, name, width, height, densityDpi, surface, flags) + return vd + + def getStableDisplay(self) -> Optional["Display"]: + return self._displays.get(0) + + +# ═══════════════════════════════════════════════════════════════ +# Display +# ═══════════════════════════════════════════════════════════════ + +class Display(Stub): + """Display — 显示信息""" + + DEFAULT_DISPLAY = 0 + + STATE_UNKNOWN = 0 + STATE_OFF = 1 + STATE_ON = 2 + STATE_DOZE = 3 + STATE_DOZE_SUSPEND = 4 + + FLAG_PRESENTATION = 2 + FLAG_PRIVATE = 4 + FLAG_ROUND = 8 + FLAG_CAN_SHOW_WITH_INSETS = 16 + FLAG_SECURE = 32 + FLAG_SUPPORTS_HDR = 64 + + def __init__(self, displayId: int = 0, name: str = "Display", + width: int = 1920, height: int = 1080, + densityDpi: int = 420, refreshRate: float = 60.0): + super().__init__() + self._display_id = displayId + self._name = name + self._width = width + self._height = height + self._density_dpi = densityDpi + self._refresh_rate = refreshRate + self._state = Display.STATE_ON + self._flags = 0 + self._app_width = width + self._app_height = height + + def getDisplayId(self) -> int: + return self._display_id + + def getName(self) -> str: + return self._name + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def getRotation(self) -> int: + return 0 + + def getState(self) -> int: + return self._state + + def getRefreshRate(self) -> float: + return self._refresh_rate + + def isHdr(self) -> bool: + return bool(self._flags & Display.FLAG_SUPPORTS_HDR) + + def getFlags(self) -> int: + return self._flags + + def getMode(self) -> Any: + return None + + def getSupportedModes(self) -> List[Any]: + return [] + + def getSupportedHdrTypes(self) -> List[int]: + return [] + + def getAppWidth(self) -> int: + return self._app_width + + def getAppHeight(self) -> int: + return self._app_height + + def getDensityDpi(self) -> int: + return self._density_dpi + + def toString(self) -> str: + return f"Display(id={self._display_id}, name={self._name}, {self._width}x{self._height}," \ + f" dpi={self._density_dpi})" + + +# ═══════════════════════════════════════════════════════════════ +# VirtualDisplay +# ═══════════════════════════════════════════════════════════════ + +class VirtualDisplay(Stub): + """VirtualDisplay — 虚拟显示""" + + def __init__(self, displayId: int, name: str, width: int, height: int, + densityDpi: int, surface: Any, flags: int): + super().__init__() + self._display_id = displayId + self._name = name + self._width = width + self._height = height + self._density_dpi = densityDpi + self._surface = surface + self._flags = flags + self._display = Display(displayId, name, width, height, densityDpi) + self._released = False + + def setSurface(self, surface: Any): + self._surface = surface + + def resize(self, width: int, height: int, densityDpi: int): + self._width = width + self._height = height + self._density_dpi = densityDpi + + def release(self): + self._released = True + + def getDisplay(self) -> Display: + return self._display + + def getSurface(self) -> Any: + return self._surface + + def getFlags(self) -> int: + return self._flags + + def toString(self) -> str: + return f"VirtualDisplay(id={self._display_id}, name={self._name}," \ + f" {self._width}x{self._height}, released={self._released})" + + +# ═══════════════════════════════════════════════════════════════ +# WifiDisplayStatus +# ═══════════════════════════════════════════════════════════════ + +class WifiDisplayStatus(Stub): + """WifiDisplayStatus — Wi-Fi 显示状态""" + + FEATURE_STATE_DISABLED = 0 + FEATURE_STATE_ON = 1 + FEATURE_STATE_OFF = 2 + FEATURE_STATE_UNAVAILABLE = 3 + + def __init__(self): + super().__init__() + self._active_display: Optional[WifiDisplay] = None + self._scan_results: List[WifiDisplay] = [] + self._feature_state = WifiDisplayStatus.FEATURE_STATE_DISABLED + + def getActiveDisplay(self) -> Optional["WifiDisplay"]: + return self._active_display + + def getScanResults(self) -> List["WifiDisplay"]: + return self._scan_results + + def getFeatureState(self) -> int: + return self._feature_state + + def toString(self) -> str: + return f"WifiDisplayStatus(featureState={self._feature_state})" + + +# ═══════════════════════════════════════════════════════════════ +# WifiDisplay +# ═══════════════════════════════════════════════════════════════ + +class WifiDisplay(Stub): + """WifiDisplay — Wi-Fi 显示设备""" + + def __init__(self, deviceAddress: str = "", deviceName: str = "", + deviceAlias: str = "", available: bool = False, + remembered: bool = False): + super().__init__() + self._device_address = deviceAddress + self._device_name = deviceName + self._device_alias = deviceAlias + self._available = available + self._remembered = remembered + + def getDeviceAddress(self) -> str: + return self._device_address + + def getDeviceName(self) -> str: + return self._device_name + + def getDeviceAlias(self) -> str: + return self._device_alias + + def isAvailable(self) -> bool: + return self._available + + def isRemembered(self) -> bool: + return self._remembered + + def equals(self, other: Any) -> bool: + if not isinstance(other, WifiDisplay): + return False + return self._device_address == other._device_address + + def hashCode(self) -> int: + return hash(self._device_address) & 0x7FFFFFFF + + def toString(self) -> str: + return f"WifiDisplay(address={self._device_address}, name={self._device_name}," \ + f" available={self._available})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "DisplayManager", + "Display", + "VirtualDisplay", + "WifiDisplayStatus", + "WifiDisplay", +] \ No newline at end of file diff --git a/android_pyapi/android/hardware_fingerprint.py b/android_pyapi/android/hardware_fingerprint.py new file mode 100644 index 0000000..b3fb524 --- /dev/null +++ b/android_pyapi/android/hardware_fingerprint.py @@ -0,0 +1,179 @@ +""" +Android hardware.fingerprint 组件 — FingerprintManager, Fingerprint 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + + +# ═══════════════════════════════════════════════════════════════ +# FingerprintManager +# ═══════════════════════════════════════════════════════════════ + +class FingerprintManager(Stub): + """FingerprintManager — 指纹管理器""" + + FINGERPRINT_ACQUIRED_GOOD = 0 + FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3 + FINGERPRINT_ACQUIRED_INSUFFICIENT = 2 + FINGERPRINT_ACQUIRED_PARTIAL = 1 + FINGERPRINT_ACQUIRED_TOO_FAST = 5 + FINGERPRINT_ACQUIRED_TOO_SLOW = 4 + + FINGERPRINT_ERROR_HW_UNAVAILABLE = 1 + FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2 + FINGERPRINT_ERROR_TIMEOUT = 3 + FINGERPRINT_ERROR_NO_SPACE = 4 + FINGERPRINT_ERROR_CANCELED = 5 + FINGERPRINT_ERROR_LOCKOUT = 7 + FINGERPRINT_ERROR_VENDOR = 8 + FINGERPRINT_ERROR_USER_CANCELED = 10 + FINGERPRINT_ERROR_BAD_CALIBRATION = 11 + FINGERPRINT_ERROR_POWER_PRESS = 12 + FINGERPRINT_ERROR_IDLE_NOT_ENROLLED = 13 + + # ═══════════════════════════════════════════════════════════ + # AuthenticationCallback + # ═══════════════════════════════════════════════════════════ + + class AuthenticationCallback(Stub): + """FingerprintManager.AuthenticationCallback — 认证回调""" + + def onAuthenticationError(self, errorCode: int, errString: str): + pass + + def onAuthenticationHelp(self, helpCode: int, helpString: str): + pass + + def onAuthenticationSucceeded(self, result: "FingerprintManager.AuthenticationResult"): + pass + + def onAuthenticationFailed(self): + pass + + def onAuthenticationAcquired(self, acquireInfo: int): + pass + + # ═══════════════════════════════════════════════════════════ + # AuthenticationResult + # ═══════════════════════════════════════════════════════════ + + class AuthenticationResult(Stub): + """FingerprintManager.AuthenticationResult — 认证结果""" + + def __init__(self, fingerprint: "Fingerprint" = None, + cryptoObject: "FingerprintManager.CryptoObject" = None): + super().__init__() + self._fingerprint = fingerprint + self._crypto_object = cryptoObject + + def getFingerprint(self) -> Optional["Fingerprint"]: + return self._fingerprint + + def getCryptoObject(self) -> Optional["FingerprintManager.CryptoObject"]: + return self._crypto_object + + # ═══════════════════════════════════════════════════════════ + # CryptoObject + # ═══════════════════════════════════════════════════════════ + + class CryptoObject(Stub): + """FingerprintManager.CryptoObject — 加密对象""" + + def __init__(self, cipher: Any = None, signature: Any = None, mac: Any = None): + super().__init__() + self._cipher = cipher + self._signature = signature + self._mac = mac + + def getCipher(self) -> Any: + return self._cipher + + def getSignature(self) -> Any: + return self._signature + + def getMac(self) -> Any: + return self._mac + + def __init__(self): + super().__init__() + self._enrolled_fingerprints: List[Fingerprint] = [] + self._callbacks: Dict[int, FingerprintManager.AuthenticationCallback] = {} + + def isHardwareDetected(self) -> bool: + return False + + def hasEnrolledFingerprints(self) -> bool: + return len(self._enrolled_fingerprints) > 0 + + def authenticate(self, crypto: Optional[CryptoObject] = None, + cancel: Any = None, flags: int = 0, + callback: Optional[AuthenticationCallback] = None, + handler: Any = None): + if callback: + callback.onAuthenticationError( + FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, + "Fingerprint hardware not available on desktop" + ) + + def enroll(self, token: Any, userId: int = 0, + callback: Optional[AuthenticationCallback] = None): + raise AndroidOnlyError("FingerprintManager.enroll() is not available on desktop") + + def remove(self, fingerprint: "Fingerprint", userId: int = 0): + if fingerprint in self._enrolled_fingerprints: + self._enrolled_fingerprints.remove(fingerprint) + + def rename(self, fingerprint: "Fingerprint", newName: str): + if fingerprint: + fingerprint._name = newName + + def getEnrolledFingerprints(self) -> List["Fingerprint"]: + return list(self._enrolled_fingerprints) + + +# ═══════════════════════════════════════════════════════════════ +# Fingerprint +# ═══════════════════════════════════════════════════════════════ + +class Fingerprint(Stub): + """Fingerprint — 指纹数据""" + + def __init__(self, name: str = "", fingerId: int = 0, groupId: int = 0, + deviceId: int = 0): + super().__init__() + self._name = name + self._finger_id = fingerId + self._group_id = groupId + self._device_id = deviceId + + def getName(self) -> str: + return self._name + + def getFingerId(self) -> int: + return self._finger_id + + def getGroupId(self) -> int: + return self._group_id + + def getDeviceId(self) -> int: + return self._device_id + + def toString(self) -> str: + return f"Fingerprint(name={self._name}, fingerId={self._finger_id})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "FingerprintManager", + "Fingerprint", +] \ No newline at end of file diff --git a/android_pyapi/android/hardware_input.py b/android_pyapi/android/hardware_input.py new file mode 100644 index 0000000..0410fec --- /dev/null +++ b/android_pyapi/android/hardware_input.py @@ -0,0 +1,254 @@ +""" +Android hardware.input 组件 — InputManager, InputDevice, MotionRange 等 +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# InputManager +# ═══════════════════════════════════════════════════════════════ + +class InputManager(Stub): + """InputManager — 输入管理器""" + + ACTION_QUERY_KEYBOARD_LAYOUTS = "android.hardware.input.action.QUERY_KEYBOARD_LAYOUTS" + + # ═══════════════════════════════════════════════════════════ + # InputDeviceListener + # ═══════════════════════════════════════════════════════════ + + class InputDeviceListener(Stub): + """InputManager.InputDeviceListener — 输入设备监听器""" + + def onInputDeviceAdded(self, deviceId: int): + pass + + def onInputDeviceRemoved(self, deviceId: int): + pass + + def onInputDeviceChanged(self, deviceId: int): + pass + + def __init__(self): + super().__init__() + self._devices: Dict[int, InputDevice] = {} + self._listeners: Dict[int, InputManager.InputDeviceListener] = {} + self._next_id = 1 + self._init_default_devices() + + def _init_default_devices(self): + # Keyboard + kb = InputDevice( + deviceId=0, name="Virtual Keyboard", + descriptor="keyboard_v1", sources=InputDevice.SOURCE_KEYBOARD, + vendorId=0x1, productId=0x1, + keyboardType=InputDevice.KEYBOARD_TYPE_ALPHABETIC, + hasMicrophone=False, isVirtual=True + ) + self._devices[0] = kb + + # Mouse + mouse = InputDevice( + deviceId=1, name="Virtual Mouse", + descriptor="mouse_v1", sources=InputDevice.SOURCE_MOUSE, + vendorId=0x2, productId=0x2, + keyboardType=InputDevice.KEYBOARD_TYPE_NONE, + hasMicrophone=False, isVirtual=True + ) + self._devices[1] = mouse + + # Trackpad + tp = InputDevice( + deviceId=2, name="Virtual Trackpad", + descriptor="trackpad_v1", sources=InputDevice.SOURCE_TOUCHPAD, + vendorId=0x3, productId=0x3, + keyboardType=InputDevice.KEYBOARD_TYPE_NONE, + hasMicrophone=False, isVirtual=False + ) + self._devices[2] = tp + + def getDevice(self, deviceId: int) -> Optional["InputDevice"]: + return self._devices.get(deviceId) + + def getDeviceIds(self) -> List[int]: + return list(self._devices.keys()) + + def registerInputDeviceListener(self, listener: InputDeviceListener, handler: Any = None): + lid = id(listener) + self._listeners[lid] = listener + + def unregisterInputDeviceListener(self, listener: InputDeviceListener): + lid = id(listener) + self._listeners.pop(lid, None) + + def getInputDevice(self, deviceId: int) -> Optional["InputDevice"]: + return self.getDevice(deviceId) + + def isInTabletMode(self) -> bool: + return False + + def getCurrentKeyboardLayoutForInputDevice(self, device: "InputDevice") -> Optional[str]: + return None + + def setKeyboardLayoutForInputDevice(self, device: "InputDevice", + keyboardLayoutDescriptor: str): + pass + + +# ═══════════════════════════════════════════════════════════════ +# InputDevice +# ═══════════════════════════════════════════════════════════════ + +class InputDevice(Stub): + """InputDevice — 输入设备""" + + KEYBOARD_TYPE_ALPHABETIC = 2 + KEYBOARD_TYPE_NON_ALPHABETIC = 1 + KEYBOARD_TYPE_NONE = 0 + + SOURCE_CLASS_JOYSTICK = 16 + SOURCE_CLASS_MASK = 0x000000FF + SOURCE_CLASS_NONE = 0x00000000 + SOURCE_CLASS_POINTER = 0x00000002 + SOURCE_CLASS_POSITION = 0x00000008 + SOURCE_CLASS_TRACKBALL = 0x00000004 + SOURCE_CLASS_BUTTON = 0x00000001 + + SOURCE_GAMEPAD = 0x00000401 + SOURCE_HDMI = 0x02000000 + SOURCE_JOYSTICK = 0x01000010 + SOURCE_KEYBOARD = 0x00000101 + SOURCE_MOUSE = 0x00002002 + SOURCE_MOUSE_RELATIVE = 0x00002000 + SOURCE_ROTARY_ENCODER = 0x00800000 + SOURCE_STYLUS = 0x00004002 + SOURCE_TOUCHPAD = 0x00100008 + SOURCE_TOUCHSCREEN = 0x00001002 + SOURCE_TOUCH_NAVIGATION = 0x00200008 + SOURCE_TRACKBALL = 0x00010004 + SOURCE_UNKNOWN = 0x00000000 + SOURCE_ANY = 0xFFFFFFFE + + # ═══════════════════════════════════════════════════════════ + # MotionRange + # ═══════════════════════════════════════════════════════════ + + class MotionRange(Stub): + """InputDevice.MotionRange — 运动范围""" + + def __init__(self, axis: int = 0, source: int = 0, + min: float = 0.0, max: float = 1.0, + flat: float = 0.0, fuzz: float = 0.0, + resolution: float = 0.0): + super().__init__() + self._axis = axis + self._source = source + self._min = min + self._max = max + self._flat = flat + self._fuzz = fuzz + self._resolution = resolution + + def getAxis(self) -> int: + return self._axis + + def getSource(self) -> int: + return self._source + + def getMin(self) -> float: + return self._min + + def getMax(self) -> float: + return self._max + + def getFlat(self) -> float: + return self._flat + + def getFuzz(self) -> float: + return self._fuzz + + def getRange(self) -> float: + return self._max - self._min + + def getResolution(self) -> float: + return self._resolution + + def toString(self) -> str: + return f"MotionRange(axis={self._axis}, source={self._source}," \ + f" range=[{self._min}, {self._max}])" + + def __init__(self, deviceId: int = 0, name: str = "", descriptor: str = "", + sources: int = 0, vendorId: int = 0, productId: int = 0, + controllerNumber: int = 0, keyboardType: int = KEYBOARD_TYPE_NONE, + hasMicrophone: bool = False, isVirtual: bool = False): + super().__init__() + self._device_id = deviceId + self._name = name + self._descriptor = descriptor + self._sources = sources + self._vendor_id = vendorId + self._product_id = productId + self._controller_number = controllerNumber + self._keyboard_type = keyboardType + self._has_microphone = hasMicrophone + self._is_virtual = isVirtual + self._motion_ranges: List[InputDevice.MotionRange] = [] + + def getId(self) -> int: + return self._device_id + + def getName(self) -> str: + return self._name + + def getDescriptor(self) -> str: + return self._descriptor + + def getSources(self) -> int: + return self._sources + + def getVendorId(self) -> int: + return self._vendor_id + + def getProductId(self) -> int: + return self._product_id + + def getControllerNumber(self) -> int: + return self._controller_number + + def getKeyCharacterMap(self) -> Any: + return None + + def getMotionRange(self, axis: int = -1, source: int = -1) -> List[MotionRange]: + results = [] + for mr in self._motion_ranges: + if axis >= 0 and mr.getAxis() != axis: + continue + if source >= 0 and mr.getSource() != source: + continue + results.append(mr) + return results + + def hasMicrophone(self) -> bool: + return self._has_microphone + + def isVirtual(self) -> bool: + return self._is_virtual + + def toString(self) -> str: + return f"InputDevice(id={self._device_id}, name={self._name}," \ + f" sources={self._sources:#x})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "InputManager", + "InputDevice", +] \ No newline at end of file diff --git a/android_pyapi/android/hardware_usb.py b/android_pyapi/android/hardware_usb.py new file mode 100644 index 0000000..3420944 --- /dev/null +++ b/android_pyapi/android/hardware_usb.py @@ -0,0 +1,409 @@ +from __future__ import annotations + +""" +Android hardware.usb 组件 — UsbManager, UsbDevice, UsbDeviceConnection 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Dict, List, Optional, Set + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# UsbConstants +# ═══════════════════════════════════════════════════════════════ + +class UsbConstants(Stub): + """UsbConstants — USB 常量""" + + USB_DIR_IN = 0x80 + USB_DIR_OUT = 0 + + USB_TYPE_MASK = 0x60 + USB_TYPE_CLASS = 0x20 + USB_TYPE_VENDOR = 0x40 + USB_TYPE_STANDARD = 0x00 + + USB_ENDPOINT_XFER_CONTROL = 0 + USB_ENDPOINT_XFER_ISOC = 1 + USB_ENDPOINT_XFER_BULK = 2 + USB_ENDPOINT_XFER_INT = 3 + + USB_CLASS_APP_SPEC = 0xFE + USB_CLASS_AUDIO = 0x01 + USB_CLASS_CDC_DATA = 0x0A + USB_CLASS_COMM = 0x02 + USB_CLASS_CONTENT_SEC = 0x0D + USB_CLASS_CSCID = 0x0B + USB_CLASS_HID = 0x03 + USB_CLASS_HUB = 0x09 + USB_CLASS_MASS_STORAGE = 0x08 + USB_CLASS_MISC = 0xEF + USB_CLASS_PER_INTERFACE = 0x00 + USB_CLASS_PHYSICA = 0x05 + USB_CLASS_PRINTER = 0x07 + USB_CLASS_STILL_IMAGE = 0x06 + USB_CLASS_VENDOR_SPEC = 0xFF + USB_CLASS_VIDEO = 0x0E + USB_CLASS_WIRELESS = 0xE0 + + +# ═══════════════════════════════════════════════════════════════ +# UsbManager +# ═══════════════════════════════════════════════════════════════ + +class UsbManager(Stub): + """UsbManager — USB 管理器""" + + ACTION_USB_DEVICE_ATTACHED = "android.hardware.usb.action.USB_DEVICE_ATTACHED" + ACTION_USB_DEVICE_DETACHED = "android.hardware.usb.action.USB_DEVICE_DETACHED" + ACTION_USB_ACCESSORY_ATTACHED = "android.hardware.usb.action.USB_ACCESSORY_ATTACHED" + ACTION_USB_ACCESSORY_DETACHED = "android.hardware.usb.action.USB_ACCESSORY_DETACHED" + + EXTRA_DEVICE = "device" + EXTRA_ACCESSORY = "accessory" + EXTRA_PERMISSION_GRANTED = "permission" + + def __init__(self): + super().__init__() + self._devices: Dict[str, UsbDevice] = {} + self._permissions: Set[str] = set() + self._accessories: List[UsbAccessory] = [] + self._connections: Dict[str, UsbDeviceConnection] = {} + # Create some mock devices + self._devices["001"] = UsbDevice( + deviceId=1, deviceName="/dev/bus/usb/001/001", + vendorId=0x18D1, productId=0x4EE1, + deviceClass=UsbConstants.USB_CLASS_MISC, + deviceSubclass=0, deviceProtocol=0, + manufacturerName="Google", productName="Pixel Device", + version="3.1", serialNumber="ABC123" + ) + + def getDeviceList(self) -> Dict[str, UsbDevice]: + return dict(self._devices) + + def openDevice(self, device: "UsbDevice") -> Optional["UsbDeviceConnection"]: + if not device: + return None + conn = UsbDeviceConnection(device) + self._connections[str(device.getDeviceId())] = conn + return conn + + def hasPermission(self, device: "UsbDevice") -> bool: + return device.getDeviceName() in self._permissions + + def requestPermission(self, device: "UsbDevice"): + self._permissions.add(device.getDeviceName()) + + def grantPermission(self, device: "UsbDevice"): + self._permissions.add(device.getDeviceName()) + + def revokePermission(self, device: "UsbDevice"): + self._permissions.discard(device.getDeviceName()) + + def setCurrentFunction(self, function: str, usbDataUnlocked: bool = False): + pass + + def setUsbDataUnlocked(self, unlocked: bool): + pass + + def getCurrentAccessory(self) -> Optional["UsbAccessory"]: + return self._accessories[0] if self._accessories else None + + def openAccessory(self, accessory: "UsbAccessory") -> Optional["UsbDeviceConnection"]: + if not accessory: + return None + conn = UsbDeviceConnection(accessory) + return conn + + +# ═══════════════════════════════════════════════════════════════ +# UsbDevice +# ═══════════════════════════════════════════════════════════════ + +class UsbDevice(Stub): + """UsbDevice — USB 设备""" + + def __init__(self, deviceId: int = 0, deviceName: str = "", + vendorId: int = 0, productId: int = 0, + deviceClass: int = 0, deviceSubclass: int = 0, + deviceProtocol: int = 0, configurationCount: int = 1, + interfaceCount: int = 0, + manufacturerName: str = "", productName: str = "", + version: str = "", serialNumber: str = ""): + super().__init__() + self._device_id = deviceId + self._device_name = deviceName + self._vendor_id = vendorId + self._product_id = productId + self._device_class = deviceClass + self._device_subclass = deviceSubclass + self._device_protocol = deviceProtocol + self._configuration_count = configurationCount + self._interface_count = interfaceCount + self._manufacturer_name = manufacturerName + self._product_name = productName + self._version = version + self._serial_number = serialNumber + self._interfaces: List[UsbInterface] = [] + + def getDeviceId(self) -> int: + return self._device_id + + def getDeviceName(self) -> str: + return self._device_name + + def getVendorId(self) -> int: + return self._vendor_id + + def getProductId(self) -> int: + return self._product_id + + def getDeviceClass(self) -> int: + return self._device_class + + def getDeviceSubclass(self) -> int: + return self._device_subclass + + def getDeviceProtocol(self) -> int: + return self._device_protocol + + def getConfigurationCount(self) -> int: + return self._configuration_count + + def getInterfaceCount(self) -> int: + return len(self._interfaces) + + def getInterface(self, index: int) -> Optional["UsbInterface"]: + if 0 <= index < len(self._interfaces): + return self._interfaces[index] + return None + + def getInterfaces(self) -> List["UsbInterface"]: + return list(self._interfaces) + + def getManufacturerName(self) -> str: + return self._manufacturer_name + + def getProductName(self) -> str: + return self._product_name + + def getVersion(self) -> str: + return self._version + + def getSerialNumber(self) -> str: + return self._serial_number + + def describeContents(self) -> int: + return 0 + + def equals(self, other: Any) -> bool: + if not isinstance(other, UsbDevice): + return False + return self._device_id == other._device_id + + def hashCode(self) -> int: + return hash(self._device_id) & 0x7FFFFFFF + + def toString(self) -> str: + return f"UsbDevice(name={self._device_name}, vid={self._vendor_id:x}," \ + f" pid={self._product_id:x})" + + +# ═══════════════════════════════════════════════════════════════ +# UsbDeviceConnection +# ═══════════════════════════════════════════════════════════════ + +class UsbDeviceConnection(Stub): + """UsbDeviceConnection — USB 设备连接""" + + def __init__(self, device: Any): + super().__init__() + self._device = device + self._claimed_interfaces: Set[int] = set() + self._fd = -1 + + def claimInterface(self, intf: "UsbInterface", force: bool = False) -> bool: + self._claimed_interfaces.add(intf.getId()) + return True + + def releaseInterface(self, intf: "UsbInterface") -> bool: + self._claimed_interfaces.discard(intf.getId()) + return True + + def bulkTransfer(self, endpoint: "UsbEndpoint", buffer: bytes, + length: int, timeout: int) -> int: + return -1 + + def controlTransfer(self, requestType: int, request: int, + value: int, index: int, buffer: bytes, + length: int, timeout: int) -> int: + return -1 + + def getRawDescriptors(self) -> bytes: + return b"" + + def getFileDescriptor(self) -> int: + return self._fd + + def serial(self) -> Optional[str]: + if hasattr(self._device, 'getSerialNumber'): + return self._device.getSerialNumber() + return None + + def close(self): + self._claimed_interfaces.clear() + + def toString(self) -> str: + return f"UsbDeviceConnection(device={self._device})" + + +# ═══════════════════════════════════════════════════════════════ +# UsbInterface +# ═══════════════════════════════════════════════════════════════ + +class UsbInterface(Stub): + """UsbInterface — USB 接口""" + + def __init__(self, id: int = 0, alternateSetting: int = 0, + name: str = "", interfaceClass: int = 0, + interfaceSubclass: int = 0, interfaceProtocol: int = 0): + super().__init__() + self._id = id + self._alternate_setting = alternateSetting + self._name = name + self._interface_class = interfaceClass + self._interface_subclass = interfaceSubclass + self._interface_protocol = interfaceProtocol + self._endpoints: List[UsbEndpoint] = [] + + def getId(self) -> int: + return self._id + + def getAlternateSetting(self) -> int: + return self._alternate_setting + + def getName(self) -> str: + return self._name + + def getInterfaceClass(self) -> int: + return self._interface_class + + def getInterfaceSubclass(self) -> int: + return self._interface_subclass + + def getInterfaceProtocol(self) -> int: + return self._interface_protocol + + def getEndpointCount(self) -> int: + return len(self._endpoints) + + def getEndpoint(self, index: int) -> Optional["UsbEndpoint"]: + if 0 <= index < len(self._endpoints): + return self._endpoints[index] + return None + + def toString(self) -> str: + return f"UsbInterface(id={self._id}, name={self._name})" + + +# ═══════════════════════════════════════════════════════════════ +# UsbEndpoint +# ═══════════════════════════════════════════════════════════════ + +class UsbEndpoint(Stub): + """UsbEndpoint — USB 端点""" + + def __init__(self, address: int = 0, endpointNumber: int = 0, + attributes: int = 0, direction: int = 0, + maxPacketSize: int = 64, interval: int = 0, + type_: int = 0): + super().__init__() + self._address = address + self._endpoint_number = endpointNumber + self._attributes = attributes + self._direction = direction + self._max_packet_size = maxPacketSize + self._interval = interval + self._type = type_ + + def getAddress(self) -> int: + return self._address + + def getEndpointNumber(self) -> int: + return self._endpoint_number + + def getAttributes(self) -> int: + return self._attributes + + def getDirection(self) -> int: + return self._direction + + def getMaxPacketSize(self) -> int: + return self._max_packet_size + + def getInterval(self) -> int: + return self._interval + + def getType(self) -> int: + return self._type + + def toString(self) -> str: + return f"UsbEndpoint(addr={self._address:#04x}, type={self._type})" + + +# ═══════════════════════════════════════════════════════════════ +# UsbAccessory +# ═══════════════════════════════════════════════════════════════ + +class UsbAccessory(Stub): + """UsbAccessory — USB 附件""" + + def __init__(self, manufacturer: str = "", model: str = "", + description: str = "", version: str = "", + uri: str = "", serial: str = ""): + super().__init__() + self._manufacturer = manufacturer + self._model = model + self._description = description + self._version = version + self._uri = uri + self._serial = serial + + def getManufacturer(self) -> str: + return self._manufacturer + + def getModel(self) -> str: + return self._model + + def getDescription(self) -> str: + return self._description + + def getVersion(self) -> str: + return self._version + + def getUri(self) -> str: + return self._uri + + def getSerial(self) -> str: + return self._serial + + def toString(self) -> str: + return f"UsbAccessory(manufacturer={self._manufacturer}, model={self._model})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "UsbManager", + "UsbDevice", + "UsbDeviceConnection", + "UsbInterface", + "UsbEndpoint", + "UsbConstants", + "UsbAccessory", +] \ No newline at end of file diff --git a/android_pyapi/android/media_audiofx.py b/android_pyapi/android/media_audiofx.py new file mode 100644 index 0000000..45901e4 --- /dev/null +++ b/android_pyapi/android/media_audiofx.py @@ -0,0 +1,547 @@ +""" +Android media.audiofx 组件 — AudioEffect, Equalizer, BassBoost, Virtualizer 等 +方法签名完全对齐 Android SDK +""" + +import math +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# AudioEffect +# ═══════════════════════════════════════════════════════════════ + +class AudioEffect(Stub): + """AudioEffect — 音频效果基类""" + + EFFECT_SUCCESS = 0 + EFFECT_ERROR = -1 + EFFECT_ALREADY_EXISTS = -2 + EFFECT_NO_MORE_SESSION = -3 + EFFECT_REQUEST_INIT = -4 + EFFECT_INIT_FAILED = -5 + + EFFECT_INSERT = 1 + EFFECT_AUXILIARY = 2 + EFFECT_PRE_PROCESSING = 4 + EFFECT_POST_PROCESSING = 8 + + CONTENT_TYPE_MUSIC = 2 + CONTENT_TYPE_MOVIE = 3 + CONTENT_TYPE_GAME = 4 + CONTENT_TYPE_VOICE = 1 + + # ═══════════════════════════════════════════════════════════ + # Descriptor + # ═══════════════════════════════════════════════════════════ + + class Descriptor(Stub): + """AudioEffect.Descriptor — 效果描述符""" + + def __init__(self, type: str = "", uuid: str = "", + connectMode: str = "", name: str = "", + implementor: str = ""): + super().__init__() + self.type = type + self.uuid = uuid + self.connectMode = connectMode + self.name = name + self.implementor = implementor + + def toString(self) -> str: + return f"Descriptor(type={self.type}, uuid={self.uuid}, name={self.name})" + + def __init__(self, effectType: str = "", priority: int = 0, + audioSession: int = 0): + super().__init__() + self._enabled = False + self._effect_type = effectType + self._priority = priority + self._audio_session = audioSession + self._id = hash(self) & 0x7FFFFFFF + self._control_status_listener: Optional[Callable] = None + self._enable_status_listener: Optional[Callable] = None + + def getEnabled(self) -> bool: + return self._enabled + + def setEnabled(self, enabled: bool) -> int: + self._enabled = enabled + return AudioEffect.EFFECT_SUCCESS + + def getId(self) -> int: + return self._id + + def getDescriptor(self) -> Descriptor: + return AudioEffect.Descriptor( + type=self._effect_type, + uuid="", + connectMode="", + name=self.__class__.__name__, + implementor="Android-pyApi" + ) + + def release(self): + self._enabled = False + + def setControlStatusListener(self, listener: Callable): + self._control_status_listener = listener + + def setEnableStatusListener(self, listener: Callable): + self._enable_status_listener = listener + + +# ═══════════════════════════════════════════════════════════════ +# Equalizer +# ═══════════════════════════════════════════════════════════════ + +class Equalizer(AudioEffect): + """Equalizer — 均衡器""" + + PARAM_CURRENT_PRESET = 0 + PARAM_GET_BAND = 1 + PARAM_STRING_SIZE_MAX = 2 + PARAM_BAND_LEVEL = 3 + PARAM_LEVEL_RANGE = 4 + PARAM_NUM_BANDS = 5 + PARAM_BAND_FREQ_RANGE = 6 + PARAM_CENTER_FREQ = 7 + PARAM_GET_PRESET_NAME = 8 + PARAM_NUM_PRESETS = 9 + PARAM_PROPERTIES = 10 + + def __init__(self, priority: int = 0, audioSession: int = 0): + super().__init__("Equalizer", priority, audioSession) + self._num_bands = 5 + self._band_levels: Dict[int, int] = {} + self._presets = [ + "Normal", "Classical", "Dance", "Flat", "Folk", + "Heavy Metal", "Hip Hop", "Jazz", "Pop", "Rock" + ] + self._current_preset = 0 + self._band_freq_ranges: Dict[int, Tuple[int, int]] = { + 0: (60, 200), 1: (200, 600), 2: (600, 2400), + 3: (2400, 8000), 4: (8000, 16000) + } + + def getNumberOfBands(self) -> int: + return self._num_bands + + def getBand(self, frequency: int) -> int: + for band, (low, high) in self._band_freq_ranges.items(): + if low <= frequency <= high: + return band + return 0 + + def getBandLevel(self, band: int) -> int: + return self._band_levels.get(band, 0) + + def setBandLevel(self, band: int, level: int): + self._band_levels[band] = level + + def getBandLevelRange(self) -> List[int]: + return [-1500, 1500] + + def getCenterFreq(self, band: int) -> int: + if band in self._band_freq_ranges: + low, high = self._band_freq_ranges[band] + return (low + high) // 2 + return 0 + + def getNumberOfPresets(self) -> int: + return len(self._presets) + + def getPresetName(self, preset: int) -> str: + if 0 <= preset < len(self._presets): + return self._presets[preset] + return "Unknown" + + def usePreset(self, preset: int): + if 0 <= preset < len(self._presets): + self._current_preset = preset + + def getCurrentPreset(self) -> int: + return self._current_preset + + def getProperties(self) -> Any: + return None + + def setProperties(self, properties: Any): + pass + + def toString(self) -> str: + return f"Equalizer(preset={self._presets[self._current_preset]}," \ + f" bands={self._num_bands})" + + +# ═══════════════════════════════════════════════════════════════ +# BassBoost +# ═══════════════════════════════════════════════════════════════ + +class BassBoost(AudioEffect): + """BassBoost — 低音增强""" + + PARAM_STRENGTH = 1 + PARAM_STRENGTH_SUPPORTED = 2 + + def __init__(self, priority: int = 0, audioSession: int = 0): + super().__init__("BassBoost", priority, audioSession) + self._strength: int = 0 + + def setStrength(self, strength: int): + self._strength = max(0, min(1000, strength)) + + def getRoundedStrength(self) -> int: + return self._strength + + def getStrengthSupported(self) -> bool: + return True + + def toString(self) -> str: + return f"BassBoost(strength={self._strength})" + + +# ═══════════════════════════════════════════════════════════════ +# Virtualizer +# ═══════════════════════════════════════════════════════════════ + +class Virtualizer(AudioEffect): + """Virtualizer — 虚拟器 (空间音频)""" + + PARAM_STRENGTH = 1 + PARAM_STRENGTH_SUPPORTED = 2 + PARAM_VIRTUALIZATION_MODE = 3 + PARAM_FORCE_VIRTUALIZATION_MODE = 4 + + VIRTUALIZATION_MODE_AUTO = 0 + VIRTUALIZATION_MODE_OFF = 1 + VIRTUALIZATION_MODE_TRANSAURAL = 2 + VIRTUALIZATION_MODE_BINAURAL = 3 + + def __init__(self, priority: int = 0, audioSession: int = 0): + super().__init__("Virtualizer", priority, audioSession) + self._strength: int = 0 + self._virtualization_mode = Virtualizer.VIRTUALIZATION_MODE_AUTO + + def setStrength(self, strength: int): + self._strength = max(0, min(1000, strength)) + + def getRoundedStrength(self) -> int: + return self._strength + + def canVirtualize(self, channelMask: int, virtualizationMode: int) -> bool: + return True + + def getVirtualizationMode(self) -> int: + return self._virtualization_mode + + def toString(self) -> str: + return f"Virtualizer(strength={self._strength}, mode={self._virtualization_mode})" + + +# ═══════════════════════════════════════════════════════════════ +# PresetReverb +# ═══════════════════════════════════════════════════════════════ + +class PresetReverb(AudioEffect): + """PresetReverb — 预设混响""" + + PRESET_NONE = 0 + PRESET_SMALLROOM = 1 + PRESET_MEDIUMROOM = 2 + PRESET_LARGEROOM = 3 + PRESET_MEDIUMHALL = 4 + PRESET_LARGEHALL = 5 + PRESET_PLATE = 6 + + PARAM_PRESET = 0 + + def __init__(self, priority: int = 0, audioSession: int = 0): + super().__init__("PresetReverb", priority, audioSession) + self._preset = PresetReverb.PRESET_NONE + self._preset_names = { + PresetReverb.PRESET_NONE: "None", + PresetReverb.PRESET_SMALLROOM: "Small Room", + PresetReverb.PRESET_MEDIUMROOM: "Medium Room", + PresetReverb.PRESET_LARGEROOM: "Large Room", + PresetReverb.PRESET_MEDIUMHALL: "Medium Hall", + PresetReverb.PRESET_LARGEHALL: "Large Hall", + PresetReverb.PRESET_PLATE: "Plate", + } + + def setPreset(self, preset: int): + if preset in self._preset_names: + self._preset = preset + + def getPreset(self) -> int: + return self._preset + + def toString(self) -> str: + return f"PresetReverb(preset={self._preset_names.get(self._preset, 'Unknown')})" + + +# ═══════════════════════════════════════════════════════════════ +# EnvironmentalReverb +# ═══════════════════════════════════════════════════════════════ + +class EnvironmentalReverb(AudioEffect): + """EnvironmentalReverb — 环境混响""" + + def __init__(self, priority: int = 0, audioSession: int = 0): + super().__init__("EnvironmentalReverb", priority, audioSession) + self._decay_time = 1500 + self._decay_hf_ratio = 500 + self._density = 500 + self._diffusion = 500 + self._reverb_level = 0 + self._room_level = 0 + self._room_hf_level = 0 + self._reflections_delay = 20 + self._reflections_level = -500 + self._reverb_delay = 40 + + def setDecayTime(self, decayTime: int): + self._decay_time = decayTime + + def getDecayTime(self) -> int: + return self._decay_time + + def setDecayHFRatio(self, decayHFRatio: int): + self._decay_hf_ratio = decayHFRatio + + def getDecayHFRatio(self) -> int: + return self._decay_hf_ratio + + def setDensity(self, density: int): + self._density = density + + def getDensity(self) -> int: + return self._density + + def setDiffusion(self, diffusion: int): + self._diffusion = diffusion + + def getDiffusion(self) -> int: + return self._diffusion + + def setReverbLevel(self, reverbLevel: int): + self._reverb_level = reverbLevel + + def getReverbLevel(self) -> int: + return self._reverb_level + + def setRoomLevel(self, roomLevel: int): + self._room_level = roomLevel + + def getRoomLevel(self) -> int: + return self._room_level + + def setRoomHFLevel(self, roomHFLevel: int): + self._room_hf_level = roomHFLevel + + def getRoomHFLevel(self) -> int: + return self._room_hf_level + + def setReflectionsDelay(self, reflectionsDelay: int): + self._reflections_delay = reflectionsDelay + + def getReflectionsDelay(self) -> int: + return self._reflections_delay + + def setReflectionsLevel(self, reflectionsLevel: int): + self._reflections_level = reflectionsLevel + + def getReflectionsLevel(self) -> int: + return self._reflections_level + + def setReverbDelay(self, reverbDelay: int): + self._reverb_delay = reverbDelay + + def getReverbDelay(self) -> int: + return self._reverb_delay + + def toString(self) -> str: + return f"EnvironmentalReverb(decayTime={self._decay_time}," \ + f" density={self._density})" + + +# ═══════════════════════════════════════════════════════════════ +# LoudnessEnhancer +# ═══════════════════════════════════════════════════════════════ + +class LoudnessEnhancer(AudioEffect): + """LoudnessEnhancer — 响度增强器""" + + PARAM_TARGET_GAIN_MB = 0 + + def __init__(self, audioSession: int = 0): + super().__init__("LoudnessEnhancer", 0, audioSession) + self._target_gain = 0 + + def setTargetGain(self, gainMillibels: int): + self._target_gain = gainMillibels + + def getTargetGain(self) -> int: + return self._target_gain + + def toString(self) -> str: + return f"LoudnessEnhancer(targetGain={self._target_gain} mB)" + + +# ═══════════════════════════════════════════════════════════════ +# DynamicsProcessing +# ═══════════════════════════════════════════════════════════════ + +class DynamicsProcessing(AudioEffect): + """DynamicsProcessing — 动态处理""" + + class Stage(Stub): + """DynamicsProcessing.Stage — 处理阶段""" + + def __init__(self, enabled: bool = True): + super().__init__() + self._enabled = enabled + self._in_use = True + + def isEnabled(self) -> bool: + return self._enabled + + def setEnabled(self, enabled: bool): + self._enabled = enabled + + class EQBand(Stage): + """DynamicsProcessing.EQBand — EQ 频段""" + + def __init__(self, enabled: bool = True): + super().__init__(enabled) + self._gain = 0.0 + self._center_freq = 1000.0 + + def setGain(self, gain: float): + self._gain = gain + + def getGain(self) -> float: + return self._gain + + def setCenterFreq(self, freq: float): + self._center_freq = freq + + def getCenterFreq(self) -> float: + return self._center_freq + + class EQ(Stage): + """DynamicsProcessing.EQ — 均衡器""" + + def __init__(self, enabled: bool = True, bandCount: int = 6): + super().__init__(enabled) + self._bands = [EQBand() for _ in range(bandCount)] + + def getBand(self, band: int) -> "DynamicsProcessing.EQBand": + if 0 <= band < len(self._bands): + return self._bands[band] + raise IndexError(f"Band {band} out of range") + + class MBCBand(Stage): + """DynamicsProcessing.MBCBand — 多频段压缩频段""" + + def __init__(self, enabled: bool = True): + super().__init__(enabled) + self._gain = 0.0 + + def setGain(self, gain: float): + self._gain = gain + + def getGain(self) -> float: + return self._gain + + class MBC(Stage): + """DynamicsProcessing.MBC — 多频段压缩""" + + def __init__(self, enabled: bool = True, bandCount: int = 6): + super().__init__(enabled) + self._bands = [MBCBand() for _ in range(bandCount)] + + def getBand(self, band: int) -> "DynamicsProcessing.MBCBand": + if 0 <= band < len(self._bands): + return self._bands[band] + raise IndexError(f"Band {band} out of range") + + class Limiter(Stage): + """DynamicsProcessing.Limiter — 限制器""" + + def __init__(self, enabled: bool = True): + super().__init__(enabled) + + class Channel(Stub): + """DynamicsProcessing.Channel — 处理通道""" + + def __init__(self, stageCount: int = 1): + super().__init__() + self._stage_count = stageCount + + class Config(Stub): + """DynamicsProcessing.Config — 处理配置""" + + def __init__(self, channelCount: int = 1, preEqCount: int = 1, + mbcCount: int = 1, postEqCount: int = 1, + limiterCount: int = 1, prefEqBandCount: int = 6, + postEqBandCount: int = 6, mbcBandCount: int = 6): + super().__init__() + self._channels = [DynamicsProcessing.Channel(0) + for _ in range(channelCount)] + + def __init__(self, audioSession: int = 0): + super().__init__("DynamicsProcessing", 0, audioSession) + self._channel_count = 1 + self._post_constraints: List[Any] = [] + self._pre_eqs: List[DynamicsProcessing.EQ] = [DynamicsProcessing.EQ()] + self._post_eqs: List[DynamicsProcessing.EQ] = [DynamicsProcessing.EQ()] + self._mbcs: List[DynamicsProcessing.MBC] = [DynamicsProcessing.MBC()] + self._limiters: List[DynamicsProcessing.Limiter] = [DynamicsProcessing.Limiter()] + self._mbands: List[Any] = [] + self._channel_stage: List[Any] = [] + + def getChannelCount(self) -> int: + return self._channel_count + + def getPostConstraints(self) -> List[Any]: + return self._post_constraints + + def getPreEQs(self) -> List[EQ]: + return self._pre_eqs + + def getMbands(self) -> List[Any]: + return self._mbands + + def getPostEQs(self) -> List[EQ]: + return self._post_eqs + + def getLimiters(self) -> List[Limiter]: + return self._limiters + + def getMbc(self) -> List[MBC]: + return self._mbcs + + def getChannelStage(self) -> List[Any]: + return self._channel_stage + + def toString(self) -> str: + return f"DynamicsProcessing(channels={self._channel_count})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "AudioEffect", + "Equalizer", + "BassBoost", + "Virtualizer", + "PresetReverb", + "EnvironmentalReverb", + "LoudnessEnhancer", + "DynamicsProcessing", +] \ No newline at end of file diff --git a/android_pyapi/android/media_browse.py b/android_pyapi/android/media_browse.py new file mode 100644 index 0000000..686ab66 --- /dev/null +++ b/android_pyapi/android/media_browse.py @@ -0,0 +1,310 @@ +""" +Android media.browse 组件 — MediaBrowser, MediaItem, MediaDescription 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Callable, Dict, List, Optional, Set + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# MediaBrowser +# ═══════════════════════════════════════════════════════════════ + +class MediaBrowser(Stub): + """MediaBrowser — 媒体浏览器""" + + EXTRA_PAGE = "android.media.browse.extra.PAGE" + EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE" + + # ═══════════════════════════════════════════════════════════ + # ConnectionCallback + # ═══════════════════════════════════════════════════════════ + + class ConnectionCallback(Stub): + """MediaBrowser.ConnectionCallback — 连接回调""" + + def onConnected(self): + pass + + def onConnectionSuspended(self): + pass + + def onConnectionFailed(self): + pass + + # ═══════════════════════════════════════════════════════════ + # SubscriptionCallback + # ═══════════════════════════════════════════════════════════ + + class SubscriptionCallback(Stub): + """MediaBrowser.SubscriptionCallback — 订阅回调""" + + def onChildrenLoaded(self, parentId: str, children: List["MediaBrowser.MediaItem"]): + pass + + def onError(self, parentId: str): + pass + + def onChildrenLoadedWithOptions(self, parentId: str, + children: List["MediaBrowser.MediaItem"], + options: Any): + pass + + # ═══════════════════════════════════════════════════════════ + # ItemCallback + # ═══════════════════════════════════════════════════════════ + + class ItemCallback(Stub): + """MediaBrowser.ItemCallback — 媒体项回调""" + + def onItemLoaded(self, item: "MediaBrowser.MediaItem"): + pass + + def onError(self, mediaId: str): + pass + + # ═══════════════════════════════════════════════════════════ + # MediaItem + # ═══════════════════════════════════════════════════════════ + + class MediaItem(Stub): + """MediaBrowser.MediaItem — 媒体项""" + + FLAG_BROWSABLE = 1 + FLAG_PLAYABLE = 2 + + def __init__(self, description: "MediaDescription" = None, flags: int = FLAG_PLAYABLE): + super().__init__() + self._description = description or MediaDescription() + self._flags = flags + + def getMediaId(self) -> Optional[str]: + return self._description.getMediaId() + + def getDescription(self) -> "MediaDescription": + return self._description + + def getFlags(self) -> int: + return self._flags + + def isBrowsable(self) -> bool: + return bool(self._flags & MediaBrowser.MediaItem.FLAG_BROWSABLE) + + def isPlayable(self) -> bool: + return bool(self._flags & MediaBrowser.MediaItem.FLAG_PLAYABLE) + + def toString(self) -> str: + return f"MediaItem(mediaId={self.getMediaId()}, flags={self._flags})" + + def __init__(self, context: Any = None, serviceComponent: Any = None, + connectionCallback: Optional[ConnectionCallback] = None, + rootHints: Any = None): + super().__init__() + self._connected = False + self._service_component = serviceComponent + self._root: str = "" + self._session_token: Any = None + self._connection_callback = connectionCallback + self._subscriptions: Dict[str, List[MediaBrowser.SubscriptionCallback]] = {} + self._media_tree: Dict[str, List[MediaBrowser.MediaItem]] = {} + self._item_cache: Dict[str, MediaBrowser.MediaItem] = {} + + def connect(self): + self._connected = True + self._root = "__ROOT__" + # Build a simple in-memory media tree + self._media_tree["__ROOT__"] = [ + MediaBrowser.MediaItem( + MediaDescription.Builder() + .setMediaId("__RECENT__") + .setTitle("Recently Played") + .setSubtitle("Your recently played media") + .build(), + flags=MediaBrowser.MediaItem.FLAG_BROWSABLE + ), + MediaBrowser.MediaItem( + MediaDescription.Builder() + .setMediaId("__ALL__") + .setTitle("All Music") + .setSubtitle("All your music library") + .build(), + flags=MediaBrowser.MediaItem.FLAG_BROWSABLE + ), + ] + self._media_tree["__RECENT__"] = [ + MediaBrowser.MediaItem( + MediaDescription.Builder() + .setMediaId("song_1") + .setTitle("Sample Song 1") + .setSubtitle("Artist 1") + .build(), + flags=MediaBrowser.MediaItem.FLAG_PLAYABLE + ), + ] + self._item_cache["song_1"] = self._media_tree["__RECENT__"][0] + + if self._connection_callback: + self._connection_callback.onConnected() + + def disconnect(self): + self._connected = False + self._subscriptions.clear() + if self._connection_callback: + self._connection_callback.onConnectionSuspended() + + def getConnected(self) -> bool: + return self._connected + + def getServiceComponent(self) -> Any: + return self._service_component + + def getRoot(self) -> str: + return self._root + + def isConnected(self) -> bool: + return self._connected + + def getSessionToken(self) -> Any: + return self._session_token + + def getItem(self, mediaId: str) -> Optional[MediaItem]: + return self._item_cache.get(mediaId) + + def subscribe(self, parentId: str, callback: SubscriptionCallback): + if parentId not in self._subscriptions: + self._subscriptions[parentId] = [] + self._subscriptions[parentId].append(callback) + + children = self._media_tree.get(parentId, []) + if children: + callback.onChildrenLoaded(parentId, children) + else: + callback.onError(parentId) + + def unsubscribe(self, parentId: str, callback: Optional[SubscriptionCallback] = None): + if parentId in self._subscriptions: + if callback: + self._subscriptions[parentId] = [ + cb for cb in self._subscriptions[parentId] + if cb is not callback + ] + if not self._subscriptions[parentId]: + del self._subscriptions[parentId] + else: + del self._subscriptions[parentId] + + +# ═══════════════════════════════════════════════════════════════ +# MediaDescription +# ═══════════════════════════════════════════════════════════════ + +class MediaDescription(Stub): + """MediaDescription — 媒体描述""" + + class Builder(Stub): + """MediaDescription.Builder — 媒体描述构建器""" + + def __init__(self): + super().__init__() + self._media_id: Optional[str] = None + self._title: Optional[str] = None + self._subtitle: Optional[str] = None + self._description: Optional[str] = None + self._icon_bitmap: Any = None + self._icon_uri: Optional[str] = None + self._extras: Any = None + self._media_uri: Optional[str] = None + + def setMediaId(self, mediaId: str) -> "MediaDescription.Builder": + self._media_id = mediaId + return self + + def setTitle(self, title: str) -> "MediaDescription.Builder": + self._title = title + return self + + def setSubtitle(self, subtitle: str) -> "MediaDescription.Builder": + self._subtitle = subtitle + return self + + def setDescription(self, description: str) -> "MediaDescription.Builder": + self._description = description + return self + + def setIconBitmap(self, icon: Any) -> "MediaDescription.Builder": + self._icon_bitmap = icon + return self + + def setIconUri(self, iconUri: str) -> "MediaDescription.Builder": + self._icon_uri = iconUri + return self + + def setExtras(self, extras: Any) -> "MediaDescription.Builder": + self._extras = extras + return self + + def setMediaUri(self, mediaUri: str) -> "MediaDescription.Builder": + self._media_uri = mediaUri + return self + + def build(self) -> "MediaDescription": + md = MediaDescription() + md._media_id = self._media_id + md._title = self._title + md._subtitle = self._subtitle + md._description = self._description + md._icon_bitmap = self._icon_bitmap + md._icon_uri = self._icon_uri + md._extras = self._extras + md._media_uri = self._media_uri + return md + + def __init__(self): + super().__init__() + self._media_id: Optional[str] = None + self._title: Optional[str] = None + self._subtitle: Optional[str] = None + self._description: Optional[str] = None + self._icon_bitmap: Any = None + self._icon_uri: Optional[str] = None + self._extras: Any = None + self._media_uri: Optional[str] = None + + def getMediaId(self) -> Optional[str]: + return self._media_id + + def getTitle(self) -> Optional[str]: + return self._title + + def getSubtitle(self) -> Optional[str]: + return self._subtitle + + def getDescription(self) -> Optional[str]: + return self._description + + def getIconBitmap(self) -> Any: + return self._icon_bitmap + + def getIconUri(self) -> Optional[str]: + return self._icon_uri + + def getExtras(self) -> Any: + return self._extras + + def getMediaUri(self) -> Optional[str]: + return self._media_uri + + def toString(self) -> str: + return f"MediaDescription(id={self._media_id}, title={self._title})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "MediaBrowser", + "MediaDescription", +] \ No newline at end of file diff --git a/android_pyapi/android/media_midi.py b/android_pyapi/android/media_midi.py new file mode 100644 index 0000000..ad911af --- /dev/null +++ b/android_pyapi/android/media_midi.py @@ -0,0 +1,315 @@ +""" +Android media.midi 组件 — MidiManager, MidiDevice, MidiReceiver, MidiSender 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Callable, Dict, List, Optional, Set + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# MidiManager +# ═══════════════════════════════════════════════════════════════ + +class MidiManager(Stub): + """MidiManager — MIDI 管理器""" + + def __init__(self): + super().__init__() + self._devices: Dict[int, MidiDevice] = {} + self._callbacks: Dict[int, Callable] = {} + self._next_device_id = 1 + self._init_default_devices() + + def _init_default_devices(self): + info = MidiDeviceInfo( + id=0, inputPortCount=1, outputPortCount=1, + type=MidiDeviceInfo.TYPE_VIRTUAL, + properties={MidiDeviceInfo.PROPERTY_NAME: "Virtual MIDI"} + ) + device = MidiDevice(info) + self._devices[0] = device + + def getDevices(self) -> List["MidiDeviceInfo"]: + return [d.getInfo() for d in self._devices.values()] + + def registerDeviceCallback(self, callback: Callable, handler: Any = None): + cid = id(callback) + self._callbacks[cid] = callback + + def unregisterDeviceCallback(self, callback: Callable): + cid = id(callback) + self._callbacks.pop(cid, None) + + def openDevice(self, deviceInfo: "MidiDeviceInfo", + callback: Callable, handler: Any = None): + device = self._devices.get(deviceInfo.getId()) + if device: + callback(device) + + def openBluetoothDevice(self, bluetoothDevice: Any) -> Optional["MidiDevice"]: + return None + + def getDefaultDevice(self) -> Optional["MidiDevice"]: + for d in self._devices.values(): + return d + return None + + def deviceConnected(self, info: "MidiDeviceInfo"): + pass + + def deviceDisconnected(self, info: "MidiDeviceInfo"): + pass + + +# ═══════════════════════════════════════════════════════════════ +# MidiDevice +# ═══════════════════════════════════════════════════════════════ + +class MidiDevice(Stub): + """MidiDevice — MIDI 设备""" + + # ═══════════════════════════════════════════════════════════ + # MidiConnection + # ═══════════════════════════════════════════════════════════ + + class MidiConnection(Stub): + """MidiDevice.MidiConnection — MIDI 连接""" + + def __init__(self): + super().__init__() + + def close(self): + pass + + def toString(self) -> str: + return "MidiConnection" + + def __init__(self, info: "MidiDeviceInfo" = None): + super().__init__() + self._info = info or MidiDeviceInfo() + self._input_ports: List[MidiInputPort] = [] + self._output_ports: List[MidiOutputPort] = [] + self._closed = False + + def getInfo(self) -> "MidiDeviceInfo": + return self._info + + def openInputPort(self, portNumber: int) -> Optional["MidiInputPort"]: + if self._closed: + return None + port = MidiInputPort(portNumber) + self._input_ports.append(port) + return port + + def openOutputPort(self, portNumber: int) -> Optional["MidiOutputPort"]: + if self._closed: + return None + port = MidiOutputPort(portNumber) + self._output_ports.append(port) + return port + + def close(self): + self._closed = True + + def toString(self) -> str: + return f"MidiDevice(info={self._info})" + + +# ═══════════════════════════════════════════════════════════════ +# MidiDeviceInfo +# ═══════════════════════════════════════════════════════════════ + +class MidiDeviceInfo(Stub): + """MidiDeviceInfo — MIDI 设备信息""" + + TYPE_USB = 1 + TYPE_BLUETOOTH = 2 + TYPE_VIRTUAL = 3 + + PROPERTY_ALSA_CARD = "alsa_card" + PROPERTY_ALSA_DEVICE = "alsa_device" + PROPERTY_MANUFACTURER = "manufacturer" + PROPERTY_NAME = "name" + PROPERTY_PRODUCT = "product" + PROPERTY_SERIAL_NUMBER = "serial_number" + PROPERTY_USB_DEVICE = "usb_device" + + def __init__(self, id: int = 0, inputPortCount: int = 0, + outputPortCount: int = 0, type_: int = TYPE_VIRTUAL, + properties: Optional[Dict[str, str]] = None): + super().__init__() + self._id = id + self._input_port_count = inputPortCount + self._output_port_count = outputPortCount + self._type = type_ + self._properties = properties or {} + + def getId(self) -> int: + return self._id + + def getProperties(self) -> Dict[str, str]: + return dict(self._properties) + + def getInputPortCount(self) -> int: + return self._input_port_count + + def getOutputPortCount(self) -> int: + return self._output_port_count + + def getType(self) -> int: + return self._type + + def equals(self, other: Any) -> bool: + if not isinstance(other, MidiDeviceInfo): + return False + return self._id == other._id + + def hashCode(self) -> int: + return hash(self._id) & 0x7FFFFFFF + + def toString(self) -> str: + return f"MidiDeviceInfo(id={self._id}, type={self._type}," \ + f" name={self._properties.get(MidiDeviceInfo.PROPERTY_NAME, '')})" + + +# ═══════════════════════════════════════════════════════════════ +# MidiReceiver +# ═══════════════════════════════════════════════════════════════ + +class MidiReceiver(Stub): + """MidiReceiver — MIDI 接收器""" + + def __init__(self, maxMessageSize: int = 1024): + super().__init__() + self._max_message_size = maxMessageSize + + def send(self, msg: bytes, offset: int, count: int): + self.onSend(msg, offset, count) + + def onSend(self, msg: bytes, offset: int, count: int): + pass + + def flush(self): + pass + + def getMaxMessageSize(self) -> int: + return self._max_message_size + + +# ═══════════════════════════════════════════════════════════════ +# MidiSender +# ═══════════════════════════════════════════════════════════════ + +class MidiSender(Stub): + """MidiSender — MIDI 发送器""" + + def __init__(self): + super().__init__() + self._receivers: List[MidiReceiver] = [] + + def send(self, msg: bytes, offset: int, count: int): + for r in self._receivers: + r.send(msg, offset, count) + + def onConnect(self, receiver: MidiReceiver): + pass + + def onDisconnect(self, receiver: MidiReceiver): + pass + + def connect(self, receiver: MidiReceiver): + self._receivers.append(receiver) + self.onConnect(receiver) + + def disconnect(self, receiver: MidiReceiver): + if receiver in self._receivers: + self._receivers.remove(receiver) + self.onDisconnect(receiver) + + +# ═══════════════════════════════════════════════════════════════ +# MidiInputPort +# ═══════════════════════════════════════════════════════════════ + +class MidiInputPort(MidiReceiver): + """MidiInputPort — MIDI 输入端口""" + + def __init__(self, portNumber: int = 0): + super().__init__() + self._port_number = portNumber + self._closed = False + + def send(self, msg: bytes, offset: int, count: int): + if not self._closed: + self.onSend(msg, offset, count) + + def onSend(self, msg: bytes, offset: int, count: int): + pass + + def flush(self): + pass + + def close(self): + self._closed = True + + +# ═══════════════════════════════════════════════════════════════ +# MidiOutputPort +# ═══════════════════════════════════════════════════════════════ + +class MidiOutputPort(MidiSender): + """MidiOutputPort — MIDI 输出端口""" + + def __init__(self, portNumber: int = 0): + super().__init__() + self._port_number = portNumber + + def send(self, msg: bytes, offset: int, count: int): + super().send(msg, offset, count) + + def onSend(self, msg: bytes, offset: int, count: int): + pass + + def getPortNumber(self) -> int: + return self._port_number + + +# ═══════════════════════════════════════════════════════════════ +# MidiEndpoint (stub) +# ═══════════════════════════════════════════════════════════════ + +class MidiEndpoint(Stub): + """MidiEndpoint — MIDI 端点""" + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# MidiDeviceService +# ═══════════════════════════════════════════════════════════════ + +class MidiDeviceService(Stub): + """MidiDeviceService — MIDI 设备服务""" + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "MidiManager", + "MidiDevice", + "MidiDeviceInfo", + "MidiReceiver", + "MidiSender", + "MidiInputPort", + "MidiOutputPort", + "MidiEndpoint", + "MidiDeviceService", +] \ No newline at end of file diff --git a/android_pyapi/android/media_projection.py b/android_pyapi/android/media_projection.py new file mode 100644 index 0000000..a7b0794 --- /dev/null +++ b/android_pyapi/android/media_projection.py @@ -0,0 +1,171 @@ +""" +Android media.projection 组件 — MediaProjectionManager, MediaProjection, MediaProjectionInfo 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# MediaProjectionManager +# ═══════════════════════════════════════════════════════════════ + +class MediaProjectionManager(Stub): + """MediaProjectionManager — 媒体投影管理器""" + + def __init__(self): + super().__init__() + self._active_projections: Dict[int, "MediaProjection"] = {} + self._callbacks: Dict[int, Callable] = {} + + def createScreenCaptureIntent(self) -> Any: + return "SCREEN_CAPTURE_INTENT" + + def getMediaProjection(self, resultCode: int, data: Any) -> Optional["MediaProjection"]: + if resultCode == -1: # RESULT_OK + proj = MediaProjection() + self._active_projections[id(proj)] = proj + return proj + return None + + def getActiveProjectionInfo(self) -> Optional["MediaProjectionInfo"]: + for proj in self._active_projections.values(): + info = proj.getProjectionInfo() + if info: + return info + return None + + def getDisplayProjection(self, displayId: int) -> Optional["MediaProjection"]: + return None + + def addCallback(self, callback: Callable, handler: Any = None): + cid = id(callback) + self._callbacks[cid] = callback + + def removeCallback(self, callback: Callable): + cid = id(callback) + self._callbacks.pop(cid, None) + + def isMediaProjectionAllowed(self, packageName: str) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# MediaProjection +# ═══════════════════════════════════════════════════════════════ + +class MediaProjection(Stub): + """MediaProjection — 媒体投影""" + + # ═══════════════════════════════════════════════════════════ + # Callback + # ═══════════════════════════════════════════════════════════ + + class Callback(Stub): + """MediaProjection.Callback — 投影回调""" + + def onStop(self): + pass + + def onCapturedContentResize(self, width: int, height: int): + pass + + def onCapturedContentVisibilityChanged(self, isVisible: bool): + pass + + def __init__(self): + super().__init__() + self._stopped = False + self._callbacks: List[MediaProjection.Callback] = [] + self._projection_info = MediaProjectionInfo( + packageName="com.android.systemui", + uid=1000, + isVirtualDisplay=False + ) + + def createVirtualDisplay(self, name: str, width: int, height: int, + densityDpi: int, flags: int, surface: Any, + callback: Callable = None, handler: Any = None) -> Any: + from .hardware_display import VirtualDisplay + vd = VirtualDisplay(99, name, width, height, densityDpi, surface, flags) + return vd + + def registerCallback(self, callback: Callback): + self._callbacks.append(callback) + + def unregisterCallback(self, callback: Callback): + if callback in self._callbacks: + self._callbacks.remove(callback) + + def stop(self): + self._stopped = True + for cb in self._callbacks: + cb.onStop() + + def getProjectionInfo(self) -> "MediaProjectionInfo": + return self._projection_info + + def getDisplay(self) -> Optional[Any]: + from .hardware_display import Display + return Display(displayId=0, name="Projection Display") + + def applyTransparentRegionOverlay(self, region: Any): + pass + + def removeTransparentRegionOverlay(self): + pass + + def toString(self) -> str: + return f"MediaProjection(stopped={self._stopped})" + + +# ═══════════════════════════════════════════════════════════════ +# MediaProjectionInfo +# ═══════════════════════════════════════════════════════════════ + +class MediaProjectionInfo(Stub): + """MediaProjectionInfo — 媒体投影信息""" + + def __init__(self, packageName: str = "", uid: int = 0, + isVirtualDisplay: bool = False): + super().__init__() + self._package_name = packageName + self._uid = uid + self._is_virtual_display = isVirtualDisplay + + def getProjectionType(self) -> int: + return 0 + + def getLaunchedFromPackage(self) -> str: + return self._package_name + + def getLaunchedFromUid(self) -> int: + return self._uid + + def isVirtualDisplay(self) -> bool: + return self._is_virtual_display + + def equals(self, other: Any) -> bool: + if not isinstance(other, MediaProjectionInfo): + return False + return (self._package_name == other._package_name + and self._uid == other._uid) + + def hashCode(self) -> int: + return hash((self._package_name, self._uid)) & 0x7FFFFFFF + + def toString(self) -> str: + return f"MediaProjectionInfo(package={self._package_name}, uid={self._uid})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "MediaProjectionManager", + "MediaProjection", + "MediaProjectionInfo", +] \ No newline at end of file diff --git a/android_pyapi/android/media_session.py b/android_pyapi/android/media_session.py new file mode 100644 index 0000000..b848285 --- /dev/null +++ b/android_pyapi/android/media_session.py @@ -0,0 +1,854 @@ +""" +Android media.session 组件 — MediaSession, MediaController, PlaybackState, MediaMetadata 等 +方法签名完全对齐 Android SDK +""" + +import time +from typing import Any, Callable, Dict, List, Optional, Set, Union + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# MediaSession +# ═══════════════════════════════════════════════════════════════ + +class MediaSession(Stub): + """MediaSession — 媒体会话""" + + FLAG_HANDLES_MEDIA_BUTTONS = 1 + FLAG_HANDLES_TRANSPORT_CONTROLS = 2 + FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 4 + + # ═══════════════════════════════════════════════════════════ + # Callback + # ═══════════════════════════════════════════════════════════ + + class Callback(Stub): + """MediaSession.Callback — 会话回调""" + + def onPlay(self): + pass + + def onPause(self): + pass + + def onSkipToNext(self): + pass + + def onSkipToPrevious(self): + pass + + def onFastForward(self): + pass + + def onRewind(self): + pass + + def onStop(self): + pass + + def onSeekTo(self, pos: int): + pass + + def onSetRating(self, rating: "Rating"): + pass + + def onSetRepeatMode(self, repeatMode: int): + pass + + def onSetShuffleMode(self, shuffleMode: int): + pass + + def onCommand(self, command: str, extras: Any, cb: Any): + pass + + def onCustomAction(self, action: str, extras: Any): + pass + + def onMediaButtonEvent(self, mediaButtonIntent: Any) -> bool: + return False + + def onPlayFromMediaId(self, mediaId: str, extras: Any): + pass + + def onPlayFromSearch(self, query: str, extras: Any): + pass + + def onPlayFromUri(self, uri: str, extras: Any): + pass + + def onPrepare(self): + pass + + def onPrepareFromMediaId(self, mediaId: str, extras: Any): + pass + + def onPrepareFromSearch(self, query: str, extras: Any): + pass + + def onPrepareFromUri(self, uri: str, extras: Any): + pass + + def __init__(self, context: Any = None, tag: str = "", + token: "SessionToken" = None): + super().__init__() + self._tag = tag + self._token = token or SessionToken() + self._active = False + self._flags = 0 + self._session_activity: Any = None + self._callback: Optional[MediaSession.Callback] = None + self._metadata: Optional["MediaMetadata"] = None + self._playback_state: Optional["PlaybackState"] = None + self._queue: List["QueueItem"] = [] + self._queue_title: Optional[str] = None + self._extras: Any = None + self._rating_type = Rating.RATING_NONE + self._repeat_mode = PlaybackState.REPEAT_MODE_NONE + self._shuffle_mode = PlaybackState.SHUFFLE_MODE_NONE + self._captioning_enabled = False + self._controllers: List[MediaController] = [] + + def setSessionActivity(self, sessionActivity: Any): + self._session_activity = sessionActivity + + def sendSessionEvent(self, event: str, extras: Any): + for ctrl in self._controllers: + ctrl._callback.onSessionEvent(event, extras) + + def release(self): + self._active = False + self._controllers.clear() + + def getSessionToken(self) -> "SessionToken": + return self._token + + def setFlags(self, flags: int): + self._flags = flags + + def setPlaybackToLocal(self, volumeProvider: Any = None): + pass + + def setPlaybackToRemote(self, volumeProvider: Any): + pass + + def setActive(self, active: bool): + self._active = active + + def isActive(self) -> bool: + return self._active + + def setMediaMetadata(self, metadata: "MediaMetadata"): + self._metadata = metadata + for ctrl in self._controllers: + ctrl._callback.onMetadataChanged(metadata) + + def setPlaybackState(self, state: "PlaybackState"): + self._playback_state = state + for ctrl in self._controllers: + ctrl._callback.onPlaybackStateChanged(state) + + def setQueue(self, queue: List["QueueItem"]): + self._queue = queue + for ctrl in self._controllers: + ctrl._callback.onQueueChanged(queue) + + def setQueueTitle(self, title: str): + self._queue_title = title + for ctrl in self._controllers: + ctrl._callback.onQueueTitleChanged(title) + + def setExtras(self, extras: Any): + self._extras = extras + for ctrl in self._controllers: + ctrl._callback.onExtrasChanged(extras) + + def setMetadata(self, metadata: "MediaMetadata"): + self.setMediaMetadata(metadata) + + def setRatingType(self, ratingType: int): + self._rating_type = ratingType + + def setRepeatMode(self, repeatMode: int): + self._repeat_mode = repeatMode + for ctrl in self._controllers: + ctrl._callback.onRepeatModeChanged(repeatMode) + + def setShuffleMode(self, shuffleMode: int): + self._shuffle_mode = shuffleMode + for ctrl in self._controllers: + ctrl._callback.onShuffleModeChanged(shuffleMode) + + def setCaptioningEnabled(self, enabled: bool): + self._captioning_enabled = enabled + for ctrl in self._controllers: + ctrl._callback.onCaptioningEnabledChanged(enabled) + + def getController(self) -> "MediaController": + ctrl = MediaController(self) + self._controllers.append(ctrl) + return ctrl + + def getCallerPackageName(self) -> str: + return "com.android.systemui" + + def setCallback(self, callback: Callback, handler: Any = None): + self._callback = callback + + +# ═══════════════════════════════════════════════════════════════ +# MediaController +# ═══════════════════════════════════════════════════════════════ + +class MediaController(Stub): + """MediaController — 媒体控制器""" + + # ═══════════════════════════════════════════════════════════ + # Callback + # ═══════════════════════════════════════════════════════════ + + class Callback(Stub): + """MediaController.Callback — 控制器回调""" + + def onSessionReady(self): + pass + + def onSessionDestroyed(self): + pass + + def onSessionEvent(self, event: str, extras: Any): + pass + + def onPlaybackStateChanged(self, state: "PlaybackState"): + pass + + def onMetadataChanged(self, metadata: "MediaMetadata"): + pass + + def onQueueChanged(self, queue: List["QueueItem"]): + pass + + def onQueueTitleChanged(self, title: str): + pass + + def onExtrasChanged(self, extras: Any): + pass + + def onAudioInfoChanged(self, info: Any): + pass + + def onCaptioningEnabledChanged(self, enabled: bool): + pass + + def onRepeatModeChanged(self, repeatMode: int): + pass + + def onShuffleModeChanged(self, shuffleMode: int): + pass + + # ═══════════════════════════════════════════════════════════ + # TransportControls + # ═══════════════════════════════════════════════════════════ + + class TransportControls(Stub): + """MediaController.TransportControls — 传输控制""" + + def __init__(self, session: "MediaSession"): + super().__init__() + self._session = session + + def play(self): + if self._session._callback: + self._session._callback.onPlay() + + def pause(self): + if self._session._callback: + self._session._callback.onPause() + + def stop(self): + if self._session._callback: + self._session._callback.onStop() + + def fastForward(self): + if self._session._callback: + self._session._callback.onFastForward() + + def rewind(self): + if self._session._callback: + self._session._callback.onRewind() + + def skipToNext(self): + if self._session._callback: + self._session._callback.onSkipToNext() + + def skipToPrevious(self): + if self._session._callback: + self._session._callback.onSkipToPrevious() + + def seekTo(self, pos: int): + if self._session._callback: + self._session._callback.onSeekTo(pos) + + def setRating(self, rating: "Rating"): + if self._session._callback: + self._session._callback.onSetRating(rating) + + def setRepeatMode(self, repeatMode: int): + if self._session._callback: + self._session._callback.onSetRepeatMode(repeatMode) + + def setShuffleMode(self, shuffleMode: int): + if self._session._callback: + self._session._callback.onSetShuffleMode(shuffleMode) + + def sendCustomAction(self, action: str, extras: Any): + if self._session._callback: + self._session._callback.onCustomAction(action, extras) + + def playFromMediaId(self, mediaId: str, extras: Any): + if self._session._callback: + self._session._callback.onPlayFromMediaId(mediaId, extras) + + def playFromSearch(self, query: str, extras: Any): + if self._session._callback: + self._session._callback.onPlayFromSearch(query, extras) + + def playFromUri(self, uri: str, extras: Any): + if self._session._callback: + self._session._callback.onPlayFromUri(uri, extras) + + def prepare(self): + if self._session._callback: + self._session._callback.onPrepare() + + def prepareFromMediaId(self, mediaId: str, extras: Any): + if self._session._callback: + self._session._callback.onPrepareFromMediaId(mediaId, extras) + + def prepareFromSearch(self, query: str, extras: Any): + if self._session._callback: + self._session._callback.onPrepareFromSearch(query, extras) + + def prepareFromUri(self, uri: str, extras: Any): + if self._session._callback: + self._session._callback.onPrepareFromUri(uri, extras) + + def __init__(self, session: "MediaSession" = None): + super().__init__() + self._session = session + self._callback = MediaController.Callback() + self._transport_controls = MediaController.TransportControls(session) + + def getSessionToken(self) -> "SessionToken": + return self._session.getSessionToken() if self._session else SessionToken() + + def getTransportControls(self) -> TransportControls: + return self._transport_controls + + def getPlaybackState(self) -> Optional["PlaybackState"]: + return self._session._playback_state if self._session else None + + def getMetadata(self) -> Optional["MediaMetadata"]: + return self._session._metadata if self._session else None + + def getQueueTitle(self) -> Optional[str]: + return self._session._queue_title if self._session else None + + def getQueue(self) -> List["QueueItem"]: + return self._session._queue if self._session else [] + + def getRepeatMode(self) -> int: + return self._session._repeat_mode if self._session else PlaybackState.REPEAT_MODE_NONE + + def getShuffleMode(self) -> int: + return self._session._shuffle_mode if self._session else PlaybackState.SHUFFLE_MODE_NONE + + def getRatingType(self) -> int: + return self._session._rating_type if self._session else Rating.RATING_NONE + + def getExtras(self) -> Any: + return self._session._extras if self._session else None + + def getFlags(self) -> int: + return self._session._flags if self._session else 0 + + def getPackageName(self) -> str: + return self._session.getCallerPackageName() if self._session else "" + + def getSessionActivity(self) -> Any: + return self._session._session_activity if self._session else None + + def registerCallback(self, callback: Callback, handler: Any = None): + self._callback = callback + + def unregisterCallback(self, callback: Callback): + self._callback = MediaController.Callback() + + def sendCommand(self, command: str, extras: Any, cb: Any): + if self._session and self._session._callback: + self._session._callback.onCommand(command, extras, cb) + + def adjustVolume(self, direction: int, flags: int): + pass + + def setVolumeTo(self, value: int, flags: int): + pass + + def isSessionReady(self) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# PlaybackState +# ═══════════════════════════════════════════════════════════════ + +class PlaybackState(Stub): + """PlaybackState — 播放状态""" + + STATE_NONE = 0 + STATE_STOPPED = 1 + STATE_PAUSED = 2 + STATE_PLAYING = 3 + STATE_FAST_FORWARDING = 4 + STATE_REWINDING = 5 + STATE_BUFFERING = 6 + STATE_ERROR = 7 + STATE_CONNECTING = 8 + STATE_SKIPPING_TO_PREVIOUS = 9 + STATE_SKIPPING_TO_NEXT = 10 + STATE_SKIPPING_TO_QUEUE_ITEM = 11 + + ACTION_PLAY = 1 << 0 + ACTION_PAUSE = 1 << 1 + ACTION_PLAY_PAUSE = 1 << 2 + ACTION_STOP = 1 << 3 + ACTION_SEEK_TO = 1 << 4 + ACTION_FAST_FORWARD = 1 << 5 + ACTION_REWIND = 1 << 6 + ACTION_SKIP_TO_NEXT = 1 << 7 + ACTION_SKIP_TO_PREVIOUS = 1 << 8 + ACTION_SET_RATING = 1 << 9 + ACTION_SET_REPEAT_MODE = 1 << 10 + ACTION_SET_SHUFFLE_MODE = 1 << 11 + ACTION_SET_CAPTIONING_ENABLED = 1 << 12 + ACTION_SKIP_TO_QUEUE_ITEM = 1 << 13 + ACTION_PLAY_FROM_MEDIA_ID = 1 << 14 + ACTION_PLAY_FROM_SEARCH = 1 << 15 + ACTION_PREPARE = 1 << 16 + ACTION_PREPARE_FROM_MEDIA_ID = 1 << 17 + ACTION_PREPARE_FROM_SEARCH = 1 << 18 + ACTION_PREPARE_FROM_URI = 1 << 19 + ACTION_PLAY_FROM_URI = 1 << 20 + + REPEAT_MODE_NONE = 0 + REPEAT_MODE_ONE = 1 + REPEAT_MODE_ALL = 2 + REPEAT_MODE_GROUP = 3 + + SHUFFLE_MODE_NONE = 0 + SHUFFLE_MODE_ALL = 1 + SHUFFLE_MODE_GROUP = 2 + + class Builder(Stub): + """PlaybackState.Builder — 播放状态构建器""" + + def __init__(self): + super().__init__() + self._state = PlaybackState.STATE_NONE + self._position = 0 + self._speed = 1.0 + self._actions = 0 + self._custom_actions: List[Any] = [] + self._active_queue_item_id = -1 + self._error_message: Optional[str] = None + self._extras: Any = None + self._buffered_position = 0 + + def setState(self, state: int, position: int, speed: float) -> "PlaybackState.Builder": + self._state = state + self._position = position + self._speed = speed + return self + + def setActions(self, actions: int) -> "PlaybackState.Builder": + self._actions = actions + return self + + def setCustomAction(self, action: str, name: str, icon: Any = None) -> "PlaybackState.Builder": + self._custom_actions.append((action, name, icon)) + return self + + def setActiveQueueItemId(self, id: int) -> "PlaybackState.Builder": + self._active_queue_item_id = id + return self + + def setErrorMessage(self, error: str) -> "PlaybackState.Builder": + self._error_message = error + return self + + def setExtras(self, extras: Any) -> "PlaybackState.Builder": + self._extras = extras + return self + + def setBufferedPosition(self, bufferedPosition: int) -> "PlaybackState.Builder": + self._buffered_position = bufferedPosition + return self + + def setPosition(self, position: int) -> "PlaybackState.Builder": + self._position = position + return self + + def setSpeed(self, speed: float) -> "PlaybackState.Builder": + self._speed = speed + return self + + def addCustomAction(self, action: "PlaybackState.CustomAction") -> "PlaybackState.Builder": + self._custom_actions.append(action) + return self + + def build(self) -> "PlaybackState": + ps = PlaybackState() + ps._state = self._state + ps._position = self._position + ps._speed = self._speed + ps._actions = self._actions + ps._custom_actions = self._custom_actions + ps._active_queue_item_id = self._active_queue_item_id + ps._error_message = self._error_message + ps._extras = self._extras + ps._buffered_position = self._buffered_position + return ps + + class CustomAction(Stub): + """PlaybackState.CustomAction — 自定义操作""" + + def __init__(self, action: str, name: str, icon: Any = None): + super().__init__() + self._action = action + self._name = name + self._icon = icon + + def getAction(self) -> str: + return self._action + + def getName(self) -> str: + return self._name + + def getIcon(self) -> Any: + return self._icon + + def __init__(self): + super().__init__() + self._state = PlaybackState.STATE_NONE + self._position = 0 + self._speed = 1.0 + self._actions = 0 + self._custom_actions: List[Any] = [] + self._active_queue_item_id = -1 + self._error_message: Optional[str] = None + self._extras: Any = None + self._buffered_position = 0 + self._last_position_update_time = int(time.time() * 1000) + + def getState(self) -> int: + return self._state + + def getPosition(self) -> int: + return self._position + + def getPlaybackSpeed(self) -> float: + return self._speed + + def getActions(self) -> int: + return self._actions + + def getCustomActions(self) -> List[Any]: + return self._custom_actions + + def getActiveQueueItemId(self) -> int: + return self._active_queue_item_id + + def getErrorMessage(self) -> Optional[str]: + return self._error_message + + def getExtras(self) -> Any: + return self._extras + + def getBufferedPosition(self) -> int: + return self._buffered_position + + def getLastPositionUpdateTime(self) -> int: + return self._last_position_update_time + + def toString(self) -> str: + state_names = { + PlaybackState.STATE_NONE: "NONE", + PlaybackState.STATE_STOPPED: "STOPPED", + PlaybackState.STATE_PAUSED: "PAUSED", + PlaybackState.STATE_PLAYING: "PLAYING", + } + return f"PlaybackState(state={state_names.get(self._state, str(self._state))}," \ + f" pos={self._position})" + + +# ═══════════════════════════════════════════════════════════════ +# MediaMetadata +# ═══════════════════════════════════════════════════════════════ + +class MediaMetadata(Stub): + """MediaMetadata — 媒体元数据""" + + METADATA_KEY_ALBUM = "android.media.metadata.ALBUM" + METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART" + METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST" + METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI" + METADATA_KEY_ART = "android.media.metadata.ART" + METADATA_KEY_ARTIST = "android.media.metadata.ARTIST" + METADATA_KEY_ART_URI = "android.media.metadata.ART_URI" + METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR" + METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION" + METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER" + METADATA_KEY_DATE = "android.media.metadata.DATE" + METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER" + METADATA_KEY_DISPLAY_DESCRIPTION = "android.media.metadata.DISPLAY_DESCRIPTION" + METADATA_KEY_DISPLAY_ICON = "android.media.metadata.DISPLAY_ICON" + METADATA_KEY_DISPLAY_ICON_URI = "android.media.metadata.DISPLAY_ICON_URI" + METADATA_KEY_DISPLAY_SUBTITLE = "android.media.metadata.DISPLAY_SUBTITLE" + METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE" + METADATA_KEY_DURATION = "android.media.metadata.DURATION" + METADATA_KEY_GENRE = "android.media.metadata.GENRE" + METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID" + METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS" + METADATA_KEY_RATING = "android.media.metadata.RATING" + METADATA_KEY_TITLE = "android.media.metadata.TITLE" + METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER" + METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING" + METADATA_KEY_WRITER = "android.media.metadata.WRITER" + METADATA_KEY_YEAR = "android.media.metadata.YEAR" + + class Builder(Stub): + """MediaMetadata.Builder — 元数据构建器""" + + def __init__(self): + super().__init__() + self._data: Dict[str, Any] = {} + + def putString(self, key: str, value: str) -> "MediaMetadata.Builder": + self._data[key] = value + return self + + def putLong(self, key: str, value: int) -> "MediaMetadata.Builder": + self._data[key] = value + return self + + def putBitmap(self, key: str, value: Any) -> "MediaMetadata.Builder": + self._data[key] = value + return self + + def putText(self, key: str, value: str) -> "MediaMetadata.Builder": + self._data[key] = value + return self + + def putRating(self, key: str, value: "Rating") -> "MediaMetadata.Builder": + self._data[key] = value + return self + + def build(self) -> "MediaMetadata": + mm = MediaMetadata() + mm._data = dict(self._data) + return mm + + def __init__(self): + super().__init__() + self._data: Dict[str, Any] = {} + + def getString(self, key: str) -> Optional[str]: + v = self._data.get(key) + return str(v) if v is not None else None + + def getLong(self, key: str) -> int: + v = self._data.get(key, 0) + try: + return int(v) + except (ValueError, TypeError): + return 0 + + def getBitmap(self, key: str) -> Any: + return self._data.get(key) + + def getText(self, key: str) -> Optional[str]: + return self.getString(key) + + def getDescription(self) -> "MediaDescription": + from .media_browse import MediaDescription + builder = MediaDescription.Builder() + title = self.getString(MediaMetadata.METADATA_KEY_TITLE) + if title: + builder.setTitle(title) + artist = self.getString(MediaMetadata.METADATA_KEY_ARTIST) + if artist: + builder.setSubtitle(artist) + media_id = self.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) + if media_id: + builder.setMediaId(media_id) + return builder.build() + + def getRating(self, key: str) -> Optional["Rating"]: + v = self._data.get(key) + return v if isinstance(v, Rating) else None + + def getBitmapDescription(self) -> Any: + return None + + def containsKey(self, key: str) -> bool: + return key in self._data + + def keySet(self) -> Set[str]: + return set(self._data.keys()) + + def getMediaId(self) -> Optional[str]: + return self.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) + + def toString(self) -> str: + title = self.getString(MediaMetadata.METADATA_KEY_TITLE) + return f"MediaMetadata(title={title})" + + +# ═══════════════════════════════════════════════════════════════ +# QueueItem +# ═══════════════════════════════════════════════════════════════ + +class QueueItem(Stub): + """QueueItem — 队列项""" + + def __init__(self, description: "MediaDescription" = None, queueId: int = 0): + super().__init__() + self._description = description + self._queue_id = queueId + + def getDescription(self) -> "MediaDescription": + return self._description + + def getQueueId(self) -> int: + return self._queue_id + + def toString(self) -> str: + return f"QueueItem(id={self._queue_id}, desc={self._description})" + + +# ═══════════════════════════════════════════════════════════════ +# Rating +# ═══════════════════════════════════════════════════════════════ + +class Rating(Stub): + """Rating — 评分""" + + RATING_NONE = 0 + RATING_HEART = 1 + RATING_THUMB_UP_DOWN = 2 + RATING_3_STARS = 3 + RATING_4_STARS = 4 + RATING_5_STARS = 5 + RATING_PERCENTAGE = 6 + + def __init__(self, ratingStyle: int = RATING_NONE): + super().__init__() + self._rating_style = ratingStyle + self._rated = False + self._heart = False + self._thumb_up = False + self._star_rating = 0.0 + self._percent_rating = 0.0 + + @staticmethod + def newHeartRating(hasHeart: bool) -> "Rating": + r = Rating(Rating.RATING_HEART) + r._rated = True + r._heart = hasHeart + return r + + @staticmethod + def newThumbRating(thumbUp: bool) -> "Rating": + r = Rating(Rating.RATING_THUMB_UP_DOWN) + r._rated = True + r._thumb_up = thumbUp + return r + + @staticmethod + def newStarRating(starRatingStyle: int, starRating: float) -> "Rating": + r = Rating(starRatingStyle) + r._rated = True + r._star_rating = starRating + return r + + @staticmethod + def newPercentageRating(percent: float) -> "Rating": + r = Rating(Rating.RATING_PERCENTAGE) + r._rated = True + r._percent_rating = percent + return r + + @staticmethod + def newUnratedRating(ratingStyle: int) -> "Rating": + return Rating(ratingStyle) + + def isRated(self) -> bool: + return self._rated + + def getRatingStyle(self) -> int: + return self._rating_style + + def hasHeart(self) -> bool: + return self._heart + + def isThumbUp(self) -> bool: + return self._thumb_up + + def getStarRating(self) -> float: + return self._star_rating + + def getPercentRating(self) -> float: + return self._percent_rating + + def toString(self) -> str: + return f"Rating(style={self._rating_style}, rated={self._rated})" + + +# ═══════════════════════════════════════════════════════════════ +# SessionToken +# ═══════════════════════════════════════════════════════════════ + +class SessionToken(Stub): + """SessionToken — 会话令牌""" + + TYPE_SESSION = 0 + + def __init__(self, type_: int = TYPE_SESSION): + super().__init__() + self._type = type_ + self._binder = None + + def getType(self) -> int: + return self._type + + def getSessionBinder(self) -> Any: + return self._binder + + def toString(self) -> str: + return f"SessionToken(type={self._type})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "MediaSession", + "MediaController", + "PlaybackState", + "MediaMetadata", + "QueueItem", + "Rating", + "SessionToken", +] \ No newline at end of file diff --git a/android_pyapi/android/media_tv.py b/android_pyapi/android/media_tv.py new file mode 100644 index 0000000..07b9882 --- /dev/null +++ b/android_pyapi/android/media_tv.py @@ -0,0 +1,359 @@ +""" +Android media.tv 组件 — TvInputManager, TvInputInfo, TvInputService 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# TvInputManager +# ═══════════════════════════════════════════════════════════════ + +class TvInputManager(Stub): + """TvInputManager — 电视输入管理器""" + + ACTION_VIEW_RECORDING_SCHEDULES = "android.tv.action.VIEW_RECORDING_SCHEDULES" + ACTION_WATCH_NEXT = "android.tv.action.WATCH_NEXT" + + TIME_SHIFT_STATUS_AVAILABLE = 0 + TIME_SHIFT_STATUS_UNAVAILABLE = 1 + TIME_SHIFT_STATUS_UNKNOWN = 2 + TIME_SHIFT_STATUS_UNSUPPORTED = 3 + + VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY = 8 + VIDEO_UNAVAILABLE_REASON_BUFFERING = 3 + VIDEO_UNAVAILABLE_REASON_CAS = 7 + VIDEO_UNAVAILABLE_REASON_INSUFFICIENT_RESOURCE = 5 + VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED = 6 + VIDEO_UNAVAILABLE_REASON_PREEMPTED = 4 + VIDEO_UNAVAILABLE_REASON_TUNING = 1 + VIDEO_UNAVAILABLE_REASON_UNKNOWN = 0 + VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL = 2 + + # ═══════════════════════════════════════════════════════════ + # TvInputCallback + # ═══════════════════════════════════════════════════════════ + + class TvInputCallback(Stub): + """TvInputManager.TvInputCallback — 电视输入回调""" + + def onInputAdded(self, inputId: str): + pass + + def onInputRemoved(self, inputId: str): + pass + + def onInputChanged(self, inputId: str): + pass + + def onInputStateChanged(self, inputId: str, state: int): + pass + + def onTvInputInfoUpdated(self, inputInfo: "TvInputInfo"): + pass + + def __init__(self): + super().__init__() + self._inputs: Dict[str, TvInputInfo] = {} + self._callbacks: Dict[int, TvInputManager.TvInputCallback] = {} + + def getTvInputList(self) -> List["TvInputInfo"]: + return list(self._inputs.values()) + + def getTvInputInfo(self, inputId: str) -> Optional["TvInputInfo"]: + return self._inputs.get(inputId) + + def getCurrentTvInputInfo(self) -> Optional["TvInputInfo"]: + for inp in self._inputs.values(): + return inp + return None + + def updateTvInputInfo(self, inputInfo: "TvInputInfo"): + if inputInfo: + self._inputs[inputInfo.getId()] = inputInfo + + def registerCallback(self, callback: TvInputCallback, handler: Any = None): + lid = id(callback) + self._callbacks[lid] = callback + + def unregisterCallback(self, callback: TvInputCallback): + lid = id(callback) + self._callbacks.pop(lid, None) + + def createRecordingSession(self, recordingService: str, inputId: str): + raise AndroidOnlyError("TvInputManager.createRecordingSession() is not available on desktop") + + def sendTvInputNotifyIntent(self, intent: Any, inputId: str): + pass + + def isParentalControlsEnabled(self) -> bool: + return False + + def isRatingBlocked(self, rating: str) -> bool: + return False + + def getBlockedRatings(self) -> List[str]: + return [] + + def getAvailableTvInputs(self) -> List[str]: + return list(self._inputs.keys()) + + def getTvContentRatingSystems(self) -> List[str]: + return ["US_TV", "US_MOVIE"] + + +# ═══════════════════════════════════════════════════════════════ +# TvInputInfo +# ═══════════════════════════════════════════════════════════════ + +class TvInputInfo(Stub): + """TvInputInfo — 电视输入信息""" + + TYPE_COMPONENT = 1005 + TYPE_COMPOSITE = 1004 + TYPE_HDMI = 1007 + TYPE_OTHER = 1001 + TYPE_SCART = 1003 + TYPE_SVIDEO = 1006 + TYPE_TUNER = 0 + TYPE_DISPLAY_LINK = 1008 + TYPE_VGA = 1002 + + def __init__(self, id: str = "", serviceInfo: Any = None, + parentId: str = "", type_: int = TYPE_TUNER, + hdmiDeviceInfo: Any = None, + isHidden: bool = False, canRecord: bool = False, + canPauseRecording: bool = False): + super().__init__() + self._id = id + self._service_info = serviceInfo + self._parent_id = parentId + self._type = type_ + self._hdmi_device_info = hdmiDeviceInfo + self._is_hidden = isHidden + self._can_record = canRecord + self._can_pause_recording = canPauseRecording + + def getId(self) -> str: + return self._id + + def getServiceInfo(self) -> Any: + return self._service_info + + def getParentId(self) -> str: + return self._parent_id + + def getType(self) -> int: + return self._type + + def getHdmiDeviceInfo(self) -> Any: + return self._hdmi_device_info + + def loadIcon(self, context: Any) -> Any: + return None + + def loadLogo(self, context: Any) -> Any: + return None + + def createIntentForSetupScreen(self) -> Any: + return None + + def isHidden(self) -> bool: + return self._is_hidden + + def canRecord(self) -> bool: + return self._can_record + + def canPauseRecording(self) -> bool: + return self._can_pause_recording + + def toString(self) -> str: + type_names = { + TvInputInfo.TYPE_TUNER: "TUNER", + TvInputInfo.TYPE_HDMI: "HDMI", + TvInputInfo.TYPE_COMPOSITE: "COMPOSITE", + TvInputInfo.TYPE_COMPONENT: "COMPONENT", + } + return f"TvInputInfo(id={self._id}, type={type_names.get(self._type, str(self._type))})" + + +# ═══════════════════════════════════════════════════════════════ +# TvInputService +# ═══════════════════════════════════════════════════════════════ + +class TvInputService(Stub): + """TvInputService — 电视输入服务""" + + RECORDING_ERROR_RECORDING_LOST = 0 + RECORDING_ERROR_TIMEOUT = 1 + RECORDING_ERROR_OTHER = 2 + + # ═══════════════════════════════════════════════════════════ + # Session + # ═══════════════════════════════════════════════════════════ + + class Session(Stub): + """TvInputService.Session — 电视会话""" + + def __init__(self): + super().__init__() + self._main = False + self._stream_volume = 1.0 + self._caption_enabled = False + + def onSetSurface(self, surface: Any) -> bool: + return True + + def onSurfaceChanged(self, format: int, width: int, height: int): + pass + + def onSurfaceReleased(self): + pass + + def onAppPrivateCommand(self, action: str, data: Any): + pass + + def onCreateOverlayView(self) -> Any: + return None + + def onRelayoutContent(self): + pass + + def onSetCaptionEnabled(self, enabled: bool): + self._caption_enabled = enabled + + def onSetStreamVolume(self, volume: float): + self._stream_volume = volume + + def onTimeShiftSetCurrentPositionRequest(self, positionMs: int): + pass + + def notifyTimeShiftStatusChanged(self, status: int): + pass + + def scheduleOverlayView(self, view: Any): + pass + + def timeShiftPlay(self, uri: str): + pass + + def timeShiftPause(self): + pass + + def timeShiftResume(self): + pass + + def timeShiftSeekTo(self, positionMs: int): + pass + + def timeShiftSetPlaybackParams(self, params: Any): + pass + + def timeShiftEnablePositionTracking(self, enable: bool): + pass + + def notifyVideoAvailable(self): + pass + + def notifyVideoUnavailable(self, reason: int): + pass + + def notifySessionEvent(self, eventType: str, eventArgs: Any): + pass + + def notifyTracksChanged(self, tracks: List[Any]): + pass + + def notifyTrackSelected(self, type_: str, trackId: str): + pass + + def notifyContentAllowed(self): + pass + + def notifyContentBlocked(self, rating: str): + pass + + def setMain(self, main: bool): + self._main = main + + def setStreamVolume(self, volume: float): + self._stream_volume = volume + + def __init__(self): + super().__init__() + self._sessions: List[TvInputService.Session] = [] + + def onCreateSession(self, inputId: str) -> Session: + session = TvInputService.Session() + self._sessions.append(session) + return session + + def onSetupFinished(self, inputId: str): + pass + + def sessionCreated(self, session: Session): + pass + + def notifyHardwareFailed(self, deviceId: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# TvRecordingClient / TvRecordingService (stubs) +# ═══════════════════════════════════════════════════════════════ + +class TvRecordingClient(Stub): + """TvRecordingClient — 电视录制客户端""" + + def __init__(self): + super().__init__() + + def startRecording(self, uri: str): + pass + + def stopRecording(self): + pass + + def pauseRecording(self): + pass + + def resumeRecording(self): + pass + + def release(self): + pass + + +class TvRecordingService(Stub): + """TvRecordingService — 电视录制服务""" + + def __init__(self): + super().__init__() + + def onStartRecording(self, inputId: str, sessionId: str): + pass + + def onStopRecording(self, sessionId: str): + pass + + def onPauseRecording(self, sessionId: str): + pass + + def onResumeRecording(self, sessionId: str): + pass + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "TvInputManager", + "TvInputInfo", + "TvInputService", + "TvRecordingClient", + "TvRecordingService", +] \ No newline at end of file diff --git a/android_pyapi/android/mtp.py b/android_pyapi/android/mtp.py new file mode 100644 index 0000000..c6c038e --- /dev/null +++ b/android_pyapi/android/mtp.py @@ -0,0 +1,476 @@ +""" +Android Media Transfer Protocol API — MtpDevice, MtpStorageInfo, MtpObjectInfo, MtpConstants +方法签名完全对齐 Android SDK +""" + +import os as _os +import sys +import time +from datetime import datetime +from typing import Any, Dict, List, Optional, Set, Tuple, Callable, BinaryIO + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "MtpDevice", + "MtpStorageInfo", + "MtpObjectInfo", + "MtpConstants", + "MtpPropertyGroup", + "MtpDeviceInfo", + "MtpStorage", +] + + +# ═══════════════════════════════════════════════════════════════ +# MtpConstants — 50+ 格式常量 +# ═══════════════════════════════════════════════════════════════ + +class MtpConstants(Stub): + """MTP 协议常量""" + + # --- 格式常量 --- + FORMAT_UNDEFINED = 0x3000 + FORMAT_ASSOCIATION = 0x3001 + FORMAT_SCRIPT = 0x3002 + FORMAT_EXECUTABLE = 0x3003 + FORMAT_TEXT = 0x3004 + FORMAT_HTML = 0x3005 + FORMAT_DPOF = 0x3006 + FORMAT_AIFF = 0x3007 + FORMAT_WAV = 0x3008 + FORMAT_MP3 = 0x3009 + FORMAT_AVI = 0x300A + FORMAT_MPEG = 0x300B + FORMAT_ASF = 0x300C + FORMAT_DEF = 0x300D + FORMAT_EXIF_JPEG = 0x3801 + FORMAT_TIFF_EP = 0x3802 + FORMAT_FLASHPIX = 0x3803 + FORMAT_BMP = 0x3804 + FORMAT_CIFF = 0x3805 + FORMAT_GIF = 0x3807 + FORMAT_JFIF = 0x3808 + FORMAT_PCD = 0x3809 + FORMAT_PICT = 0x380A + FORMAT_PNG = 0x380B + FORMAT_TIFF = 0x380D + FORMAT_TIFF_IT = 0x380E + FORMAT_JP2 = 0x380F + FORMAT_JPX = 0x3810 + FORMAT_XML = 0x3811 + FORMAT_FIRMWARE = 0x3802 + FORMAT_WMA = 0xB901 + FORMAT_WMV = 0xB980 + FORMAT_WPL = 0xBA05 + FORMAT_FLAC = 0xB906 + FORMAT_OGG = 0xB902 + FORMAT_AAC = 0xB903 + FORMAT_MP4_CONTAINER = 0xB904 + FORMAT_MP2 = 0xB905 + FORMAT_3GP = 0xB907 + FORMAT_3GP_CONTAINER = 0xB908 + FORMAT_AVC = 0xB909 + FORMAT_HEVC = 0xB90A + FORMAT_DNG = 0xB90B + FORMAT_ARW = 0xB90C + FORMAT_CR2 = 0xB90D + FORMAT_RAF = 0xB90E + FORMAT_NEF = 0xB90F + FORMAT_ORF = 0xB910 + FORMAT_RW2 = 0xB911 + FORMAT_SRW = 0xB912 + + # --- 保护状态 --- + PROTECTION_STATUS_NONE = 0 + PROTECTION_STATUS_READ_ONLY = 0x8001 + PROTECTION_STATUS_PROTECTED = 0x8002 + + # --- 关联类型 --- + ASSOCIATION_TYPE_UNDEFINED = 0x0000 + ASSOCIATION_TYPE_GENERIC_FOLDER = 0x0001 + + # --- 属性格式常量 --- + PROP_FORMAT_UNDEFINED = 0x0000 + PROP_FORMAT_STRING = 0xFFFF + PROP_FORMAT_UINT8 = 0x0001 + PROP_FORMAT_UINT16 = 0x0002 + PROP_FORMAT_UINT32 = 0x0004 + PROP_FORMAT_UINT64 = 0x0008 + PROP_FORMAT_INT8 = 0x0009 + PROP_FORMAT_INT16 = 0x000A + PROP_FORMAT_INT32 = 0x000B + PROP_FORMAT_INT64 = 0x000C + PROP_FORMAT_BOOL = 0x4001 + PROP_FORMAT_DATE = 0x4002 + + # --- 操作码 --- + OPERATION_GET_DEVICE_INFO = 0x1001 + OPERATION_OPEN_SESSION = 0x1002 + OPERATION_CLOSE_SESSION = 0x1003 + OPERATION_GET_STORAGE_IDS = 0x1004 + OPERATION_GET_STORAGE_INFO = 0x1005 + OPERATION_GET_OBJECT_PROP_VALUE = 0x9802 + OPERATION_SET_OBJECT_PROP_VALUE = 0x9803 + OPERATION_GET_OBJECT_INFO = 0x1007 + OPERATION_GET_OBJECT = 0x1009 + OPERATION_GET_THUMB = 0x100A + OPERATION_DELETE_OBJECT = 0x100B + OPERATION_SEND_OBJECT_INFO = 0x100C + OPERATION_SEND_OBJECT = 0x100D + OPERATION_GET_OBJECT_REFERENCES = 0x1010 + OPERATION_SET_OBJECT_REFERENCES = 0x1011 + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# MtpDevice +# ═══════════════════════════════════════════════════════════════ + +class MtpDevice(Stub): + """MtpDevice — MTP 设备 (桌面存根)""" + + def __init__(self, device: Any = None): + super().__init__() + self._device = device + self._opened = False + + def open(self) -> bool: + """打开 MTP 设备""" + raise AndroidOnlyError("MtpDevice.open() 仅在 Android 设备上可用") + + def close(self): + """关闭 MTP 设备""" + self._opened = False + + def getDeviceInfo(self) -> "MtpDeviceInfo": + """获取设备信息""" + return MtpDeviceInfo() + + def getDeviceName(self) -> str: + """获取设备名称""" + return "MTP Device" + + def getObjectHandles( + self, storageId: int, format: int, objectHandle: int + ) -> List[int]: + """获取对象句柄列表""" + return [] + + def getObjectInfo(self, objectHandle: int) -> Optional["MtpObjectInfo"]: + """获取对象信息""" + return None + + def getObject(self, objectHandle: int, dataSize: int) -> Optional[bytes]: + """获取对象数据""" + return None + + def getThumbnail(self, objectHandle: int) -> Optional[bytes]: + """获取缩略图""" + return None + + def getStorageIds(self) -> List[int]: + """获取存储 ID 列表""" + return [0x00010001] + + def getStorageInfo(self, storageId: int) -> "MtpStorageInfo": + """获取存储信息""" + return MtpStorageInfo() + + def importFile(self, objectHandle: int, destPath: str) -> bool: + """导入文件""" + raise AndroidOnlyError("MtpDevice.importFile() 仅在 Android 设备上可用") + + def deleteObject(self, objectHandle: int) -> bool: + """删除对象""" + raise AndroidOnlyError("MtpDevice.deleteObject() 仅在 Android 设备上可用") + + def getParent(self, objectHandle: int) -> int: + """获取父对象句柄""" + return 0xFFFFFFFF + + def getObjectReferences(self, objectHandle: int) -> List[int]: + """获取对象引用列表""" + return [] + + def setObjectReferences(self, objectHandle: int, references: List[int]) -> bool: + """设置对象引用""" + raise AndroidOnlyError("MtpDevice.setObjectReferences() 仅在 Android 设备上可用") + + def sendObject(self, objectHandle: int, data: bytes) -> bool: + """发送对象""" + raise AndroidOnlyError("MtpDevice.sendObject() 仅在 Android 设备上可用") + + def sendObjectInfo( + self, storageId: int, parent: int, info: "MtpObjectInfo" + ) -> int: + """发送对象信息""" + raise AndroidOnlyError("MtpDevice.sendObjectInfo() 仅在 Android 设备上可用") + + def toString(self) -> str: + return f"MtpDevice(name={self.getDeviceName()})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# MtpStorageInfo +# ═══════════════════════════════════════════════════════════════ + +class MtpStorageInfo(Stub): + """MtpStorageInfo — MTP 存储信息""" + + def __init__(self): + super().__init__() + self._storage_id = 0x00010001 + self._max_capacity = 32 * 1024 * 1024 * 1024 # 32GB + self._free_space = 16 * 1024 * 1024 * 1024 # 16GB + self._description = "Internal Storage" + self._volume_identifier = "" + + def getStorageId(self) -> int: + return self._storage_id + + def getMaxCapacity(self) -> int: + return self._max_capacity + + def getFreeSpace(self) -> int: + return self._free_space + + def getDescription(self) -> str: + return self._description + + def getVolumeIdentifier(self) -> str: + return self._volume_identifier + + def toString(self) -> str: + return (f"MtpStorageInfo(id={self._storage_id}, " + f"free={self._free_space}B)") + + def equals(self, other) -> bool: + if not isinstance(other, MtpStorageInfo): + return False + return self._storage_id == other._storage_id + + def hashCode(self) -> int: + return self._storage_id & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# MtpObjectInfo +# ═══════════════════════════════════════════════════════════════ + +class MtpObjectInfo(Stub): + """MtpObjectInfo — MTP 对象信息""" + + def __init__(self): + super().__init__() + self._object_handle = 0 + self._storage_id = 0 + self._format = MtpConstants.FORMAT_UNDEFINED + self._protection_status = MtpConstants.PROTECTION_STATUS_NONE + self._compressed_size = 0 + self._thumb_format = MtpConstants.FORMAT_UNDEFINED + self._thumb_compressed_size = 0 + self._thumb_pix_width = 0 + self._thumb_pix_height = 0 + self._image_pix_width = 0 + self._image_pix_height = 0 + self._image_pix_depth = 0 + self._parent = 0 + self._association_desc = 0 + self._association_type = 0 + self._sequence_number = 0 + self._name = "" + self._date_created = 0 + self._date_modified = 0 + self._keywords = "" + + def getObjectHandle(self) -> int: + return self._object_handle + + def getStorageId(self) -> int: + return self._storage_id + + def getFormat(self) -> int: + return self._format + + def getProtectionStatus(self) -> int: + return self._protection_status + + def getCompressedSize(self) -> int: + return self._compressed_size + + def getThumbFormat(self) -> int: + return self._thumb_format + + def getThumbCompressedSize(self) -> int: + return self._thumb_compressed_size + + def getThumbPixWidth(self) -> int: + return self._thumb_pix_width + + def getThumbPixHeight(self) -> int: + return self._thumb_pix_height + + def getImagePixWidth(self) -> int: + return self._image_pix_width + + def getImagePixHeight(self) -> int: + return self._image_pix_height + + def getImagePixDepth(self) -> int: + return self._image_pix_depth + + def getParent(self) -> int: + return self._parent + + def getAssociationDesc(self) -> int: + return self._association_desc + + def getAssociationType(self) -> int: + return self._association_type + + def getSequenceNumber(self) -> int: + return self._sequence_number + + def getName(self) -> str: + return self._name + + def getDateCreated(self) -> int: + return self._date_created + + def getDateModified(self) -> int: + return self._date_modified + + def getKeywords(self) -> str: + return self._keywords + + def toString(self) -> str: + return f"MtpObjectInfo(name={self._name}, handle={self._object_handle})" + + def equals(self, other) -> bool: + if not isinstance(other, MtpObjectInfo): + return False + return self._object_handle == other._object_handle + + def hashCode(self) -> int: + return self._object_handle & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# MtpPropertyGroup +# ═══════════════════════════════════════════════════════════════ + +class MtpPropertyGroup(Stub): + """MtpPropertyGroup — MTP 属性组""" + + def __init__(self): + super().__init__() + self._properties: Dict[int, Any] = {} + + def getPropertyValue(self, propertyCode: int) -> Optional[Any]: + return self._properties.get(propertyCode) + + def toString(self) -> str: + return f"MtpPropertyGroup(properties={len(self._properties)})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# MtpDeviceInfo +# ═══════════════════════════════════════════════════════════════ + +class MtpDeviceInfo(Stub): + """MtpDeviceInfo — MTP 设备信息""" + + def __init__(self): + super().__init__() + self._manufacturer = "Unknown" + self._model = "MTP Device" + self._device_version = "1.0" + self._serial_number = "00000000" + self._operations_supported: List[int] = [] + self._events_supported: List[int] = [] + self._device_properties_supported: List[int] = [] + + def getManufacturer(self) -> str: + return self._manufacturer + + def getModel(self) -> str: + return self._model + + def getDeviceVersion(self) -> str: + return self._device_version + + def getSerialNumber(self) -> str: + return self._serial_number + + def getOperationsSupported(self) -> List[int]: + return list(self._operations_supported) + + def getEventsSupported(self) -> List[int]: + return list(self._events_supported) + + def getDevicePropertySupported(self) -> List[int]: + return list(self._device_properties_supported) + + def toString(self) -> str: + return f"MtpDeviceInfo(manufacturer={self._manufacturer}, model={self._model})" + + def equals(self, other) -> bool: + if not isinstance(other, MtpDeviceInfo): + return False + return self._serial_number == other._serial_number + + def hashCode(self) -> int: + return hash(self._serial_number) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# MtpStorage +# ═══════════════════════════════════════════════════════════════ + +class MtpStorage(Stub): + """MtpStorage — MTP 存储""" + + def __init__(self, storageId: int = 0x00010001, path: str = ""): + super().__init__() + self._storage_id = storageId + self._path = path + self._description = "Storage" + + def getStorageId(self) -> int: + return self._storage_id + + def getPath(self) -> str: + return self._path + + def getDescription(self) -> str: + return self._description + + def toString(self) -> str: + return f"MtpStorage(id={self._storage_id}, path={self._path})" + + def equals(self, other) -> bool: + if not isinstance(other, MtpStorage): + return False + return self._storage_id == other._storage_id + + def hashCode(self) -> int: + return self._storage_id & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/opengl.py b/android_pyapi/android/opengl.py new file mode 100644 index 0000000..ef85a1c --- /dev/null +++ b/android_pyapi/android/opengl.py @@ -0,0 +1,464 @@ +""" +Android OpenGL 组件 — GLSurfaceView, GLU, GLUtils, EGL14 等 +方法签名完全对齐 Android SDK (android.opengl 包) +""" + +import math +from typing import Any, Callable, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# EGL 常量 +# ═══════════════════════════════════════════════════════════════ + +class EGL14(Stub): + """EGL14 — EGL 1.4 API 常量 (桌面仅提供常量)""" + + # EGL 布尔值 + EGL_TRUE = 1 + EGL_FALSE = 0 + + # EGL 错误码 + EGL_SUCCESS = 0x3000 + EGL_NOT_INITIALIZED = 0x3001 + EGL_BAD_ACCESS = 0x3002 + EGL_BAD_ALLOC = 0x3003 + EGL_BAD_ATTRIBUTE = 0x3004 + EGL_BAD_CONFIG = 0x3005 + EGL_BAD_CONTEXT = 0x3006 + EGL_BAD_CURRENT_SURFACE = 0x3007 + EGL_BAD_DISPLAY = 0x3008 + EGL_BAD_MATCH = 0x3009 + EGL_BAD_NATIVE_PIXMAP = 0x300A + EGL_BAD_NATIVE_WINDOW = 0x300B + EGL_BAD_PARAMETER = 0x300C + EGL_BAD_SURFACE = 0x300D + EGL_CONTEXT_LOST = 0x300E + + # EGL 配置属性 + EGL_BUFFER_SIZE = 0x3020 + EGL_ALPHA_SIZE = 0x3021 + EGL_BLUE_SIZE = 0x3022 + EGL_GREEN_SIZE = 0x3023 + EGL_RED_SIZE = 0x3024 + EGL_DEPTH_SIZE = 0x3025 + EGL_STENCIL_SIZE = 0x3026 + EGL_CONFIG_CAVEAT = 0x3027 + EGL_CONFIG_ID = 0x3028 + EGL_LEVEL = 0x3029 + EGL_MAX_PBUFFER_HEIGHT = 0x302A + EGL_MAX_PBUFFER_PIXELS = 0x302B + EGL_MAX_PBUFFER_WIDTH = 0x302C + EGL_NATIVE_RENDERABLE = 0x302D + EGL_NATIVE_VISUAL_ID = 0x302E + EGL_NATIVE_VISUAL_TYPE = 0x302F + EGL_PRESERVED_RESOURCES = 0x3030 + EGL_SAMPLES = 0x3031 + EGL_SAMPLE_BUFFERS = 0x3032 + EGL_SURFACE_TYPE = 0x3033 + EGL_TRANSPARENT_TYPE = 0x3034 + EGL_TRANSPARENT_BLUE_VALUE = 0x3035 + EGL_TRANSPARENT_GREEN_VALUE = 0x3036 + EGL_TRANSPARENT_RED_VALUE = 0x3037 + EGL_NONE = 0x3038 + EGL_BIND_TO_TEXTURE_RGB = 0x3039 + EGL_BIND_TO_TEXTURE_RGBA = 0x303A + EGL_MIN_SWAP_INTERVAL = 0x303B + EGL_MAX_SWAP_INTERVAL = 0x303C + + # EGL 表面属性 + EGL_WIDTH = 0x3057 + EGL_HEIGHT = 0x3056 + EGL_LARGEST_PBUFFER = 0x3058 + EGL_TEXTURE_FORMAT = 0x3080 + EGL_TEXTURE_TARGET = 0x3081 + EGL_MIPMAP_TEXTURE = 0x3082 + EGL_MIPMAP_LEVEL = 0x3083 + + # EGL 渲染上下文属性 + EGL_CONTEXT_CLIENT_TYPE = 0x3097 + EGL_CONTEXT_CLIENT_VERSION = 0x3098 + + # EGL 显示 + EGL_DEFAULT_DISPLAY = 0 + + # EGL 版本 + EGL_VERSION_1_4 = 1 + + # OpenGL ES 位标记 + EGL_OPENGL_ES_BIT = 0x0001 + EGL_OPENGL_ES2_BIT = 0x0004 + EGL_OPENGL_ES3_BIT = 0x0040 + + def __init__(self): + super().__init__() + + @staticmethod + def eglGetDisplay(display_id: Any) -> "EGLDisplay": + """获取 EGL 显示 (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglGetDisplay() 仅在 Android 设备上可用") + + @staticmethod + def eglInitialize(display: "EGLDisplay", major_minor: Optional[List[int]] = None) -> bool: + """初始化 EGL (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglInitialize() 仅在 Android 设备上可用") + + @staticmethod + def eglChooseConfig(display: "EGLDisplay", attrib_list: List[int], + configs: List[Any], config_size: int, + num_config: List[int]) -> bool: + """选择 EGL 配置 (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglChooseConfig() 仅在 Android 设备上可用") + + @staticmethod + def eglCreateContext(display: "EGLDisplay", config: "EGLConfig", + share_context: "EGLContext", + attrib_list: List[int]) -> "EGLContext": + """创建 EGL 上下文 (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglCreateContext() 仅在 Android 设备上可用") + + @staticmethod + def eglCreateWindowSurface(display: "EGLDisplay", config: "EGLConfig", + win: Any, attrib_list: List[int]) -> "EGLSurface": + """创建窗口表面 (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglCreateWindowSurface() 仅在 Android 设备上可用") + + @staticmethod + def eglMakeCurrent(display: "EGLDisplay", draw: "EGLSurface", + read: "EGLSurface", context: "EGLContext") -> bool: + """绑定 EGL 上下文 (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglMakeCurrent() 仅在 Android 设备上可用") + + @staticmethod + def eglSwapBuffers(display: "EGLDisplay", surface: "EGLSurface") -> bool: + """交换缓冲区 (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglSwapBuffers() 仅在 Android 设备上可用") + + @staticmethod + def eglDestroyContext(display: "EGLDisplay", context: "EGLContext") -> bool: + """销毁 EGL 上下文 (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglDestroyContext() 仅在 Android 设备上可用") + + @staticmethod + def eglDestroySurface(display: "EGLDisplay", surface: "EGLSurface") -> bool: + """销毁表面 (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglDestroySurface() 仅在 Android 设备上可用") + + @staticmethod + def eglTerminate(display: "EGLDisplay") -> bool: + """终止 EGL (桌面仅返回存根)""" + raise AndroidOnlyError("EGL14.eglTerminate() 仅在 Android 设备上可用") + + @staticmethod + def eglGetError() -> int: + """获取 EGL 错误码""" + return EGL14.EGL_SUCCESS + + @staticmethod + def eglQueryString(display: "EGLDisplay", name: int) -> str: + """查询 EGL 字符串""" + if name == 0x3029: # EGL_VERSION + return "1.4 (desktop stub)" + return "" + + def toString(self) -> str: + return "EGL14(desktop-stub)" + + +class EGLDisplay(Stub): + """EGLDisplay — EGL 显示连接""" + + def __init__(self): + super().__init__() + + def toString(self) -> str: + return f"EGLDisplay@{hex(id(self))}" + + def hashCode(self) -> int: + return hash(id(self)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +class EGLConfig(Stub): + """EGLConfig — EGL 帧缓冲配置""" + + def __init__(self): + super().__init__() + + def toString(self) -> str: + return f"EGLConfig@{hex(id(self))}" + + def hashCode(self) -> int: + return hash(id(self)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +class EGLContext(Stub): + """EGLContext — EGL 渲染上下文""" + + def __init__(self): + super().__init__() + + def toString(self) -> str: + return f"EGLContext@{hex(id(self))}" + + def hashCode(self) -> int: + return hash(id(self)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +class EGLSurface(Stub): + """EGLSurface — EGL 渲染表面""" + + def __init__(self): + super().__init__() + + def toString(self) -> str: + return f"EGLSurface@{hex(id(self))}" + + def hashCode(self) -> int: + return hash(id(self)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# GLSurfaceView +# ═══════════════════════════════════════════════════════════════ + +class GLSurfaceView(Stub): + """GLSurfaceView — OpenGL 渲染视图 (桌面存根)""" + + # 渲染模式 + RENDERMODE_CONTINUOUSLY = 1 + RENDERMODE_WHEN_DIRTY = 0 + + # 调试标志 + DEBUG_CHECK_GL_ERROR = 1 + DEBUG_LOG_GL_CALLS = 2 + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._renderer: Optional[Any] = None + self._render_mode: int = GLSurfaceView.RENDERMODE_CONTINUOUSLY + self._egl_context_client_version: int = 2 + self._preserve_egl_context_on_pause: bool = False + self._debug_flags: int = 0 + self._paused: bool = False + self._queue: List[Callable] = [] + + def setRenderer(self, renderer: Any): + """设置渲染器""" + self._renderer = renderer + if renderer: + renderer.onSurfaceCreated(None, None) + renderer.onSurfaceChanged(None, 0, 0) + + def setRenderMode(self, render_mode: int): + """设置渲染模式""" + self._render_mode = render_mode + + def getRenderMode(self) -> int: + """获取渲染模式""" + return self._render_mode + + def onPause(self): + """暂停渲染""" + self._paused = True + + def onResume(self): + """恢复渲染""" + self._paused = False + + def queueEvent(self, r: Callable): + """将事件排入 GL 线程队列""" + self._queue.append(r) + # 立即执行 (桌面模拟) + try: + r() + except Exception: + pass + + def requestRender(self): + """请求渲染""" + if self._renderer and not self._paused: + self._renderer.onDrawFrame(None) + + def setEGLContextClientVersion(self, version: int): + """设置 EGL 客户端版本""" + self._egl_context_client_version = version + + def setPreserveEGLContextOnPause(self, preserve: bool): + """设置暂停时是否保留 EGL 上下文""" + self._preserve_egl_context_on_pause = preserve + + def setDebugFlags(self, debug_flags: int): + """设置调试标志""" + self._debug_flags = debug_flags + + def getPreserveEGLContextOnPause(self) -> bool: + return self._preserve_egl_context_on_pause + + def surfaceCreated(self, holder: Any): + """表面创建回调""" + pass + + def surfaceDestroyed(self, holder: Any): + """表面销毁回调""" + pass + + def surfaceChanged(self, holder: Any, format: int, w: int, h: int): + """表面变化回调""" + pass + + def toString(self) -> str: + return f"GLSurfaceView(renderMode={self._render_mode}, paused={self._paused})" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# GLU +# ═══════════════════════════════════════════════════════════════ + +class GLU(Stub): + """GLU — OpenGL 实用工具函数""" + + def __init__(self): + super().__init__() + + @staticmethod + def gluLookAt(eye_x: float, eye_y: float, eye_z: float, + center_x: float, center_y: float, center_z: float, + up_x: float, up_y: float, up_z: float): + """定义视图变换 (桌面仅抛异常)""" + raise AndroidOnlyError("GLU.gluLookAt() 仅在 Android (OpenGL) 设备上可用") + + @staticmethod + def gluPerspective(fov_y: float, aspect: float, z_near: float, z_far: float): + """设置透视投影 (桌面仅抛异常)""" + raise AndroidOnlyError("GLU.gluPerspective() 仅在 Android (OpenGL) 设备上可用") + + @staticmethod + def gluOrtho2D(left: float, right: float, bottom: float, top: float): + """设置正交投影 (桌面仅抛异常)""" + raise AndroidOnlyError("GLU.gluOrtho2D() 仅在 Android (OpenGL) 设备上可用") + + @staticmethod + def gluProject(obj_x: float, obj_y: float, obj_z: float, + model: List[float], proj: List[float], + viewport: List[int]) -> Tuple[bool, float, float, float]: + """将对象坐标映射到窗口坐标""" + win_x = (obj_x * proj[0] + 1.0) * viewport[2] / 2.0 + viewport[0] + win_y = (obj_y * proj[5] + 1.0) * viewport[3] / 2.0 + viewport[1] + win_z = obj_z * proj[10] + proj[11] + return True, win_x, win_y, win_z + + @staticmethod + def gluUnProject(win_x: float, win_y: float, win_z: float, + model: List[float], proj: List[float], + viewport: List[int]) -> Tuple[bool, float, float, float]: + """将窗口坐标映射到对象坐标""" + obj_x = 2.0 * (win_x - viewport[0]) / viewport[2] - 1.0 + obj_y = 2.0 * (win_y - viewport[1]) / viewport[3] - 1.0 + obj_z = win_z * 2.0 - 1.0 + return True, obj_x, obj_y, obj_z + + @staticmethod + def gluErrorString(error: int) -> str: + """返回 OpenGL 错误描述字符串""" + errors = { + 0x0500: "GL_INVALID_ENUM", + 0x0501: "GL_INVALID_VALUE", + 0x0502: "GL_INVALID_OPERATION", + 0x0503: "GL_STACK_OVERFLOW", + 0x0504: "GL_STACK_UNDERFLOW", + 0x0505: "GL_OUT_OF_MEMORY", + 0x0506: "GL_INVALID_FRAMEBUFFER_OPERATION", + } + return errors.get(error, f"Unknown error 0x{error:04X}") + + def toString(self) -> str: + return "GLU(desktop-stub)" + + def hashCode(self) -> int: + return hash("android.opengl.GLU") & 0x7FFFFFFF + + def equals(self, other) -> bool: + return isinstance(other, GLU) + + +# ═══════════════════════════════════════════════════════════════ +# GLUtils +# ═══════════════════════════════════════════════════════════════ + +class GLUtils(Stub): + """GLUtils — OpenGL 工具函数""" + + def __init__(self): + super().__init__() + + @staticmethod + def getInternalFormat(bitmap: Any) -> int: + """获取位图的内部格式""" + return 0x1908 # GL_RGBA + + @staticmethod + def getType(bitmap: Any) -> int: + """获取位图数据类型""" + return 0x1401 # GL_UNSIGNED_BYTE + + @staticmethod + def texImage2D(target: int, level: int, internal_format: int, + bitmap: Any, border: int = 0): + """上传 2D 纹理图像""" + raise AndroidOnlyError("GLUtils.texImage2D() 仅在 Android (OpenGL) 设备上可用") + + @staticmethod + def texSubImage2D(target: int, level: int, xoffset: int, yoffset: int, + bitmap: Any, format: int = -1, type_: int = -1): + """更新 2D 纹理子图像""" + raise AndroidOnlyError("GLUtils.texSubImage2D() 仅在 Android (OpenGL) 设备上可用") + + @staticmethod + def toString(bitmap_or_buffer: Any) -> str: + return str(bitmap_or_buffer) + + def toString(self) -> str: + return "GLUtils(desktop-stub)" + + def hashCode(self) -> int: + return hash("android.opengl.GLUtils") & 0x7FFFFFFF + + def equals(self, other) -> bool: + return isinstance(other, GLUtils) + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "GLSurfaceView", + "GLU", + "GLUtils", + "EGL14", + "EGLConfig", + "EGLContext", + "EGLDisplay", + "EGLSurface", +] \ No newline at end of file diff --git a/android_pyapi/android/print.py b/android_pyapi/android/print.py new file mode 100644 index 0000000..7dd47ae --- /dev/null +++ b/android_pyapi/android/print.py @@ -0,0 +1,581 @@ +""" +Android Print 组件 — PrintManager, PrintJob, PrintDocumentAdapter, PdfPrint 等 +方法签名完全对齐 Android SDK (android.print 包) +""" + +import os +import time +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# PrintAttributes +# ═══════════════════════════════════════════════════════════════ + +class PrintAttributes(Stub): + """PrintAttributes — 打印属性""" + + # 颜色模式 + COLOR_MODE_MONOCHROME = 1 + COLOR_MODE_COLOR = 2 + + # 方向 + ORIENTATION_PORTRAIT = 1 + ORIENTATION_LANDSCAPE = 2 + + # 媒质尺寸 ID + MEDIA_SIZE_UNKNOWN = "unknown" + MEDIA_SIZE_A4 = "A4" + MEDIA_SIZE_A3 = "A3" + MEDIA_SIZE_LETTER = "letter" + MEDIA_SIZE_LEGAL = "legal" + MEDIA_SIZE_ISO_A4 = "iso_a4" + MEDIA_SIZE_ISO_A3 = "iso_a3" + MEDIA_SIZE_NA_LETTER = "na_letter" + + def __init__(self): + super().__init__() + self._media_size: Optional["PrintAttributes.MediaSize"] = None + self._resolution: Optional["PrintAttributes.Resolution"] = None + self._min_margins: Optional["PrintAttributes.Margins"] = None + self._color_mode: int = PrintAttributes.COLOR_MODE_COLOR + self._orientation: int = PrintAttributes.ORIENTATION_PORTRAIT + + def getMediaSize(self) -> Optional["MediaSize"]: + return self._media_size + + def getResolution(self) -> Optional["Resolution"]: + return self._resolution + + def getMinMargins(self) -> Optional["Margins"]: + return self._min_margins + + def getColorMode(self) -> int: + return self._color_mode + + def getOrientation(self) -> int: + return self._orientation + + def isPortrait(self) -> bool: + return self._orientation == PrintAttributes.ORIENTATION_PORTRAIT + + def toString(self) -> str: + return (f"PrintAttributes(colorMode={self._color_mode}, " + f"orientation={self._orientation})") + + def hashCode(self) -> int: + return hash((self._color_mode, self._orientation)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, PrintAttributes): + return False + return (self._color_mode == other._color_mode + and self._orientation == other._orientation) + + # ── 内部嵌套类 ── + + class MediaSize(Stub): + """MediaSize — 媒质尺寸""" + + def __init__(self, id: str, label: str, width_mils: int, height_mils: int): + super().__init__() + self._id = id + self._label = label + self._width_mils = width_mils + self._height_mils = height_mils + + def getId(self) -> str: + return self._id + + def getLabel(self, package_manager: Any = None) -> str: + return self._label + + def getWidthMils(self) -> int: + return self._width_mils + + def getHeightMils(self) -> int: + return self._height_mils + + def asPortrait(self) -> "PrintAttributes.MediaSize": + if self._width_mils > self._height_mils: + return PrintAttributes.MediaSize( + self._id, self._label, self._height_mils, self._width_mils) + return self + + def asLandscape(self) -> "PrintAttributes.MediaSize": + if self._width_mils < self._height_mils: + return PrintAttributes.MediaSize( + self._id, self._label, self._height_mils, self._width_mils) + return self + + def toString(self) -> str: + return f"MediaSize({self._id}: {self._width_mils}x{self._height_mils}mils)" + + def hashCode(self) -> int: + return hash(self._id) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, PrintAttributes.MediaSize): + return False + return self._id == other._id + + class Resolution(Stub): + """Resolution — 打印分辨率""" + + def __init__(self, id: str, label: str, horizontal_dpi: int, vertical_dpi: int): + super().__init__() + self._id = id + self._label = label + self._horizontal_dpi = horizontal_dpi + self._vertical_dpi = vertical_dpi + + def getId(self) -> str: + return self._id + + def getLabel(self) -> str: + return self._label + + def getHorizontalDpi(self) -> int: + return self._horizontal_dpi + + def getVerticalDpi(self) -> int: + return self._vertical_dpi + + def toString(self) -> str: + return f"Resolution({self._id}: {self._horizontal_dpi}x{self._vertical_dpi}dpi)" + + class Margins(Stub): + """Margins — 页边距""" + + NO_MARGINS: "PrintAttributes.Margins" = None + + def __init__(self, left_mils: int, top_mils: int, + right_mils: int, bottom_mils: int): + super().__init__() + self._left_mils = left_mils + self._top_mils = top_mils + self._right_mils = right_mils + self._bottom_mils = bottom_mils + + def getLeftMils(self) -> int: + return self._left_mils + + def getTopMils(self) -> int: + return self._top_mils + + def getRightMils(self) -> int: + return self._right_mils + + def getBottomMils(self) -> int: + return self._bottom_mils + + def toString(self) -> str: + return f"Margins({self._left_mils},{self._top_mils},{self._right_mils},{self._bottom_mils})" + + class Builder(Stub): + """Builder — PrintAttributes 构建器""" + + def __init__(self): + super().__init__() + self._attrs = PrintAttributes() + + def setMediaSize(self, media_size: "PrintAttributes.MediaSize") -> "PrintAttributes.Builder": + self._attrs._media_size = media_size + return self + + def setResolution(self, resolution: "PrintAttributes.Resolution") -> "PrintAttributes.Builder": + self._attrs._resolution = resolution + return self + + def setMinMargins(self, margins: "PrintAttributes.Margins") -> "PrintAttributes.Builder": + self._attrs._min_margins = margins + return self + + def setColorMode(self, color_mode: int) -> "PrintAttributes.Builder": + self._attrs._color_mode = color_mode + return self + + def setOrientation(self, orientation: int) -> "PrintAttributes.Builder": + self._attrs._orientation = orientation + return self + + def build(self) -> "PrintAttributes": + return self._attrs + + +PrintAttributes.Margins.NO_MARGINS = PrintAttributes.Margins(0, 0, 0, 0) + + +# ═══════════════════════════════════════════════════════════════ +# PrintJobInfo +# ═══════════════════════════════════════════════════════════════ + +class PrintJobInfo(Stub): + """PrintJobInfo — 打印任务信息""" + + # 状态常量 + STATE_CREATED = 1 + STATE_QUEUED = 2 + STATE_STARTED = 3 + STATE_BLOCKED = 4 + STATE_COMPLETED = 5 + STATE_FAILED = 6 + STATE_CANCELED = 7 + + def __init__(self): + super().__init__() + self._label: str = "" + self._id: Optional[Any] = None + self._state: int = PrintJobInfo.STATE_CREATED + self._attributes: Optional[PrintAttributes] = None + self._copies: int = 1 + self._creation_time: int = int(time.time() * 1000) + self._canceling: bool = False + self._pages: Optional[List[int]] = None + self._has_advanced_options: bool = False + + def getLabel(self) -> str: + return self._label + + def setLabel(self, label: str): + self._label = label + + def getId(self) -> Optional[Any]: + return self._id + + def getState(self) -> int: + return self._state + + def setState(self, state: int): + self._state = state + + def getAttributes(self) -> Optional[PrintAttributes]: + return self._attributes + + def setAttributes(self, attributes: PrintAttributes): + self._attributes = attributes + + def getCopies(self) -> int: + return self._copies + + def setCopies(self, copies: int): + self._copies = copies + + def getCreationTime(self) -> int: + return self._creation_time + + def isCanceling(self) -> bool: + return self._canceling + + def getPages(self) -> Optional[List[int]]: + return self._pages + + def setPages(self, pages: Optional[List[int]]): + self._pages = pages + + def hasAdvancedOptions(self) -> bool: + return self._has_advanced_options + + def toString(self) -> str: + return f"PrintJobInfo(label={self._label}, state={self._state}, copies={self._copies})" + + def hashCode(self) -> int: + return hash(self._id) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, PrintJobInfo): + return False + return self._id == other._id + + # ── Builder ── + + class Builder(Stub): + """Builder — PrintJobInfo 构建器""" + + def __init__(self, info: Optional["PrintJobInfo"] = None): + super().__init__() + self._info = PrintJobInfo() + if info: + self._info._label = info._label + self._info._id = info._id + self._info._attributes = info._attributes + self._info._copies = info._copies + + def setCopies(self, copies: int) -> "PrintJobInfo.Builder": + self._info._copies = copies + return self + + def setAttributes(self, attributes: PrintAttributes) -> "PrintJobInfo.Builder": + self._info._attributes = attributes + return self + + def setLabel(self, label: str) -> "PrintJobInfo.Builder": + self._info._label = label + return self + + def setPages(self, pages: Optional[List[int]]) -> "PrintJobInfo.Builder": + self._info._pages = pages + return self + + def build(self) -> "PrintJobInfo": + return self._info + + +# ═══════════════════════════════════════════════════════════════ +# PrintJob +# ═══════════════════════════════════════════════════════════════ + +class PrintJob(Stub): + """PrintJob — 打印任务""" + + def __init__(self, info: PrintJobInfo): + super().__init__() + self._info = info + self._cancelled = False + self._result: Optional[Any] = None + + def getId(self) -> Optional[Any]: + return self._info.getId() + + def getInfo(self) -> PrintJobInfo: + return self._info + + def isStarted(self) -> bool: + return self._info.getState() >= PrintJobInfo.STATE_STARTED + + def isBlocked(self) -> bool: + return self._info.getState() == PrintJobInfo.STATE_BLOCKED + + def isCompleted(self) -> bool: + return self._info.getState() == PrintJobInfo.STATE_COMPLETED + + def isFailed(self) -> bool: + return self._info.getState() == PrintJobInfo.STATE_FAILED + + def isCancelled(self) -> bool: + return self._cancelled + + def cancel(self): + """取消打印任务""" + self._cancelled = True + self._info.setState(PrintJobInfo.STATE_CANCELED) + + def restart(self): + """重新启动打印任务""" + self._cancelled = False + self._info.setState(PrintJobInfo.STATE_QUEUED) + + def setResult(self, result: Any): + self._result = result + + def getResult(self) -> Optional[Any]: + return self._result + + def toString(self) -> str: + return f"PrintJob(id={self.getId()}, state={self._info.getState()})" + + def hashCode(self) -> int: + return hash(self.getId()) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, PrintJob): + return False + return self.getId() == other.getId() + + +# ═══════════════════════════════════════════════════════════════ +# PrintDocumentAdapter +# ═══════════════════════════════════════════════════════════════ + +class PrintDocumentAdapter(Stub): + """PrintDocumentAdapter — 打印文档适配器""" + + EXTRA_PRINT_PREVIEW = "EXTRA_PRINT_PREVIEW" + + def __init__(self): + super().__init__() + + def onStart(self): + """打印开始回调""" + pass + + def onFinish(self): + """打印结束回调""" + pass + + def onLayout(self, old_attributes: PrintAttributes, new_attributes: PrintAttributes, + cancellation_signal: Any, callback: "PrintDocumentAdapterLayoutCallback", + metadata: Any): + """布局回调 - 桌面实现直接完成""" + if callback: + callback.onLayoutFinished(self, new_attributes) + + def onWrite(self, pages: List[int], destination: Any, + cancellation_signal: Any, + callback: "PrintDocumentAdapterWriteCallback"): + """写入回调 - 桌面实现直接完成""" + if callback: + callback.onWriteFinished(pages) + + +class PrintDocumentAdapterLayoutCallback(Stub): + """PrintDocumentAdapterLayoutCallback — 布局回调""" + + def __init__(self): + super().__init__() + + def onLayoutFinished(self, adapter: PrintDocumentAdapter, attributes: PrintAttributes): + """布局完成""" + pass + + def onLayoutFailed(self, error: Optional[str] = None): + """布局失败""" + pass + + def onLayoutCancelled(self): + """布局取消""" + pass + + +class PrintDocumentAdapterWriteCallback(Stub): + """PrintDocumentAdapterWriteCallback — 写入回调""" + + def __init__(self): + super().__init__() + + def onWriteFinished(self, pages: List[int]): + """写入完成""" + pass + + def onWriteFailed(self, error: Optional[str] = None): + """写入失败""" + pass + + def onWriteCancelled(self): + """写入取消""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# PdfPrint +# ═══════════════════════════════════════════════════════════════ + +class PdfPrint(Stub): + """PdfPrint — PDF 打印辅助类""" + + def __init__(self, attributes: Optional[PrintAttributes] = None): + super().__init__() + self._attributes = attributes or PrintAttributes() + + def getAttributes(self) -> PrintAttributes: + return self._attributes + + def setAttributes(self, attributes: PrintAttributes): + self._attributes = attributes + + def print(self, print_adapter: PrintDocumentAdapter, output_path: str) -> bool: + """执行 PDF 打印 (桌面实现: 写入文件)""" + try: + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + # 在桌面环境, PDF 打印由适配器的 onWrite 实现处理 + callback = PrintDocumentAdapterWriteCallback() + print_adapter.onWrite([1], output_path, None, callback) + return True + except Exception: + return False + + def toString(self) -> str: + return f"PdfPrint(attributes={self._attributes})" + + def hashCode(self) -> int: + return hash(str(self._attributes)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, PdfPrint): + return False + return str(self._attributes) == str(other._attributes) + + +# ═══════════════════════════════════════════════════════════════ +# PrintManager +# ═══════════════════════════════════════════════════════════════ + +class PrintManager(Stub): + """PrintManager — 打印管理器""" + + # 打印任务 ID 生成 + _next_job_id = 1 + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._print_jobs: Dict[str, PrintJob] = {} + + def getPrintJobs(self) -> List[PrintJob]: + """获取打印任务列表""" + return list(self._print_jobs.values()) + + def print(self, job_name: str, document_adapter: PrintDocumentAdapter, + attributes: Optional[PrintAttributes] = None) -> PrintJob: + """提交打印任务""" + info = PrintJobInfo() + info.setLabel(job_name) + info.setAttributes(attributes or PrintAttributes()) + info.setState(PrintJobInfo.STATE_QUEUED) + + job_id = f"print_job_{PrintManager._next_job_id}" + PrintManager._next_job_id += 1 + + job = PrintJob(info) + self._print_jobs[job_id] = job + + # 自动触发布局和写入 + cancellation_signal = type("CancellationSignal", (), {"isCanceled": lambda: False})() + document_adapter.onStart() + layout_cb = PrintDocumentAdapterLayoutCallback() + document_adapter.onLayout(None, info.getAttributes(), + cancellation_signal, layout_cb, None) + + info.setState(PrintJobInfo.STATE_COMPLETED) + document_adapter.onFinish() + + return job + + def getPrintJobById(self, job_id: str) -> Optional[PrintJob]: + """根据 ID 获取打印任务""" + return self._print_jobs.get(job_id) + + def cancelPrintJob(self, job_id: str) -> bool: + """取消打印任务""" + job = self._print_jobs.get(job_id) + if job: + job.cancel() + return True + return False + + def toString(self) -> str: + return f"PrintManager(jobs={len(self._print_jobs)})" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "PrintManager", + "PrintJob", + "PrintJobInfo", + "PrintDocumentAdapter", + "PrintAttributes", + "PrintDocumentAdapterLayoutCallback", + "PrintDocumentAdapterWriteCallback", + "PdfPrint", +] \ No newline at end of file diff --git a/android_pyapi/android/renderscript.py b/android_pyapi/android/renderscript.py new file mode 100644 index 0000000..c86457b --- /dev/null +++ b/android_pyapi/android/renderscript.py @@ -0,0 +1,903 @@ +from __future__ import annotations + +""" +Android RenderScript 组件 — RenderScript, Allocation, Element, Type, Script 等 +方法签名完全对齐 Android SDK (android.renderscript 包) +""" + +import math +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# RenderScript +# ═══════════════════════════════════════════════════════════════ + +class RenderScript(Stub): + """RenderScript — RenderScript 运行时上下文 (桌面仅存根)""" + + # 优先级 + Priority_LOW = 0 + Priority_NORMAL = 1 + Priority_HIGH = 2 + + # 上下文类型 + ContextType_NORMAL = 0 + ContextType_DEBUG = 1 + ContextType_PROFILE = 2 + + def __init__(self): + super().__init__() + self._context_type = RenderScript.ContextType_NORMAL + self._error_handler: Optional[Callable] = None + self._message_handler: Optional[Callable] = None + self._priority = RenderScript.Priority_NORMAL + self._initialized = False + + @staticmethod + def create(context: Any = None) -> "RenderScript": + """创建 RenderScript 上下文""" + rs = RenderScript() + rs._initialized = True + return rs + + @staticmethod + def createMultiContext(context: Any = None, + context_type: int = ContextType_NORMAL, + flags: int = 0, + api_version: int = 0) -> "RenderScript": + """创建多上下文 RenderScript""" + rs = RenderScript() + rs._context_type = context_type + rs._initialized = True + return rs + + def setPriority(self, priority: int): + """设置优先级""" + self._priority = priority + + def getPriority(self) -> int: + return self._priority + + def setErrorHandler(self, handler: Callable): + """设置错误处理器""" + self._error_handler = handler + + def setMessageHandler(self, handler: Callable): + """设置消息处理器""" + self._message_handler = handler + + def finish(self): + """等待所有挂起渲染完成""" + pass + + def destroy(self): + """销毁 RenderScript 上下文""" + self._initialized = False + + def getApplicationContext(self) -> Any: + return None + + def getErrorHandler(self) -> Optional[Callable]: + return self._error_handler + + def getMessageHandler(self) -> Optional[Callable]: + return self._message_handler + + def isDestroyed(self) -> bool: + return not self._initialized + + def nvLoadAll(self): + pass + + def nvLoadFont(self, name: str) -> int: + return 0 + + def nvLoadProp(self, name: str) -> int: + return 0 + + def toString(self) -> str: + return f"RenderScript(priority={self._priority}, initialized={self._initialized})" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# Element +# ═══════════════════════════════════════════════════════════════ + +class Element(Stub): + """Element — RenderScript 数据类型元素""" + + # 预定义数据类型 + F32: "Element" = None + F64: "Element" = None + I8: "Element" = None + I16: "Element" = None + I32: "Element" = None + I64: "Element" = None + U8: "Element" = None + U16: "Element" = None + U32: "Element" = None + U64: "Element" = None + BOOLEAN: "Element" = None + ALLOCATION: "Element" = None + A_8: "Element" = None + RGB_565: "Element" = None + RGBA_4444: "Element" = None + RGBA_8888: "Element" = None + F32_2: "Element" = None + F32_3: "Element" = None + F32_4: "Element" = None + F64_2: "Element" = None + F64_3: "Element" = None + F64_4: "Element" = None + I8_2: "Element" = None + I8_3: "Element" = None + I8_4: "Element" = None + I16_2: "Element" = None + I16_3: "Element" = None + I16_4: "Element" = None + I32_2: "Element" = None + I32_3: "Element" = None + I32_4: "Element" = None + I64_2: "Element" = None + I64_3: "Element" = None + I64_4: "Element" = None + U8_2: "Element" = None + U8_3: "Element" = None + U8_4: "Element" = None + U16_2: "Element" = None + U16_3: "Element" = None + U16_4: "Element" = None + U32_2: "Element" = None + U32_3: "Element" = None + U32_4: "Element" = None + U64_2: "Element" = None + U64_3: "Element" = None + U64_4: "Element" = None + + # 数据类型 + DataType = type("DataType", (), { + "NONE": 0, "FLOAT_32": 1, "FLOAT_64": 2, + "SIGNED_8": 3, "SIGNED_16": 4, "SIGNED_32": 5, "SIGNED_64": 6, + "UNSIGNED_8": 7, "UNSIGNED_16": 8, "UNSIGNED_32": 9, "UNSIGNED_64": 10, + "BOOLEAN": 11, "UNSIGNED_5_6_5": 12, "UNSIGNED_5_5_5_1": 13, + "UNSIGNED_4_4_4_4": 14, "MATRIX_4X4": 15, "MATRIX_3X3": 16, + "UNSIGNED_2_10_10_10": 17, "RS_ELEMENT": 18, + }) + + # 数据类型大小 + DataType = DataType() + + def __init__(self, rs: Optional[RenderScript] = None, data_type: int = 0, + vector_size: int = 1): + super().__init__() + self._rs = rs + self._data_type = data_type + self._vector_size = vector_size + self._element_size = self._compute_size() + + def _compute_size(self) -> int: + base_sizes = { + Element.DataType.FLOAT_32: 4, Element.DataType.FLOAT_64: 8, + Element.DataType.SIGNED_8: 1, Element.DataType.SIGNED_16: 2, + Element.DataType.SIGNED_32: 4, Element.DataType.SIGNED_64: 8, + Element.DataType.UNSIGNED_8: 1, Element.DataType.UNSIGNED_16: 2, + Element.DataType.UNSIGNED_32: 4, Element.DataType.UNSIGNED_64: 8, + Element.DataType.BOOLEAN: 1, + Element.DataType.UNSIGNED_5_6_5: 2, + Element.DataType.UNSIGNED_5_5_5_1: 2, + Element.DataType.UNSIGNED_4_4_4_4: 2, + Element.DataType.MATRIX_4X4: 64, + Element.DataType.MATRIX_3X3: 36, + Element.DataType.RS_ELEMENT: 4, + } + return base_sizes.get(self._data_type, 4) * self._vector_size + + def getBytesSize(self) -> int: + return self._element_size + + def getVectorSize(self) -> int: + return self._vector_size + + def getDataType(self) -> int: + return self._data_type + + def isCompatible(self, e: "Element") -> bool: + return self._data_type == e._data_type and self._vector_size == e._vector_size + + def getSubElement(self, index: int) -> Optional["Element"]: + return None + + def getSubElementCount(self) -> int: + return 0 + + def getSubElementArraySize(self, index: int) -> int: + return 0 + + def getSubElementOffsetBytes(self, index: int) -> int: + return 0 + + def toString(self) -> str: + return f"Element(dataType={self._data_type}, vectorSize={self._vector_size})" + + def hashCode(self) -> int: + return hash((self._data_type, self._vector_size)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, Element): + return False + return self._data_type == other._data_type and self._vector_size == other._vector_size + + @staticmethod + def BOOL(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.BOOLEAN, 1) + + @staticmethod + def U8(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.UNSIGNED_8, 1) + + @staticmethod + def I8(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.SIGNED_8, 1) + + @staticmethod + def U16(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.UNSIGNED_16, 1) + + @staticmethod + def I16(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.SIGNED_16, 1) + + @staticmethod + def U32(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.UNSIGNED_32, 1) + + @staticmethod + def I32(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.SIGNED_32, 1) + + @staticmethod + def U64(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.UNSIGNED_64, 1) + + @staticmethod + def I64(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.SIGNED_64, 1) + + @staticmethod + def F32(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.FLOAT_32, 1) + + @staticmethod + def F64(rs: RenderScript) -> "Element": + return Element(rs, Element.DataType.FLOAT_64, 1) + + @staticmethod + def createVector(rs: RenderScript, data_type: int, size: int) -> "Element": + return Element(rs, data_type, size) + + @staticmethod + def createPixel(rs: RenderScript, data_type: int, pixel_type: int) -> "Element": + return Element(rs, data_type, 4) + + +# ═══════════════════════════════════════════════════════════════ +# Type +# ═══════════════════════════════════════════════════════════════ + +class Type(Stub): + """Type — RenderScript 内存分配类型""" + + CubemapFace = type("CubemapFace", (), { + "POSITIVE_X": 0, "NEGATIVE_X": 1, + "POSITIVE_Y": 2, "NEGATIVE_Y": 3, + "POSITIVE_Z": 4, "NEGATIVE_Z": 5, + }) + + def __init__(self, rs: Optional[RenderScript] = None, + element: Optional[Element] = None, + dim_x: int = 0, dim_y: int = 0, dim_z: int = 0, + mipmaps: bool = False, faces: bool = False): + super().__init__() + self._rs = rs + self._element = element + self._dim_x = dim_x + self._dim_y = dim_y + self._dim_z = dim_z + self._mipmaps = mipmaps + self._faces = faces + self._yuv_format = 0 + + def getElement(self) -> Optional[Element]: + return self._element + + def getX(self) -> int: + return self._dim_x + + def getY(self) -> int: + return self._dim_y + + def getZ(self) -> int: + return self._dim_z + + def getCount(self) -> int: + return self._dim_x * max(self._dim_y, 1) * max(self._dim_z, 1) + + def hasMipmaps(self) -> bool: + return self._mipmaps + + def hasFaces(self) -> bool: + return self._faces + + def getYuvFormat(self) -> int: + return self._yuv_format + + def setYuvFormat(self, yuv_format: int): + self._yuv_format = yuv_format + + def toString(self) -> str: + return (f"Type(element={self._element}, dims={self._dim_x}x{self._dim_y}" + f"x{self._dim_z}, mipmaps={self._mipmaps})") + + def hashCode(self) -> int: + return hash((self._dim_x, self._dim_y, self._dim_z)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, Type): + return False + return (self._dim_x == other._dim_x and self._dim_y == other._dim_y + and self._dim_z == other._dim_z) + + # ── Builder ── + + class Builder(Stub): + """Builder — Type 构建器""" + + def __init__(self, rs: RenderScript, element: Element): + super().__init__() + self._rs = rs + self._element = element + self._dim_x = 0 + self._dim_y = 0 + self._dim_z = 0 + self._mipmaps = False + self._faces = False + self._yuv_format = 0 + + def setX(self, value: int) -> "Type.Builder": + self._dim_x = value + return self + + def setY(self, value: int) -> "Type.Builder": + self._dim_y = value + return self + + def setZ(self, value: int) -> "Type.Builder": + self._dim_z = value + return self + + def setMipmaps(self, enable: bool) -> "Type.Builder": + self._mipmaps = enable + return self + + def setFaces(self, enable: bool) -> "Type.Builder": + self._faces = enable + return self + + def setYuvFormat(self, yuv_format: int) -> "Type.Builder": + self._yuv_format = yuv_format + return self + + def create(self) -> Type: + t = Type(self._rs, self._element, self._dim_x, self._dim_y, + self._dim_z, self._mipmaps, self._faces) + t._yuv_format = self._yuv_format + return t + + +# ═══════════════════════════════════════════════════════════════ +# Allocation +# ═══════════════════════════════════════════════════════════════ + +class Allocation(Stub): + """Allocation — RenderScript 内存分配""" + + # 使用类型 + USAGE_SCRIPT = 0x0001 + USAGE_GRAPHICS_TEXTURE = 0x0002 + USAGE_GRAPHICS_VERTEX = 0x0004 + USAGE_GRAPHICS_CONSTANTS = 0x0008 + USAGE_GRAPHICS_RENDER_TARGET = 0x0010 + USAGE_IO_INPUT = 0x0020 + USAGE_IO_OUTPUT = 0x0040 + USAGE_SHARED = 0x0080 + + # Mipmap 控制 + MIPMAP_FULL = 0 + MIPMAP_ON_SYNC_TO_TEXTURE = 1 + + def __init__(self, rs: Optional[RenderScript] = None, + type_: Optional[Type] = None, + usage: int = USAGE_SCRIPT): + super().__init__() + self._rs = rs + self._type = type_ + self._usage = usage + self._data: Any = None + self._element_size = type_.getElement().getBytesSize() if type_ and type_.getElement() else 0 + + @staticmethod + def createTyped(rs: RenderScript, type_: Type, + usage: int = USAGE_SCRIPT) -> "Allocation": + """创建分配""" + alloc = Allocation(rs, type_, usage) + total_size = type_.getCount() * type_.getElement().getBytesSize() if type_.getElement() else 0 + alloc._data = bytearray(total_size) + return alloc + + @staticmethod + def createFromBitmap(rs: RenderScript, bitmap: Any, + usage: int = USAGE_SCRIPT) -> "Allocation": + """从位图创建分配""" + # 尝试获取位图尺寸 + width, height = 0, 0 + try: + width = bitmap.getWidth() if hasattr(bitmap, 'getWidth') else 0 + height = bitmap.getHeight() if hasattr(bitmap, 'getHeight') else 0 + except Exception: + pass + element = Element(rs, Element.DataType.UNSIGNED_8, 4) + type_ = Type(rs, element, width, height) + alloc = Allocation(rs, type_, usage) + alloc._data = bytearray(width * height * 4) + return alloc + + @staticmethod + def createFromBitmapBoxed(rs: RenderScript, bitmap: Any, + usage: int = USAGE_SCRIPT) -> "Allocation": + return Allocation.createFromBitmap(rs, bitmap, usage) + + def copyFrom(self, data: Any): + """从数组或位图复制数据""" + if isinstance(data, (bytes, bytearray)): + self._data = bytearray(data) + elif hasattr(data, '__iter__'): + import struct + try: + self._data = bytearray(struct.pack(f'<{len(list(data))}f', + *[float(v) for v in data])) + except Exception: + self._data = bytearray(len(list(data)) * 4) + else: + self._data = bytearray(1024) + + def copyTo(self, data: Any): + """将数据复制到目标""" + if isinstance(data, bytearray) and self._data: + data[:] = self._data + elif isinstance(data, list): + data.clear() + if self._data: + for i in range(0, len(self._data), 4): + data.append(int.from_bytes(self._data[i:i+4], 'little')) + + def resize(self, dim_x: int): + """重新调整分配大小""" + if self._type: + self._type._dim_x = dim_x + self._data = bytearray(self._type.getCount() * self._element_size) + + def getType(self) -> Optional[Type]: + return self._type + + def getUsage(self) -> int: + return self._usage + + def getElement(self) -> Optional[Element]: + return self._type.getElement() if self._type else None + + def syncAll(self, src_location: int): + pass + + def destroy(self): + self._data = None + + def toString(self) -> str: + return (f"Allocation(type={self._type}, usage={self._usage}, " + f"data_size={len(self._data) if self._data else 0})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# Matrix4f +# ═══════════════════════════════════════════════════════════════ + +class Matrix4f(Stub): + """Matrix4f — 4x4 浮点矩阵""" + + def __init__(self): + super().__init__() + self._matrix = [0.0] * 16 + self._matrix[0] = 1.0 # 单位矩阵 + self._matrix[5] = 1.0 + self._matrix[10] = 1.0 + self._matrix[15] = 1.0 + + def getArray(self) -> List[float]: + return list(self._matrix) + + def setArray(self, array: List[float]): + if len(array) >= 16: + self._matrix = list(array[:16]) + + def loadIdentity(self): + self._matrix = [0.0] * 16 + self._matrix[0] = 1.0 + self._matrix[5] = 1.0 + self._matrix[10] = 1.0 + self._matrix[15] = 1.0 + + def load(self, src: "Matrix4f"): + self._matrix = list(src._matrix) + + def loadRotate(self, rot: float, x: float, y: float, z: float): + angle_rad = math.radians(rot) + c = math.cos(angle_rad) + s = math.sin(angle_rad) + self.loadIdentity() + if x == 1 and y == 0 and z == 0: + self._matrix[5] = c + self._matrix[6] = -s + self._matrix[9] = s + self._matrix[10] = c + elif x == 0 and y == 1 and z == 0: + self._matrix[0] = c + self._matrix[2] = s + self._matrix[8] = -s + self._matrix[10] = c + elif x == 0 and y == 0 and z == 1: + self._matrix[0] = c + self._matrix[1] = -s + self._matrix[4] = s + self._matrix[5] = c + + def loadScale(self, x: float, y: float, z: float): + self.loadIdentity() + self._matrix[0] = x + self._matrix[5] = y + self._matrix[10] = z + + def loadTranslate(self, x: float, y: float, z: float): + self.loadIdentity() + self._matrix[12] = x + self._matrix[13] = y + self._matrix[14] = z + + def loadMultiply(self, lhs: "Matrix4f", rhs: "Matrix4f"): + for i in range(4): + for j in range(4): + self._matrix[i * 4 + j] = sum( + lhs._matrix[i * 4 + k] * rhs._matrix[k * 4 + j] + for k in range(4) + ) + + def multiply(self, rhs: "Matrix4f"): + result = [0.0] * 16 + for i in range(4): + for j in range(4): + result[i * 4 + j] = sum( + self._matrix[i * 4 + k] * rhs._matrix[k * 4 + j] + for k in range(4) + ) + self._matrix = result + + def transpose(self): + for i in range(4): + for j in range(i + 1, 4): + self._matrix[i * 4 + j], self._matrix[j * 4 + i] = \ + self._matrix[j * 4 + i], self._matrix[i * 4 + j] + + def toString(self) -> str: + rows = [] + for i in range(4): + row = self._matrix[i*4:(i+1)*4] + rows.append(f"[{', '.join(f'{v:.4f}' for v in row)}]") + return f"Matrix4f({', '.join(rows)})" + + def hashCode(self) -> int: + return hash(tuple(self._matrix)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, Matrix4f): + return False + return self._matrix == other._matrix + + +# ═══════════════════════════════════════════════════════════════ +# Matrix3f +# ═══════════════════════════════════════════════════════════════ + +class Matrix3f(Stub): + """Matrix3f — 3x3 浮点矩阵""" + + def __init__(self): + super().__init__() + self._matrix = [0.0] * 9 + self._matrix[0] = 1.0 + self._matrix[4] = 1.0 + self._matrix[8] = 1.0 + + def getArray(self) -> List[float]: + return list(self._matrix) + + def setArray(self, array: List[float]): + if len(array) >= 9: + self._matrix = list(array[:9]) + + def loadIdentity(self): + self._matrix = [0.0] * 9 + self._matrix[0] = 1.0 + self._matrix[4] = 1.0 + self._matrix[8] = 1.0 + + def load(self, src: "Matrix3f"): + self._matrix = list(src._matrix) + + def loadMultiply(self, lhs: "Matrix3f", rhs: "Matrix3f"): + for i in range(3): + for j in range(3): + self._matrix[i * 3 + j] = sum( + lhs._matrix[i * 3 + k] * rhs._matrix[k * 3 + j] + for k in range(3) + ) + + def multiply(self, rhs: "Matrix3f"): + result = [0.0] * 9 + for i in range(3): + for j in range(3): + result[i * 3 + j] = sum( + self._matrix[i * 3 + k] * rhs._matrix[k * 3 + j] + for k in range(3) + ) + self._matrix = result + + def transpose(self): + for i in range(3): + for j in range(i + 1, 3): + self._matrix[i * 3 + j], self._matrix[j * 3 + i] = \ + self._matrix[j * 3 + i], self._matrix[i * 3 + j] + + def toString(self) -> str: + rows = [] + for i in range(3): + row = self._matrix[i*3:(i+1)*3] + rows.append(f"[{', '.join(f'{v:.4f}' for v in row)}]") + return f"Matrix3f({', '.join(rows)})" + + def hashCode(self) -> int: + return hash(tuple(self._matrix)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, Matrix3f): + return False + return self._matrix == other._matrix + + +# ═══════════════════════════════════════════════════════════════ +# Script (base) +# ═══════════════════════════════════════════════════════════════ + +class Script(Stub): + """Script — RenderScript 脚本基类""" + + def __init__(self, rs: Optional[RenderScript] = None): + super().__init__() + self._rs = rs + self._kernel_groups: List[Any] = [] + + def setVar(self, index: int, value: Any): + """设置脚本变量""" + pass + + def getVar(self, index: int) -> Any: + return None + + def setTimeZone(self, timezone: str): + pass + + def forEach(self, slot: int, ains: List[Allocation], + aout: Allocation, vdata: Any = None): + """执行 forEach 内核""" + raise AndroidOnlyError("Script.forEach() 仅在 Android (RenderScript) 设备上可用") + + def invoke(self, slot: int): + """调用可调用函数""" + pass + + def bindAllocation(self, va: Allocation, slot: int): + """绑定分配""" + pass + + def destroy(self): + pass + + def toString(self) -> str: + return f"Script@{hex(id(self))}" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# ScriptC +# ═══════════════════════════════════════════════════════════════ + +class ScriptC(Script): + """ScriptC — 编译的 RenderScript 内核脚本""" + + def __init__(self, rs: RenderScript, resource_id: int = 0): + super().__init__(rs) + self._resource_id = resource_id + + def toString(self) -> str: + return f"ScriptC(resourceId={self._resource_id})" + + +# ═══════════════════════════════════════════════════════════════ +# ScriptIntrinsicBlur +# ═══════════════════════════════════════════════════════════════ + +class ScriptIntrinsicBlur(Script): + """ScriptIntrinsicBlur — 高斯模糊内在函数""" + + def __init__(self, rs: RenderScript): + super().__init__(rs) + self._radius: float = 5.0 + self._input: Optional[Allocation] = None + + @staticmethod + def create(rs: RenderScript, e: Element) -> "ScriptIntrinsicBlur": + """创建模糊内在函数""" + return ScriptIntrinsicBlur(rs) + + def setRadius(self, radius: float): + """设置模糊半径""" + self._radius = max(0.0, min(radius, 25.0)) + + def setInput(self, alloc: Allocation): + """设置输入分配""" + self._input = alloc + + def forEach(self, aout: Allocation): + """执行模糊""" + if self._input and aout: + pass # 在桌面环境不执行实际模糊 + + def toString(self) -> str: + return f"ScriptIntrinsicBlur(radius={self._radius})" + + +# ═══════════════════════════════════════════════════════════════ +# ScriptIntrinsicConvolve3x3 +# ═══════════════════════════════════════════════════════════════ + +class ScriptIntrinsicConvolve3x3(Script): + """ScriptIntrinsicConvolve3x3 — 3x3 卷积内在函数""" + + def __init__(self, rs: RenderScript): + super().__init__(rs) + self._coefficients: List[float] = [0.0] * 9 + self._input: Optional[Allocation] = None + + @staticmethod + def create(rs: RenderScript, e: Element) -> "ScriptIntrinsicConvolve3x3": + """创建卷积内在函数""" + return ScriptIntrinsicConvolve3x3(rs) + + def setCoefficients(self, coeffs: List[float]): + """设置卷积系数""" + for i in range(min(len(coeffs), 9)): + self._coefficients[i] = coeffs[i] + + def setInput(self, alloc: Allocation): + """设置输入分配""" + self._input = alloc + + def forEach(self, aout: Allocation): + """执行卷积""" + pass + + def toString(self) -> str: + return f"ScriptIntrinsicConvolve3x3(coefficients={self._coefficients})" + + +# ═══════════════════════════════════════════════════════════════ +# 初始化 Element 静态实例 +# ═══════════════════════════════════════════════════════════════ + +def _init_elements(): + """初始化 Element 预定义实例(在模块导入时调用)""" + rs = RenderScript.create() + Element.F32 = Element.F32(rs) + Element.F64 = Element.F64(rs) + Element.I8 = Element.I8(rs) + Element.I16 = Element.I16(rs) + Element.I32 = Element.I32(rs) + Element.I64 = Element.I64(rs) + Element.U8 = Element.U8(rs) + Element.U16 = Element.U16(rs) + Element.U32 = Element.U32(rs) + Element.U64 = Element.U64(rs) + Element.BOOLEAN = Element.BOOL(rs) + Element.ALLOCATION = Element(rs, Element.DataType.RS_ELEMENT, 1) + Element.A_8 = Element.createPixel(rs, Element.DataType.UNSIGNED_8, 1) + Element.RGB_565 = Element.createPixel(rs, Element.DataType.UNSIGNED_5_6_5, 3) + Element.RGBA_4444 = Element.createPixel(rs, Element.DataType.UNSIGNED_4_4_4_4, 4) + Element.RGBA_8888 = Element.createPixel(rs, Element.DataType.UNSIGNED_8, 4) + Element.F32_2 = Element.createVector(rs, Element.DataType.FLOAT_32, 2) + Element.F32_3 = Element.createVector(rs, Element.DataType.FLOAT_32, 3) + Element.F32_4 = Element.createVector(rs, Element.DataType.FLOAT_32, 4) + Element.F64_2 = Element.createVector(rs, Element.DataType.FLOAT_64, 2) + Element.F64_3 = Element.createVector(rs, Element.DataType.FLOAT_64, 3) + Element.F64_4 = Element.createVector(rs, Element.DataType.FLOAT_64, 4) + Element.I8_2 = Element.createVector(rs, Element.DataType.SIGNED_8, 2) + Element.I8_3 = Element.createVector(rs, Element.DataType.SIGNED_8, 3) + Element.I8_4 = Element.createVector(rs, Element.DataType.SIGNED_8, 4) + Element.I16_2 = Element.createVector(rs, Element.DataType.SIGNED_16, 2) + Element.I16_3 = Element.createVector(rs, Element.DataType.SIGNED_16, 3) + Element.I16_4 = Element.createVector(rs, Element.DataType.SIGNED_16, 4) + Element.I32_2 = Element.createVector(rs, Element.DataType.SIGNED_32, 2) + Element.I32_3 = Element.createVector(rs, Element.DataType.SIGNED_32, 3) + Element.I32_4 = Element.createVector(rs, Element.DataType.SIGNED_32, 4) + Element.I64_2 = Element.createVector(rs, Element.DataType.SIGNED_64, 2) + Element.I64_3 = Element.createVector(rs, Element.DataType.SIGNED_64, 3) + Element.I64_4 = Element.createVector(rs, Element.DataType.SIGNED_64, 4) + Element.U8_2 = Element.createVector(rs, Element.DataType.UNSIGNED_8, 2) + Element.U8_3 = Element.createVector(rs, Element.DataType.UNSIGNED_8, 3) + Element.U8_4 = Element.createVector(rs, Element.DataType.UNSIGNED_8, 4) + Element.U16_2 = Element.createVector(rs, Element.DataType.UNSIGNED_16, 2) + Element.U16_3 = Element.createVector(rs, Element.DataType.UNSIGNED_16, 3) + Element.U16_4 = Element.createVector(rs, Element.DataType.UNSIGNED_16, 4) + Element.U32_2 = Element.createVector(rs, Element.DataType.UNSIGNED_32, 2) + Element.U32_3 = Element.createVector(rs, Element.DataType.UNSIGNED_32, 3) + Element.U32_4 = Element.createVector(rs, Element.DataType.UNSIGNED_32, 4) + Element.U64_2 = Element.createVector(rs, Element.DataType.UNSIGNED_64, 2) + Element.U64_3 = Element.createVector(rs, Element.DataType.UNSIGNED_64, 3) + Element.U64_4 = Element.createVector(rs, Element.DataType.UNSIGNED_64, 4) + + +_init_elements() + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "RenderScript", + "Allocation", + "Element", + "Type", + "Script", + "ScriptIntrinsicBlur", + "ScriptIntrinsicConvolve3x3", + "ScriptC", + "Matrix4f", + "Matrix3f", +] \ No newline at end of file diff --git a/android_pyapi/android/se.py b/android_pyapi/android/se.py new file mode 100644 index 0000000..00d9366 --- /dev/null +++ b/android_pyapi/android/se.py @@ -0,0 +1,175 @@ +""" +Android se 组件 — SecureElement, Session, Channel, Reader, OmapiConnection 等 +方法签名完全对齐 Android SDK (SecureElement API) +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# SecureElement +# ═══════════════════════════════════════════════════════════════ + +class SecureElement(Stub): + """SecureElement — 安全元件""" + + SERVICE_INTERFACE = "android.se.SECURE_ELEMENT_SERVICE" + EXTRA_READER = "android.se.extra.READER" + EXTRA_READER_NAME = "android.se.extra.READER_NAME" + EXTRA_SLOT_ID = "android.se.extra.SLOT_ID" + EXTRA_CONNECT_RESPONSE = "android.se.extra.CONNECT_RESPONSE" + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# Reader +# ═══════════════════════════════════════════════════════════════ + +class Reader(Stub): + """Reader — 安全元件读取器""" + + def __init__(self, name: str = "eSE"): + super().__init__() + self._name = name + self._sessions: List[Session] = [] + + def getName(self) -> str: + return self._name + + def openSession(self) -> "Session": + session = Session(self) + self._sessions.append(session) + return session + + def closeSessions(self): + for s in self._sessions: + s.close() + self._sessions.clear() + + def isSecureElementPresent(self) -> bool: + return True + + def toString(self) -> str: + return f"Reader(name={self._name})" + + +# ═══════════════════════════════════════════════════════════════ +# Session +# ═══════════════════════════════════════════════════════════════ + +class Session(Stub): + """Session — 安全元件会话""" + + def __init__(self, reader: Optional[Reader] = None): + super().__init__() + self._reader = reader + self._channels: List[Channel] = [] + self._closed = False + + def openLogicalChannel(self, aid: bytes) -> Optional["Channel"]: + if self._closed: + return None + channel = Channel(self, logical=True) + self._channels.append(channel) + channel._select_response = bytes([0x90, 0x00]) # SW_OK + return channel + + def openBasicChannel(self, aid: bytes) -> Optional["Channel"]: + if self._closed: + return None + channel = Channel(self, logical=False) + self._channels.append(channel) + channel._select_response = bytes([0x90, 0x00]) # SW_OK + return channel + + def close(self): + for c in self._channels: + c.close() + self._channels.clear() + self._closed = True + + def getSessions(self) -> List["Session"]: + return [self] + + def getChannels(self) -> List["Channel"]: + return list(self._channels) + + def toString(self) -> str: + return f"Session(reader={self._reader}, channels={len(self._channels)})" + + +# ═══════════════════════════════════════════════════════════════ +# Channel +# ═══════════════════════════════════════════════════════════════ + +class Channel(Stub): + """Channel — 安全元件通道""" + + def __init__(self, session: Session, logical: bool = True): + super().__init__() + self._session = session + self._logical = logical + self._closed = False + self._select_response: Optional[bytes] = None + + def close(self): + self._closed = True + + def sendData(self, command: bytes) -> bytes: + if self._closed: + return bytes([0x6F, 0x00]) # No such EF + # Simulate successful APDU response + return bytes([0x90, 0x00]) + + def getSelectResponse(self) -> Optional[bytes]: + return self._select_response + + def getSession(self) -> Session: + return self._session + + def isBasicChannel(self) -> bool: + return not self._logical + + def isClosed(self) -> bool: + return self._closed + + def toString(self) -> str: + return f"Channel(logical={self._logical}, closed={self._closed})" + + +# ═══════════════════════════════════════════════════════════════ +# OmapiConnection +# ═══════════════════════════════════════════════════════════════ + +class OmapiConnection(Stub): + """OmapiConnection — OMAPI 连接入口""" + + def __init__(self): + super().__init__() + + @staticmethod + def getInstance(context: Any = None) -> "OmapiConnection": + return OmapiConnection() + + def getReaders(self) -> List[Reader]: + return [Reader("eSE")] + + def getReader(self, name: str) -> Optional[Reader]: + return Reader(name) + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "SecureElement", + "Reader", + "Session", + "Channel", + "OmapiConnection", +] \ No newline at end of file diff --git a/android_pyapi/android/security.py b/android_pyapi/android/security.py new file mode 100644 index 0000000..f4a6431 --- /dev/null +++ b/android_pyapi/android/security.py @@ -0,0 +1,182 @@ +""" +Android Security 组件 — KeyChain, NetworkSecurityPolicy, AppUriAuthenticationPolicy 等 +方法签名完全对齐 Android SDK (android.security 包) +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# KeyChain +# ═══════════════════════════════════════════════════════════════ + +class KeyChain(Stub): + """KeyChain — 系统级密钥链访问 (桌面环境不可用)""" + + ACTION_KEYCHAIN_CHANGED = "android.security.action.KEYCHAIN_CHANGED" + ACTION_KEY_ACCESS_CHANGED = "android.security.action.KEY_ACCESS_CHANGED" + EXTRA_KEY_ACCESSIBLE = "android.security.extra.KEY_ACCESSIBLE" + EXTRA_KEY_ALIAS = "android.security.extra.KEY_ALIAS" + EXTRA_RESULT = "android.security.extra.RESULT" + + def __init__(self): + super().__init__() + + @staticmethod + def createInstallIntent() -> Any: + """创建安装证书的 Intent (Android-only)""" + raise AndroidOnlyError("KeyChain.createInstallIntent() 仅在 Android 设备上可用") + + @staticmethod + def choosePrivateKeyAlias(activity: Any, response: Any, + keyTypes: Optional[List[str]] = None, + issuers: Optional[List[Any]] = None, + host: Optional[str] = None, + port: int = -1, + alias: Optional[str] = None): + """选择私钥别名 (Android-only)""" + raise AndroidOnlyError("KeyChain.choosePrivateKeyAlias() 仅在 Android 设备上可用") + + @staticmethod + def isKeyAlgorithmSupported(algorithm: str) -> bool: + """检查密钥算法是否支持""" + supported = {"RSA", "EC", "DSA", "AES", "HmacSHA256", "HmacSHA1"} + return algorithm in supported + + @staticmethod + def isBoundKeyAlgorithm(algorithm: str) -> bool: + """检查是否是绑定到硬件的密钥算法 (桌面环境不支持)""" + return False + + @staticmethod + def getPrivateKey(context: Any, alias: str) -> Any: + """获取私钥 (Android-only)""" + raise AndroidOnlyError("KeyChain.getPrivateKey() 仅在 Android 设备上可用") + + @staticmethod + def getCertificateChain(context: Any, alias: str) -> Optional[List[Any]]: + """获取证书链 (Android-only)""" + raise AndroidOnlyError("KeyChain.getCertificateChain() 仅在 Android 设备上可用") + + def toString(self) -> str: + return "KeyChain(desktop-stub)" + + def hashCode(self) -> int: + return hash("android.security.KeyChain") & 0x7FFFFFFF + + def equals(self, other) -> bool: + return isinstance(other, KeyChain) + + +# ═══════════════════════════════════════════════════════════════ +# AppUriAuthenticationPolicy +# ═══════════════════════════════════════════════════════════════ + +class AppUriAuthenticationPolicy(Stub): + """AppUriAuthenticationPolicy — URI 认证策略 (Android 11+)""" + + def __init__(self): + super().__init__() + self._rules: Dict[str, List[str]] = {} + + def addAppAndUriMapping(self, app: Any, uri: Any, alias: str): + """添加应用与 URI 映射""" + app_pkg = str(app) if app else "" + uri_str = str(uri) if uri else "" + if app_pkg not in self._rules: + self._rules[app_pkg] = [] + self._rules[app_pkg].append(uri_str) + + def getAppAndUriMappings(self) -> Dict[str, List[str]]: + """获取所有规则映射""" + return dict(self._rules) + + def toString(self) -> str: + return f"AppUriAuthenticationPolicy(rules={self._rules})" + + def hashCode(self) -> int: + return hash(str(self._rules)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, AppUriAuthenticationPolicy): + return False + return self._rules == other._rules + + +# ═══════════════════════════════════════════════════════════════ +# KeyChainAliasResponse +# ═══════════════════════════════════════════════════════════════ + +class KeyChainAliasResponse(Stub): + """KeyChainAliasResponse — 密钥别名选择响应""" + + def __init__(self, alias: Optional[str] = None): + super().__init__() + self._alias = alias + + def getAlias(self) -> Optional[str]: + """获取选定的密钥别名""" + return self._alias + + def toString(self) -> str: + return f"KeyChainAliasResponse(alias={self._alias})" + + def hashCode(self) -> int: + return hash(self._alias) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, KeyChainAliasResponse): + return False + return self._alias == other._alias + + +# ═══════════════════════════════════════════════════════════════ +# NetworkSecurityPolicy +# ═══════════════════════════════════════════════════════════════ + +class NetworkSecurityPolicy(Stub): + """NetworkSecurityPolicy — 网络安全策略""" + + _instance: Optional["NetworkSecurityPolicy"] = None + + def __init__(self): + super().__init__() + self._cleartext_permitted = True + + @staticmethod + def getInstance() -> "NetworkSecurityPolicy": + """获取全局 NetworkSecurityPolicy 实例""" + if NetworkSecurityPolicy._instance is None: + NetworkSecurityPolicy._instance = NetworkSecurityPolicy() + return NetworkSecurityPolicy._instance + + def isCleartextTrafficPermitted(self, host: Optional[str] = None) -> bool: + """检查是否允许明文流量 (桌面默认允许)""" + return self._cleartext_permitted + + def setCleartextTrafficPermitted(self, permitted: bool): + """设置是否允许明文流量""" + self._cleartext_permitted = permitted + + def toString(self) -> str: + return f"NetworkSecurityPolicy(cleartextPermitted={self._cleartext_permitted})" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "KeyChain", + "AppUriAuthenticationPolicy", + "KeyChainAliasResponse", + "NetworkSecurityPolicy", +] \ No newline at end of file diff --git a/android_pyapi/android/service_autofill.py b/android_pyapi/android/service_autofill.py new file mode 100644 index 0000000..393a1f5 --- /dev/null +++ b/android_pyapi/android/service_autofill.py @@ -0,0 +1,423 @@ +from __future__ import annotations + +""" +Android Service Autofill 组件 — AutofillService, FillRequest, FillResponse, +SaveRequest, SaveInfo, Dataset, AutofillFieldClassificationService 等 +方法签名完全对齐 Android SDK (android.service.autofill 包) +""" + +import time +from typing import Any, Callable, Dict, List, Optional, Set + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# FillRequest +# ═══════════════════════════════════════════════════════════════ + +class FillRequest(Stub): + """FillRequest — 自动填充请求""" + + # 请求标志 + FLAG_MANUAL_REQUEST = 0x0001 + FLAG_COMPATIBILITY_MODE_REQUEST = 0x0002 + + def __init__(self, request_id: int = 0, flags: int = 0, + fill_contexts: Optional[List[Any]] = None): + super().__init__() + self._request_id = request_id + self._flags = flags + self._fill_contexts = fill_contexts or [] + self._client_state: Optional[Any] = None + + def getId(self) -> int: + """获取请求 ID""" + return self._request_id + + def getFlags(self) -> int: + """获取标志""" + return self._flags + + def getFillContexts(self) -> List[Any]: + """获取填充上下文""" + return list(self._fill_contexts) + + def getClientState(self) -> Optional[Any]: + return self._client_state + + def setClientState(self, state: Any): + self._client_state = state + + def toString(self) -> str: + return (f"FillRequest(id={self._request_id}, " + f"flags={self._flags}, contexts={len(self._fill_contexts)})") + + def hashCode(self) -> int: + return self._request_id & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, FillRequest): + return False + return self._request_id == other._request_id + + +# ═══════════════════════════════════════════════════════════════ +# Dataset +# ═══════════════════════════════════════════════════════════════ + +class Dataset(Stub): + """Dataset — 自动填充数据集""" + + def __init__(self): + super().__init__() + self._field_values: Dict[Any, Any] = {} + self._id: Optional[Any] = None + self._authentication: Optional[Any] = None + + def getFieldValues(self) -> Dict[Any, Any]: + return dict(self._field_values) + + def getId(self) -> Optional[Any]: + return self._id + + def setId(self, id: Any): + self._id = id + + def toString(self) -> str: + return f"Dataset(fields={len(self._field_values)}, id={self._id})" + + def hashCode(self) -> int: + return hash(str(self._id)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, Dataset): + return False + return self._id == other._id + + # ── Builder ── + + class Builder(Stub): + """Builder — Dataset 构建器""" + + def __init__(self): + super().__init__() + self._dataset = Dataset() + + def setId(self, id: Any) -> "Dataset.Builder": + self._dataset._id = id + return self + + def setValue(self, id: Any, value: Any) -> "Dataset.Builder": + self._dataset._field_values[id] = value + return self + + def setAuthentication(self, authentication: Any) -> "Dataset.Builder": + self._dataset._authentication = authentication + return self + + def build(self) -> Dataset: + return self._dataset + + +# ═══════════════════════════════════════════════════════════════ +# SaveInfo +# ═══════════════════════════════════════════════════════════════ + +class SaveInfo(Stub): + """SaveInfo — 保存信息""" + + # 保存类型 + SAVE_DATA_TYPE_EMAIL = 0x0001 + SAVE_DATA_TYPE_PASSWORD = 0x0002 + SAVE_DATA_TYPE_USERNAME = 0x0004 + SAVE_DATA_TYPE_CREDIT_CARD = 0x0008 + SAVE_DATA_TYPE_ADDRESS = 0x0010 + SAVE_DATA_TYPE_GENERIC = 0x0020 + SAVE_DATA_TYPE_PASSWORD_V2 = 0x0040 + SAVE_DATA_TYPE_SMS_2FA = 0x0080 + SAVE_DATA_TYPE_BIRTH_DATE = 0x0100 + SAVE_DATA_TYPE_EMAIL_ADDRESS = 0x0200 + SAVE_DATA_TYPE_PHONE = 0x0400 + SAVE_DATA_TYPE_USERNAME_V2 = 0x0800 + SAVE_DATA_TYPE_CARD_NUMBER = 0x1000 + SAVE_DATA_TYPE_CARD_EXPIRATION_DATE = 0x2000 + SAVE_DATA_TYPE_CARD_SECURITY_CODE = 0x4000 + SAVE_DATA_TYPE_FI_ACCOUNT_NUMBER = 0x8000 + SAVE_DATA_TYPE_FI_ROUTING_NUMBER = 0x10000 + SAVE_DATA_TYPE_PASSPORT = 0x20000 + SAVE_DATA_TYPE_DRIVERS_LICENSE = 0x40000 + SAVE_DATA_TYPE_SSN = 0x80000 + + # 标志 + FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 0x0001 + + def __init__(self): + super().__init__() + self._type: int = SaveInfo.SAVE_DATA_TYPE_GENERIC + self._required_ids: List[Any] = [] + self._optional_ids: List[Any] = [] + self._description: Optional[Any] = None + self._flags: int = 0 + self._trigger_ids: List[Any] = [] + self._validator: Optional[Any] = None + + def getType(self) -> int: + return self._type + + def getRequiredIds(self) -> List[Any]: + return list(self._required_ids) + + def getOptionalIds(self) -> List[Any]: + return list(self._optional_ids) + + def getDescription(self) -> Optional[Any]: + return self._description + + def getFlags(self) -> int: + return self._flags + + def toString(self) -> str: + return (f"SaveInfo(type={self._type}, required={len(self._required_ids)}, " + f"optional={len(self._optional_ids)})") + + def hashCode(self) -> int: + return hash((self._type, tuple(self._required_ids))) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, SaveInfo): + return False + return self._type == other._type + + # ── Builder ── + + class Builder(Stub): + """Builder — SaveInfo 构建器""" + + def __init__(self, type_: int, required_ids: List[Any]): + super().__init__() + self._save_info = SaveInfo() + self._save_info._type = type_ + self._save_info._required_ids = list(required_ids) + + def setOptionalIds(self, optional_ids: List[Any]) -> "SaveInfo.Builder": + self._save_info._optional_ids = list(optional_ids) + return self + + def setDescription(self, description: Any) -> "SaveInfo.Builder": + self._save_info._description = description + return self + + def setFlags(self, flags: int) -> "SaveInfo.Builder": + self._save_info._flags = flags + return self + + def setTriggerIds(self, trigger_ids: List[Any]) -> "SaveInfo.Builder": + self._save_info._trigger_ids = list(trigger_ids) + return self + + def setValidator(self, validator: Any) -> "SaveInfo.Builder": + self._save_info._validator = validator + return self + + def build(self) -> SaveInfo: + return self._save_info + + +# ═══════════════════════════════════════════════════════════════ +# FillResponse +# ═══════════════════════════════════════════════════════════════ + +class FillResponse(Stub): + """FillResponse — 自动填充响应""" + + def __init__(self): + super().__init__() + self._datasets: List[Dataset] = [] + self._save_info: Optional[SaveInfo] = None + self._client_state: Optional[Any] = None + self._authentication_result: Optional[Any] = None + + def getDatasets(self) -> List[Dataset]: + return list(self._datasets) + + def getSaveInfo(self) -> Optional[SaveInfo]: + return self._save_info + + def getClientState(self) -> Optional[Any]: + return self._client_state + + def toString(self) -> str: + return (f"FillResponse(datasets={len(self._datasets)}, " + f"saveInfo={'set' if self._save_info else 'none'})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + # ── Builder ── + + class Builder(Stub): + """Builder — FillResponse 构建器""" + + def __init__(self): + super().__init__() + self._response = FillResponse() + + def addDataset(self, dataset: Dataset) -> "FillResponse.Builder": + self._response._datasets.append(dataset) + return self + + def setDatasets(self, datasets: List[Dataset]) -> "FillResponse.Builder": + self._response._datasets = list(datasets) + return self + + def setSaveInfo(self, save_info: SaveInfo) -> "FillResponse.Builder": + self._response._save_info = save_info + return self + + def setClientState(self, client_state: Any) -> "FillResponse.Builder": + self._response._client_state = client_state + return self + + def setAuthenticationResult(self, result: Any) -> "FillResponse.Builder": + self._response._authentication_result = result + return self + + def build(self) -> FillResponse: + return self._response + + +# ═══════════════════════════════════════════════════════════════ +# SaveRequest +# ═══════════════════════════════════════════════════════════════ + +class SaveRequest(Stub): + """SaveRequest — 保存请求""" + + def __init__(self, request_id: int = 0, + client_state: Optional[Any] = None, + fill_contexts: Optional[List[Any]] = None): + super().__init__() + self._request_id = request_id + self._client_state = client_state + self._fill_contexts = fill_contexts or [] + + def getId(self) -> int: + return self._request_id + + def getClientState(self) -> Optional[Any]: + return self._client_state + + def getFillContexts(self) -> List[Any]: + return list(self._fill_contexts) + + def toString(self) -> str: + return (f"SaveRequest(id={self._request_id}, " + f"contexts={len(self._fill_contexts)})") + + def hashCode(self) -> int: + return self._request_id & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, SaveRequest): + return False + return self._request_id == other._request_id + + +# ═══════════════════════════════════════════════════════════════ +# AutofillService +# ═══════════════════════════════════════════════════════════════ + +class AutofillService(Stub): + """AutofillService — 自动填充服务""" + + SERVICE_INTERFACE = "android.service.autofill.AutofillService" + SERVICE_META_DATA = "android.autofill" + + def __init__(self): + super().__init__() + + def onFillRequest(self, request: FillRequest, + cancellation_signal: Any, + callback: Any): + """填充请求回调""" + response = FillResponse() + if callback and hasattr(callback, 'onSuccess'): + callback.onSuccess(response) + + def onSaveRequest(self, request: SaveRequest, callback: Any): + """保存请求回调""" + if callback and hasattr(callback, 'onSuccess'): + callback.onSuccess() + + def onConnected(self): + """服务已连接""" + pass + + def onDisconnected(self): + """服务已断开""" + pass + + def onUnbind(self, intent: Any) -> bool: + return False + + def toString(self) -> str: + return f"AutofillService@{hex(id(self))}" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# AutofillFieldClassificationService +# ═══════════════════════════════════════════════════════════════ + +class AutofillFieldClassificationService(Stub): + """AutofillFieldClassificationService — 自动填充字段分类服务""" + + SERVICE_INTERFACE = ( + "android.service.autofill.AutofillFieldClassificationService" + ) + + def __init__(self): + super().__init__() + + def onConnected(self): + pass + + def onDisconnected(self): + pass + + def onClassificationRequest(self, request: Any, callback: Any): + if callback and hasattr(callback, 'onSuccess'): + callback.onSuccess(None) + + def toString(self) -> str: + return f"AutofillFieldClassificationService@{hex(id(self))}" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "AutofillService", + "FillRequest", + "FillResponse", + "SaveRequest", + "SaveInfo", + "Dataset", + "AutofillFieldClassificationService", +] \ No newline at end of file diff --git a/android_pyapi/android/service_controls.py b/android_pyapi/android/service_controls.py new file mode 100644 index 0000000..8a1e45c --- /dev/null +++ b/android_pyapi/android/service_controls.py @@ -0,0 +1,350 @@ +""" +Android Service Controls 组件 — ControlsProviderService, Control, DeviceTypes, +StatefulControl, StatelessControl 等 +方法签名完全对齐 Android SDK (android.service.controls 包) +""" + +from typing import Any, Callable, Dict, List, Optional, Set + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# DeviceTypes +# ═══════════════════════════════════════════════════════════════ + +class DeviceTypes(Stub): + """DeviceTypes — 设备类型常量""" + + TYPE_AC_HEATER = "AC_HEATER" + TYPE_AC_UNIT = "AC_UNIT" + TYPE_AIR_FRESHENER = "AIR_FRESHENER" + TYPE_AIR_PURIFIER = "AIR_PURIFIER" + TYPE_BLINDS = "BLINDS" + TYPE_CAMERA = "CAMERA" + TYPE_CLOSET = "CLOSET" + TYPE_COFFEE_MAKER = "COFFEE_MAKER" + TYPE_CURTAIN = "CURTAIN" + TYPE_DEHUMIDIFIER = "DEHUMIDIFIER" + TYPE_DISHWASHER = "DISHWASHER" + TYPE_DISPLAY = "DISPLAY" + TYPE_DOOR = "DOOR" + TYPE_DOORBELL = "DOORBELL" + TYPE_DRAWER = "DRAWER" + TYPE_DRYER = "DRYER" + TYPE_FAN = "FAN" + TYPE_GARAGE = "GARAGE" + TYPE_GATE = "GATE" + TYPE_HEATER = "HEATER" + TYPE_HOOD = "HOOD" + TYPE_HUMIDIFIER = "HUMIDIFIER" + TYPE_KETTLE = "KETTLE" + TYPE_LIGHT = "LIGHT" + TYPE_LIGHTBULB = "LIGHTBULB" + TYPE_LOCK = "LOCK" + TYPE_MICROWAVE = "MICROWAVE" + TYPE_MOP = "MOP" + TYPE_MOWER = "MOWER" + TYPE_MULTICOOKER = "MULTICOOKER" + TYPE_OUTLET = "OUTLET" + TYPE_OVEN = "OVEN" + TYPE_PERGOLLA = "PERGOLLA" + TYPE_RADIATOR = "RADIATOR" + TYPE_REFRIGERATOR = "REFRIGERATOR" + TYPE_REMOTE_STAND = "REMOTE_STAND" + TYPE_RICE_COOKER = "RICE_COOKER" + TYPE_ROUTINE = "ROUTINE" + TYPE_SCENE = "SCENE" + TYPE_SECURITY_SYSTEM = "SECURITY_SYSTEM" + TYPE_SET_TOP = "SET_TOP" + TYPE_SHOWER = "SHOWER" + TYPE_SHUTTER = "SHUTTER" + TYPE_SMOKE_DETECTOR = "SMOKE_DETECTOR" + TYPE_SOUNDBAR = "SOUNDBAR" + TYPE_SPRINKLER = "SPRINKLER" + TYPE_STANDMIXER = "STANDMIXER" + TYPE_THERMOSTAT = "THERMOSTAT" + TYPE_TV = "TV" + TYPE_VACUUM = "VACUUM" + TYPE_VALVE = "VALVE" + TYPE_WASHER = "WASHER" + TYPE_WATER_HEATER = "WATER_HEATER" + TYPE_WINDOW = "WINDOW" + TYPE_WINE_COOLER = "WINE_COOLER" + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# Control +# ═══════════════════════════════════════════════════════════════ + +class Control(Stub): + """Control — 设备控件基类""" + + # 状态 + STATUS_UNKNOWN = 0 + STATUS_OK = 1 + STATUS_NOT_RESPONDING = 2 + STATUS_ERROR = 3 + STATUS_FIRMWARE_UPDATE = 4 + + def __init__(self, control_id: str, title: str, + subtitle: str, device_type: str): + super().__init__() + self._control_id = control_id + self._title = title + self._subtitle = subtitle + self._device_type = device_type + self._status = Control.STATUS_OK + self._status_text: str = "" + self._custom_icon: Optional[Any] = None + self._custom_color: int = 0 + self._is_auth_required: bool = False + self._structure: str = "" + self._zone: str = "" + self._is_favorite: bool = False + self._device_types_list: List[str] = [device_type] + self._parent_id: Optional[str] = None + + def getControlId(self) -> str: + """获取控件 ID""" + return self._control_id + + def getTitle(self) -> str: + """获取标题""" + return self._title + + def getSubtitle(self) -> str: + """获取副标题""" + return self._subtitle + + def getDeviceType(self) -> str: + """获取设备类型""" + return self._device_type + + def getStatus(self) -> int: + return self._status + + def setStatus(self, status: int): + self._status = status + + def getStatusText(self) -> str: + return self._status_text + + def setStatusText(self, status_text: str): + self._status_text = status_text + + def getStructure(self) -> str: + return self._structure + + def setStructure(self, structure: str): + self._structure = structure + + def getZone(self) -> str: + return self._zone + + def setZone(self, zone: str): + self._zone = zone + + def isFavorite(self) -> bool: + return self._is_favorite + + def setFavorite(self, favorite: bool): + self._is_favorite = favorite + + def toString(self) -> str: + return (f"Control(id={self._control_id}, title={self._title}, " + f"type={self._device_type}, status={self._status})") + + def hashCode(self) -> int: + return hash(self._control_id) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, Control): + return False + return self._control_id == other._control_id + + +# ═══════════════════════════════════════════════════════════════ +# StatefulControl +# ═══════════════════════════════════════════════════════════════ + +class StatefulControl(Control): + """StatefulControl — 有状态控件""" + + def __init__(self, control_id: str, title: str, + subtitle: str, device_type: str): + super().__init__(control_id, title, subtitle, device_type) + self._current_value: Any = None + self._min_value: Optional[float] = None + self._max_value: Optional[float] = None + self._step: Optional[float] = None + self._unit: Optional[str] = None + self._is_on: bool = False + self._mode: str = "" + + def setStatus(self, status: int) -> "StatefulControl": + """设置状态""" + self._status = status + return self + + def setStatusText(self, status_text: str) -> "StatefulControl": + """设置状态文本""" + self._status_text = status_text + return self + + def getCurrentValue(self) -> Any: + return self._current_value + + def setCurrentValue(self, value: Any) -> "StatefulControl": + self._current_value = value + return self + + def getMinValue(self) -> Optional[float]: + return self._min_value + + def setMinValue(self, min_value: float) -> "StatefulControl": + self._min_value = min_value + return self + + def getMaxValue(self) -> Optional[float]: + return self._max_value + + def setMaxValue(self, max_value: float) -> "StatefulControl": + self._max_value = max_value + return self + + def getStep(self) -> Optional[float]: + return self._step + + def setStep(self, step: float) -> "StatefulControl": + self._step = step + return self + + def isOn(self) -> bool: + return self._is_on + + def setOn(self, is_on: bool) -> "StatefulControl": + self._is_on = is_on + return self + + def getMode(self) -> str: + return self._mode + + def setMode(self, mode: str) -> "StatefulControl": + self._mode = mode + return self + + def toString(self) -> str: + return (f"StatefulControl(id={self._control_id}, title={self._title}, " + f"type={self._device_type}, on={self._is_on}, " + f"value={self._current_value})") + + +# ═══════════════════════════════════════════════════════════════ +# StatelessControl +# ═══════════════════════════════════════════════════════════════ + +class StatelessControl(Control): + """StatelessControl — 无状态控件""" + + def __init__(self, control_id: str, title: str, + subtitle: str, device_type: str): + super().__init__(control_id, title, subtitle, device_type) + self._action_text: str = "" + + def getActionText(self) -> str: + return self._action_text + + def setActionText(self, action_text: str) -> "StatelessControl": + self._action_text = action_text + return self + + def toString(self) -> str: + return (f"StatelessControl(id={self._control_id}, " + f"title={self._title}, type={self._device_type})") + + +# ═══════════════════════════════════════════════════════════════ +# ControlsProviderService +# ═══════════════════════════════════════════════════════════════ + +class ControlsProviderService(Stub): + """ControlsProviderService — 控件提供者服务""" + + SERVICE_INTERFACE = "android.service.controls.ControlsProviderService" + + # 控件类型 + CONTROL_TYPE_TOGGLE = 1 + CONTROL_TYPE_RANGE = 2 + CONTROL_TYPE_STATELESS = 3 + CONTROL_TYPE_BUTTON = 4 + + def __init__(self): + super().__init__() + self._controls: Dict[str, Control] = {} + self._publishers: Dict[str, Any] = {} + self._subscribers: Dict[str, Any] = {} + + def onCreatePublisherFor(self, control_ids: List[str]) -> Any: + """为指定控件创建发布者""" + publisher = type("Publisher", (), { + "onUnsubscribe": lambda: None, + "onRequest": lambda cid: None, + })() + for cid in control_ids: + self._publishers[cid] = publisher + return publisher + + def onGetSubscriberFor(self, control_ids: List[str]) -> Any: + """获取控件订阅者""" + subscriber = type("Subscriber", (), { + "onSubscribe": lambda cid: None, + "onUnsubscribe": lambda cid: None, + })() + for cid in control_ids: + self._subscribers[cid] = subscriber + return subscriber + + def publishControls(self, control_ids: List[str], controls: List[Control]): + """发布控件状态""" + for control in controls: + self._controls[control.getControlId()] = control + + def addControl(self, control: Control): + """添加控件""" + self._controls[control.getControlId()] = control + + def removeControl(self, control_id: str): + """移除控件""" + self._controls.pop(control_id, None) + + def getControl(self, control_id: str) -> Optional[Control]: + return self._controls.get(control_id) + + def getControls(self) -> List[Control]: + return list(self._controls.values()) + + def toString(self) -> str: + return f"ControlsProviderService(controls={len(self._controls)})" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "ControlsProviderService", + "Control", + "DeviceTypes", + "StatefulControl", + "StatelessControl", +] \ No newline at end of file diff --git a/android_pyapi/android/service_dreams.py b/android_pyapi/android/service_dreams.py new file mode 100644 index 0000000..0248fa2 --- /dev/null +++ b/android_pyapi/android/service_dreams.py @@ -0,0 +1,140 @@ +""" +Android Service Dreams 组件 — DreamService 等 +方法签名完全对齐 Android SDK (android.service.dreams 包) +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# DreamService +# ═══════════════════════════════════════════════════════════════ + +class DreamService(Stub): + """DreamService — 屏保服务基类""" + + # 服务接口 + SERVICE_INTERFACE = "android.service.dreams.DreamService" + SERVICE_META_DATA = "android.service.dream" + + # 退出原因 + EXIT_CAUSE_UNKNOWN = 0 + EXIT_CAUSE_TIMEOUT = 1 + EXIT_CAUSE_USER = 2 + EXIT_CAUSE_DREAM_FINISHED = 3 + + def __init__(self): + super().__init__() + self._fullscreen: bool = False + self._interactive: bool = False + self._dreaming: bool = False + self._window: Optional[Any] = None + self._content_view: Optional[Any] = None + self._exit_cause: int = DreamService.EXIT_CAUSE_UNKNOWN + self._created: bool = False + self._screen_bright: bool = True + self._doze_component: Optional[Any] = None + self._doze_mode: bool = False + self._debug: bool = False + + def setContentView(self, view: Any): + """设置内容视图""" + self._content_view = view + + def getContentView(self) -> Optional[Any]: + return self._content_view + + def setFullscreen(self, fullscreen: bool): + """设置全屏模式""" + self._fullscreen = fullscreen + + def isFullscreen(self) -> bool: + """检查是否全屏""" + return self._fullscreen + + def isInteractive(self) -> bool: + """检查是否可交互""" + return self._interactive + + def setInteractive(self, interactive: bool): + """设置可交互""" + self._interactive = interactive + + def getWindow(self) -> Optional[Any]: + """获取窗口""" + return self._window + + def startDream(self): + """开始屏保""" + self._dreaming = True + + def awaken(self): + """唤醒/结束屏保""" + self._dreaming = False + + def onDreamingStarted(self): + """屏保开始时回调""" + self._dreaming = True + + def onDreamingStopped(self): + """屏保停止时回调""" + self._dreaming = False + + def onCreate(self): + """创建时回调""" + self._created = True + + def onDestroy(self): + """销毁时回调""" + self._created = False + self._dreaming = False + + def onAttachedToWindow(self): + """附加到窗口时回调""" + pass + + def onDetachedFromWindow(self): + """从窗口分离时回调""" + pass + + def isDreaming(self) -> bool: + """检查是否正在运行屏保""" + return self._dreaming + + def getExitCause(self) -> int: + return self._exit_cause + + def setExitCause(self, cause: int): + self._exit_cause = cause + + def onWakeUp(self): + """唤醒回调""" + self.awaken() + + def canDoze(self) -> bool: + return self._doze_mode + + def setDozeComponent(self, component: Any): + self._doze_component = component + + def toString(self) -> str: + return (f"DreamService(dreaming={self._dreaming}, " + f"fullscreen={self._fullscreen}, " + f"interactive={self._interactive})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "DreamService", +] \ No newline at end of file diff --git a/android_pyapi/android/service_notification.py b/android_pyapi/android/service_notification.py new file mode 100644 index 0000000..f397be8 --- /dev/null +++ b/android_pyapi/android/service_notification.py @@ -0,0 +1,378 @@ +""" +Android Service Notification 组件 — NotificationListenerService, StatusBarNotification, RankingMap 等 +方法签名完全对齐 Android SDK (android.service.notification 包) +""" + +import time +from typing import Any, Callable, Dict, List, Optional, Set, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# StatusBarNotification +# ═══════════════════════════════════════════════════════════════ + +class StatusBarNotification(Stub): + """StatusBarNotification — 状态栏通知""" + + def __init__(self, pkg: str = "", op_pkg: str = "", + id: int = 0, tag: Optional[str] = None, + uid: int = 10001, pid: int = 1000, + user: Any = None, notification: Any = None, + post_time: int = 0): + super().__init__() + self._pkg = pkg + self._op_pkg = op_pkg or pkg + self._id = id + self._tag = tag + self._uid = uid + self._pid = pid + self._user = user + self._notification = notification + self._post_time = post_time or int(time.time() * 1000) + self._is_clearable = True + self._is_ongoing = False + self._key = f"{pkg}|{id}|{tag or ''}" + self._override_group_key: Optional[str] = None + self._override_public = False + self._initial_source_id = 0 + self._initial_source_uid = uid + self._initial_source_pkg = pkg + + def getPackageName(self) -> str: + """获取通知所属包名""" + return self._pkg + + def getOpPkg(self) -> str: + return self._op_pkg + + def getNotification(self) -> Any: + """获取 Notification 对象""" + return self._notification + + def getId(self) -> int: + """获取通知 ID""" + return self._id + + def getTag(self) -> Optional[str]: + """获取通知标签""" + return self._tag + + def getPostTime(self) -> int: + """获取发送时间 (毫秒)""" + return self._post_time + + def getUid(self) -> int: + return self._uid + + def getPid(self) -> int: + return self._pid + + def getUser(self) -> Any: + return self._user + + def getKey(self) -> str: + return self._key + + def isClearable(self) -> bool: + """是否可以清除""" + return self._is_clearable + + def isOngoing(self) -> bool: + """是否是持续通知""" + return self._is_ongoing + + def setClearable(self, clearable: bool): + self._is_clearable = clearable + + def setOngoing(self, ongoing: bool): + self._is_ongoing = ongoing + + def getOverrideGroupKey(self) -> Optional[str]: + return self._override_group_key + + def setOverrideGroupKey(self, override_group_key: Optional[str]): + self._override_group_key = override_group_key + + def isGroup(self) -> bool: + return self._override_group_key is not None + + def clone(self) -> "StatusBarNotification": + """克隆通知""" + clone = StatusBarNotification( + pkg=self._pkg, op_pkg=self._op_pkg, id=self._id, + tag=self._tag, uid=self._uid, pid=self._pid, + user=self._user, notification=self._notification, + post_time=self._post_time, + ) + clone._is_clearable = self._is_clearable + clone._is_ongoing = self._is_ongoing + clone._key = self._key + clone._override_group_key = self._override_group_key + return clone + + def toString(self) -> str: + return (f"StatusBarNotification(pkg={self._pkg}, id={self._id}, " + f"tag={self._tag}, postTime={self._post_time})") + + def hashCode(self) -> int: + return hash(self._key) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, StatusBarNotification): + return False + return self._key == other._key + + +# ═══════════════════════════════════════════════════════════════ +# NotificationListenerService +# ═══════════════════════════════════════════════════════════════ + +class NotificationListenerService(Stub): + """NotificationListenerService — 通知监听服务""" + + # 取消原因 + REASON_LISTENER_CANCEL = 1 + REASON_CLICK = 2 + REASON_CANCEL = 3 + REASON_DELETE = 4 + REASON_PACKAGE_CHANGED = 5 + REASON_USER_STOPPED = 6 + REASON_PACKACE_BANNED = 7 + REASON_APP_CANCEL = 8 + REASON_APP_CANCEL_ALL = 9 + REASON_CHANNEL_BANNED = 10 + REASON_SNOOZED = 11 + REASON_TIMEOUT = 12 + REASON_ERROR = 13 + REASON_GROUP_SUMMARY_CANCELED = 14 + REASON_ASSISTANT_CANCEL = 15 + + # 中断过滤器 + INTERRUPTION_FILTER_NONE = 0 + INTERRUPTION_FILTER_PRIORITY = 1 + INTERRUPTION_FILTER_ALARMS = 2 + INTERRUPTION_FILTER_ALL = 3 + INTERRUPTION_FILTER_UNKNOWN = 4 + + # 提示 + HINT_HOST_DISABLE_CALL_EFFECTS = 1 + HINT_HOST_DISABLE_EFFECTS = 2 + HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 3 + + # SUW 组件 + TRAMPOLINE_ACTIVITY_CLASS = ( + "com.android.settings.Settings$NotificationAppListActivity" + ) + + def __init__(self): + super().__init__() + self._active_notifications: Dict[str, StatusBarNotification] = {} + self._keys_list: List[str] = [] + self._interruption_filter: int = ( + NotificationListenerService.INTERRUPTION_FILTER_ALL + ) + self._ranking_map: Optional["NotificationListenerService.RankingMap"] = None + self._is_listener_bound = False + + @staticmethod + def requestRebind(component_name: Any): + """请求重新绑定通知监听器""" + pass + + @staticmethod + def requestUnbind(component_name: Any): + """请求解绑通知监听器""" + pass + + def onNotificationPosted(self, sbn: StatusBarNotification, + ranking_map: Optional["RankingMap"] = None): + """通知被发送时回调""" + if sbn: + self._active_notifications[sbn.getKey()] = sbn + self._keys_list = list(self._active_notifications.keys()) + if ranking_map: + self._ranking_map = ranking_map + + def onNotificationRemoved(self, sbn: StatusBarNotification, + ranking_map: Optional["RankingMap"] = None, + reason: int = REASON_LISTENER_CANCEL): + """通知被移除时回调""" + if sbn: + key = sbn.getKey() + self._active_notifications.pop(key, None) + if key in self._keys_list: + self._keys_list.remove(key) + + def onListenerConnected(self): + """监听器已连接""" + self._is_listener_bound = True + + def onListenerDisconnected(self): + """监听器已断开""" + self._is_listener_bound = False + + def onNotificationRemovedReason(self, sbn: StatusBarNotification, + ranking_map: "RankingMap", + reason: int): + """通知被移除 (带原因)""" + self.onNotificationRemoved(sbn, ranking_map, reason) + + def onNotificationPostedWithRankingMap(self, sbn: StatusBarNotification, + ranking_map: "RankingMap"): + """通知被发送 (带排名映射)""" + self.onNotificationPosted(sbn, ranking_map) + + def getActiveNotifications(self) -> List[StatusBarNotification]: + """获取活动通知列表""" + return list(self._active_notifications.values()) + + def getActiveNotificationsWithRankingMap(self) -> Tuple[ + List[StatusBarNotification], + Optional["RankingMap"]]: + """获取活动通知及排名映射""" + return list(self._active_notifications.values()), self._ranking_map + + def getCurrentInterruptionFilter(self) -> int: + """获取当前中断过滤器""" + return self._interruption_filter + + def setInterruptionFilter(self, interruption_filter: int): + """设置中断过滤器""" + self._interruption_filter = interruption_filter + + def cancelNotification(self, key: str): + """取消指定通知""" + self._active_notifications.pop(key, None) + if key in self._keys_list: + self._keys_list.remove(key) + + def cancelAllNotifications(self): + """取消所有通知""" + self._active_notifications.clear() + self._keys_list.clear() + + def requestRebind(self, component_name: Any): + """请求重新绑定""" + pass + + def requestUnbind(self): + """请求解绑""" + pass + + def registerAsSystemService(self, context: Any, component_name: Any, + user: int): + pass + + def unregisterAsSystemService(self): + pass + + def isBound(self) -> bool: + return self._is_listener_bound + + def toString(self) -> str: + return (f"NotificationListenerService(" + f"notifications={len(self._active_notifications)}, " + f"filter={self._interruption_filter})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + # ── RankingMap ── + + class RankingMap(Stub): + """RankingMap — 通知排名映射""" + + def __init__(self): + super().__init__() + self._rankings: Dict[str, "NotificationListenerService.Ranking"] = {} + + def getOrderedKeys(self) -> List[str]: + """获取有序键列表""" + return list(self._rankings.keys()) + + def getRanking(self, key: str) -> Optional["NotificationListenerService.Ranking"]: + """获取指定通知的排名信息""" + return self._rankings.get(key) + + def getRawRanking(self, key: str) -> Optional[Any]: + return self._rankings.get(key) + + def toString(self) -> str: + return f"RankingMap(keys={len(self._rankings)})" + + def hashCode(self) -> int: + return hash(tuple(self._rankings.keys())) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, NotificationListenerService.RankingMap): + return False + return self._rankings == other._rankings + + class Ranking(Stub): + """Ranking — 通知排名""" + + def __init__(self): + super().__init__() + self._key: str = "" + self._rank: int = 0 + self._is_ambient: bool = False + self._matches_interruption_filter: bool = True + self._suppressed_visual_effects: int = 0 + self._importance: int = 3 # IMPORTANCE_DEFAULT + self._importance_explanation: Optional[str] = None + self._group_key: Optional[str] = None + self._override_group_key: Optional[str] = None + self._channel: Optional[Any] = None + self._is_suspended: bool = False + + def getKey(self) -> str: + return self._key + + def getRank(self) -> int: + return self._rank + + def isAmbient(self) -> bool: + return self._is_ambient + + def matchesInterruptionFilter(self) -> bool: + return self._matches_interruption_filter + + def getSuppressedVisualEffects(self) -> int: + return self._suppressed_visual_effects + + def getImportance(self) -> int: + return self._importance + + def getImportanceExplanation(self) -> Optional[str]: + return self._importance_explanation + + def getGroupKey(self) -> Optional[str]: + return self._group_key + + def getOverrideGroupKey(self) -> Optional[str]: + return self._override_group_key + + def getChannel(self) -> Optional[Any]: + return self._channel + + def isSuspended(self) -> bool: + return self._is_suspended + + def toString(self) -> str: + return (f"Ranking(key={self._key}, rank={self._rank}, " + f"importance={self._importance})") + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "NotificationListenerService", + "StatusBarNotification", +] \ No newline at end of file diff --git a/android_pyapi/android/service_quicksettings.py b/android_pyapi/android/service_quicksettings.py new file mode 100644 index 0000000..eeccab8 --- /dev/null +++ b/android_pyapi/android/service_quicksettings.py @@ -0,0 +1,214 @@ +""" +Android Service QuickSettings 组件 — TileService, Tile 等 +方法签名完全对齐 Android SDK (android.service.quicksettings 包) +""" + +import time +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# Tile +# ═══════════════════════════════════════════════════════════════ + +class Tile(Stub): + """Tile — 快捷设置磁贴""" + + # 状态常量 + STATE_ACTIVE = 1 + STATE_INACTIVE = 2 + STATE_UNAVAILABLE = 0 + + def __init__(self): + super().__init__() + self._state: int = Tile.STATE_INACTIVE + self._label: str = "" + self._subtitle: str = "" + self._content_description: str = "" + self._icon: Optional[Any] = None + self._icon_res_id: int = 0 + self._meta_data: Optional[Dict[str, Any]] = None + self._last_updated: int = 0 + + def getState(self) -> int: + """获取磁贴状态""" + return self._state + + def setState(self, state: int): + """设置磁贴状态""" + self._state = state + self._last_updated = int(time.time() * 1000) + + def getLabel(self) -> str: + """获取标签""" + return self._label + + def setLabel(self, label: str): + """设置标签""" + self._label = label + self._last_updated = int(time.time() * 1000) + + def getSubtitle(self) -> str: + """获取副标题""" + return self._subtitle + + def setSubtitle(self, subtitle: str): + """设置副标题""" + self._subtitle = subtitle + self._last_updated = int(time.time() * 1000) + + def getContentDescription(self) -> str: + """获取内容描述""" + return self._content_description + + def setContentDescription(self, content_description: str): + """设置内容描述""" + self._content_description = content_description + + def getIcon(self) -> Optional[Any]: + """获取图标""" + return self._icon + + def setIcon(self, icon: Any): + """设置图标""" + self._icon = icon + self._last_updated = int(time.time() * 1000) + + def updateTile(self): + """更新磁贴 (通知系统刷新)""" + self._last_updated = int(time.time() * 1000) + + def getMetaData(self) -> Optional[Dict[str, Any]]: + return self._meta_data + + def setMetaData(self, meta_data: Dict[str, Any]): + self._meta_data = meta_data + + def toString(self) -> str: + state_names = { + Tile.STATE_ACTIVE: "ACTIVE", + Tile.STATE_INACTIVE: "INACTIVE", + Tile.STATE_UNAVAILABLE: "UNAVAILABLE", + } + return (f"Tile(label={self._label}, state=" + f"{state_names.get(self._state, self._state)})") + + def hashCode(self) -> int: + return hash((self._label, self._state)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, Tile): + return False + return (self._label == other._label and self._state == other._state) + + +# ═══════════════════════════════════════════════════════════════ +# TileService +# ═══════════════════════════════════════════════════════════════ + +class TileService(Stub): + """TileService — 快捷设置磁贴服务""" + + # 动作 + ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE" + ACTION_REQUEST_LISTENING = ( + "android.service.quicksettings.action.REQUEST_LISTENING" + ) + META_DATA_ACTIVE_TILE = "android.service.quicksettings.ACTIVE_TILE" + + def __init__(self): + super().__init__() + self._qs_tile: Optional[Tile] = None + self._locked: bool = False + self._secure: bool = False + self._listening: bool = False + self._tile_added: bool = False + self._tile_removed: bool = False + + @staticmethod + def requestListeningState(context: Any, component: Any): + """请求监听状态 (Android-only)""" + pass + + def isLocked(self) -> bool: + """检查设备是否锁定""" + return self._locked + + def isSecure(self) -> bool: + """检查设备是否安全锁定""" + return self._secure + + def startActivity(self, intent: Any): + """启动 Activity (Android-only)""" + raise AndroidOnlyError( + "TileService.startActivity() 仅在 Android 设备上可用" + ) + + def showDialog(self, dialog: Any): + """显示对话框""" + pass + + def getQsTile(self) -> Optional[Tile]: + """获取快速设置磁贴""" + return self._qs_tile + + def setQsTile(self, tile: Tile): + self._qs_tile = tile + + def unlockAndRun(self, runnable: Callable): + """解锁并运行""" + runnable() + + def onTileAdded(self): + """磁贴被添加时回调""" + self._tile_added = True + + def onTileRemoved(self): + """磁贴被移除时回调""" + self._tile_removed = True + + def onStartListening(self): + """开始监听时回调""" + self._listening = True + + def onStopListening(self): + """停止监听时回调""" + self._listening = False + + def onClick(self): + """被点击时回调""" + if self._qs_tile: + current_state = self._qs_tile.getState() + if current_state == Tile.STATE_ACTIVE: + self._qs_tile.setState(Tile.STATE_INACTIVE) + elif current_state == Tile.STATE_INACTIVE: + self._qs_tile.setState(Tile.STATE_ACTIVE) + + def onDestroy(self): + """销毁时回调""" + pass + + def isListening(self) -> bool: + return self._listening + + def toString(self) -> str: + return (f"TileService(locked={self._locked}, secure={self._secure}, " + f"listening={self._listening})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "TileService", + "Tile", +] \ No newline at end of file diff --git a/android_pyapi/android/service_vr.py b/android_pyapi/android/service_vr.py new file mode 100644 index 0000000..23c6e1e --- /dev/null +++ b/android_pyapi/android/service_vr.py @@ -0,0 +1,62 @@ +""" +Android Service VR 组件 — VrListenerService 等 +方法签名完全对齐 Android SDK (android.service.vr 包) +""" + +from typing import Any, Callable, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# VrListenerService +# ═══════════════════════════════════════════════════════════════ + +class VrListenerService(Stub): + """VrListenerService — VR 监听服务 (已弃用, 仅存根)""" + + SERVICE_INTERFACE = "android.service.vr.VrListenerService" + + def __init__(self): + super().__init__() + self._current_vr_activity: Optional[str] = None + self._is_listening: bool = False + + def onCurrentVrActivityChanged(self, component: Any): + """当前 VR Activity 变化时回调""" + if component: + self._current_vr_activity = str(component) + + def getCurrentVrActivity(self) -> Optional[str]: + """获取当前 VR Activity""" + return self._current_vr_activity + + def onConnected(self): + """服务连接回调""" + self._is_listening = True + + def onDisconnected(self): + """服务断开回调""" + self._is_listening = False + + def isListening(self) -> bool: + return self._is_listening + + def toString(self) -> str: + return (f"VrListenerService(" + f"activity={self._current_vr_activity})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "VrListenerService", +] \ No newline at end of file diff --git a/android_pyapi/android/service_wallpaper.py b/android_pyapi/android/service_wallpaper.py new file mode 100644 index 0000000..4908029 --- /dev/null +++ b/android_pyapi/android/service_wallpaper.py @@ -0,0 +1,264 @@ +""" +Android Service Wallpaper 组件 — WallpaperService, Engine, SurfaceHolder 等 +方法签名完全对齐 Android SDK (android.service.wallpaper 包) +""" + +import time +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# SurfaceHolder +# ═══════════════════════════════════════════════════════════════ + +class SurfaceHolder(Stub): + """SurfaceHolder — 表面持有者 (桌面存根)""" + + # 表面类型 + SURFACE_TYPE_NORMAL = 0 + SURFACE_TYPE_HARDWARE = 1 + SURFACE_TYPE_GPU = 2 + SURFACE_TYPE_PUSH_BUFFERS = 3 + + def __init__(self): + super().__init__() + self._surface: Optional[Any] = None + self._surface_frame: Optional[Any] = None + self._surface_type: int = SurfaceHolder.SURFACE_TYPE_NORMAL + self._fixed_size: Optional[Tuple[int, int]] = None + self._format: int = 0 # PixelFormat.UNKNOWN + self._callbacks: List[Any] = [] + + def getSurface(self) -> Any: + """获取 Surface""" + return self._surface + + def getSurfaceFrame(self) -> Any: + return self._surface_frame + + def isCreating(self) -> bool: + return False + + def setType(self, surface_type: int): + self._surface_type = surface_type + + def getType(self) -> int: + return self._surface_type + + def setFormat(self, format: int): + self._format = format + + def getFormat(self) -> int: + return self._format + + def setFixedSize(self, width: int, height: int): + self._fixed_size = (width, height) + + def setSizeFromLayout(self): + pass + + def addCallback(self, callback: Any): + if callback not in self._callbacks: + self._callbacks.append(callback) + + def removeCallback(self, callback: Any): + if callback in self._callbacks: + self._callbacks.remove(callback) + + def notifySurfaceCreated(self): + for cb in self._callbacks: + if hasattr(cb, 'surfaceCreated'): + cb.surfaceCreated(self) + + def notifySurfaceChanged(self, format: int, width: int, height: int): + for cb in self._callbacks: + if hasattr(cb, 'surfaceChanged'): + cb.surfaceChanged(self, format, width, height) + + def notifySurfaceDestroyed(self): + for cb in self._callbacks: + if hasattr(cb, 'surfaceDestroyed'): + cb.surfaceDestroyed(self) + + def lockCanvas(self) -> Any: + """锁定画布""" + return None + + def lockCanvas(self, dirty: Any) -> Any: + """锁定画布 (指定脏区域)""" + return None + + def unlockCanvasAndPost(self, canvas: Any): + """解锁并提交画布""" + pass + + def toString(self) -> str: + size_str = (f"{self._fixed_size[0]}x{self._fixed_size[1]}" + if self._fixed_size else "dynamic") + return f"SurfaceHolder(size={size_str}, callbacks={len(self._callbacks)})" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# WallpaperService +# ═══════════════════════════════════════════════════════════════ + +class WallpaperService(Stub): + """WallpaperService — 动态壁纸服务基类""" + + # 服务接口 + SERVICE_INTERFACE = "android.service.wallpaper.WallpaperService" + ACTION_LIVE_WALLPAPER_CHOOSER = ( + "android.service.wallpaper.LIVE_WALLPAPER_CHOOSER" + ) + + # 元数据 + META_DATA_WALLPAPER = "android.service.wallpaper" + + def __init__(self): + super().__init__() + + def onCreate(self): + """创建壁纸服务""" + pass + + def onCreateEngine(self) -> "WallpaperService.Engine": + """创建壁纸引擎""" + return WallpaperService.Engine(self) + + def onDestroy(self): + """销毁壁纸服务""" + pass + + def toString(self) -> str: + return f"WallpaperService@{hex(id(self))}" + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + # ── Engine ── + + class Engine(Stub): + """Engine — 壁纸引擎""" + + def __init__(self, service: "WallpaperService" = None): + super().__init__() + self._service = service + self._is_visible: bool = True + self._is_created: bool = False + self._surface_holder = SurfaceHolder() + self._surface_holder.addCallback(self) + self._offset: Tuple[float, float, float, float, float] = (0, 0, 0, 0, 0) + self._x_ppw: float = 1.0 + self._y_ppw: float = 1.0 + self._desired_width: int = 0 + self._desired_height: int = 0 + self._touch_events_enabled: bool = False + self._offset_notifications_enabled: bool = False + self._draw_count: int = 0 + + def onCreate(self, surface_holder: SurfaceHolder): + """创建引擎""" + self._is_created = True + + def onSurfaceCreated(self, holder: SurfaceHolder): + """表面创建时回调""" + pass + + def onSurfaceDestroyed(self, holder: SurfaceHolder): + """表面销毁时回调""" + pass + + def onSurfaceChanged(self, holder: SurfaceHolder, + format: int, width: int, height: int): + """表面尺寸变化时回调""" + pass + + def onDraw(self, canvas: Any): + """绘制壁纸""" + self._draw_count += 1 + + def onOffsetsChanged(self, x_offset: float, y_offset: float, + x_step: float, y_step: float, + x_pixel_offset: int, y_pixel_offset: int): + """偏移量变化回调""" + self._offset = (x_offset, y_offset, x_step, y_step) + + def onTouchEvent(self, motion_event: Any): + """触摸事件回调""" + pass + + def onVisibilityChanged(self, visible: bool): + """可见性变化回调""" + self._is_visible = visible + + def setOffsetNotifications(self, x_ppw: float, y_ppw: float): + """设置偏移通知""" + self._x_ppw = x_ppw + self._y_ppw = y_ppw + self._offset_notifications_enabled = True + + def getSurfaceHolder(self) -> SurfaceHolder: + """获取表面持有者""" + return self._surface_holder + + def getDesiredMinimumWidth(self) -> int: + return self._desired_width + + def getDesiredMinimumHeight(self) -> int: + return self._desired_height + + def isVisible(self) -> bool: + return self._is_visible + + def isPreview(self) -> bool: + return False + + def setTouchEventsEnabled(self, enabled: bool): + self._touch_events_enabled = enabled + + def setCreateSurfaceFromSurfaceControlResult(self, result: Any): + pass + + def surfaceCreated(self, holder: SurfaceHolder): + """SurfaceHolder.Callback — 表面创建""" + self.onSurfaceCreated(holder) + + def surfaceChanged(self, holder: SurfaceHolder, + format: int, width: int, height: int): + """SurfaceHolder.Callback — 表面变化""" + self.onSurfaceChanged(holder, format, width, height) + + def surfaceDestroyed(self, holder: SurfaceHolder): + """SurfaceHolder.Callback — 表面销毁""" + self.onSurfaceDestroyed(holder) + + def toString(self) -> str: + return (f"Engine(visible={self._is_visible}, " + f"draws={self._draw_count})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "WallpaperService", + "SurfaceHolder", +] \ No newline at end of file diff --git a/android_pyapi/android/sip.py b/android_pyapi/android/sip.py new file mode 100644 index 0000000..275d4f0 --- /dev/null +++ b/android_pyapi/android/sip.py @@ -0,0 +1,519 @@ +""" +Android SIP API — SipManager, SipProfile, SipSession, SipAudioCall +(已废弃但存在于 SDK 中) +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "SipManager", + "SipProfile", + "SipSession", + "SipAudioCall", +] + + +# ═══════════════════════════════════════════════════════════════ +# SipManager +# ═══════════════════════════════════════════════════════════════ + +class SipManager(Stub): + """SipManager — SIP 会话管理器 (桌面存根, 已废弃)""" + + EXTRA_CALL_ID = "android.sip.extra.CALL_ID" + EXTRA_OFFER_SD = "android.sip.extra.OFFER_SD" + EXTRA_PEER_PROFILE = "android.sip.extra.PEER_PROFILE" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._opened = False + self._registered = False + self._session: Optional["SipSession"] = None + self._listener: Optional[Callable] = None + + @staticmethod + def isSipWifiOnly(context: Any) -> bool: + """检查 SIP 是否仅限 WiFi""" + return False + + @staticmethod + def isVoipSupported(context: Any) -> bool: + """检查是否支持 VoIP""" + return True + + @staticmethod + def isApiSupported(context: Any) -> bool: + """检查 SIP API 是否支持""" + return True + + @staticmethod + def newInstance(context: Any) -> "SipManager": + """创建新实例""" + return SipManager(context) + + def open(self, localProfile: "SipProfile") -> "SipSession": + """打开 SIP 会话""" + self._opened = True + self._session = SipSession(localProfile) + return self._session + + def setRegistrationListener( + self, localProfileUri: str, listener: "SipSession.Listener" + ): + """设置注册监听器""" + if self._session: + self._session.setListener(listener) + + def close(self, localProfileUri: str): + """关闭 SIP 会话""" + self._opened = False + self._registered = False + self._session = None + + def getSessionFor(self, intent: Any) -> Optional["SipSession"]: + """获取 Intent 对应的 SIP 会话""" + return self._session + + def register( + self, localProfile: "SipProfile", expiryTime: int, listener: "SipSession.Listener" + ): + """注册 SIP""" + raise AndroidOnlyError("SipManager.register() 仅在 Android 设备上可用") + + def unregister( + self, localProfile: "SipProfile", listener: "SipSession.Listener" + ): + """注销 SIP""" + raise AndroidOnlyError("SipManager.unregister() 仅在 Android 设备上可用") + + def isOpened(self, localProfileUri: str) -> bool: + """检查是否已打开""" + return self._opened + + def isRegistered(self, localProfileUri: str) -> bool: + """检查是否已注册""" + return self._registered + + def createSipSessionIfNeeded(self, localProfile: "SipProfile") -> "SipSession": + """如有必要则创建 SIP 会话""" + if self._session is None: + self._session = SipSession(localProfile) + return self._session + + def createSipProfile(self, intent: Any) -> Optional["SipProfile"]: + """从 Intent 创建 SIP 配置文件""" + return None + + def toString(self) -> str: + return f"SipManager(opened={self._opened}, registered={self._registered})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# SipProfile +# ═══════════════════════════════════════════════════════════════ + +class SipProfile(Stub): + """SipProfile — SIP 配置文件""" + + # --- Protocol constants --- + PROTOCOL_UDP = "UDP" + PROTOCOL_TCP = "TCP" + PROTOCOL_TLS = "TLS" + + class Builder(Stub): + """SipProfile.Builder — SIP 配置文件构建器""" + + def __init__(self, uri: str = ""): + super().__init__() + self._profile = SipProfile() + self._profile._uri = uri + + def setProfileName(self, name: str) -> "SipProfile.Builder": + self._profile._profile_name = name + return self + + def setUserName(self, userName: str) -> "SipProfile.Builder": + self._profile._user_name = userName + return self + + def setDomain(self, domain: str) -> "SipProfile.Builder": + self._profile._domain = domain + return self + + def setPassword(self, password: str) -> "SipProfile.Builder": + self._profile._password = password + return self + + def setPort(self, port: int) -> "SipProfile.Builder": + self._profile._port = port + return self + + def setProtocol(self, protocol: str) -> "SipProfile.Builder": + self._profile._protocol = protocol + return self + + def setSendKeepAlive(self, enabled: bool) -> "SipProfile.Builder": + self._profile._send_keep_alive = enabled + return self + + def setAutoRegistration(self, enabled: bool) -> "SipProfile.Builder": + self._profile._auto_registration = enabled + return self + + def setDisplayName(self, displayName: str) -> "SipProfile.Builder": + self._profile._display_name = displayName + return self + + def setAuthUserName(self, authUserName: str) -> "SipProfile.Builder": + self._profile._auth_user_name = authUserName + return self + + def build(self) -> "SipProfile": + return self._profile + + def toString(self) -> str: + return f"SipProfile.Builder(name={self._profile._profile_name})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def __init__(self): + super().__init__() + self._uri = "" + self._profile_name = "" + self._user_name = "" + self._domain = "" + self._display_name = "" + self._password = "" + self._protocol = self.PROTOCOL_UDP + self._port = 5060 + self._send_keep_alive = False + self._auto_registration = True + self._auth_user_name = "" + + def getProfileName(self) -> str: + return self._profile_name + + def getUserName(self) -> str: + return self._user_name + + def getDomain(self) -> str: + return self._domain + + def getDisplayName(self) -> str: + return self._display_name + + def getPassword(self) -> str: + return self._password + + def getProtocol(self) -> str: + return self._protocol + + def getPort(self) -> int: + return self._port + + def getSendKeepAlive(self) -> bool: + return self._send_keep_alive + + def isAutoRegistration(self) -> bool: + return self._auto_registration + + def getUri(self) -> str: + return self._uri + + def toString(self) -> str: + return (f"SipProfile(name={self._profile_name}, " + f"user={self._user_name}@{self._domain})") + + def equals(self, other) -> bool: + if not isinstance(other, SipProfile): + return False + return self._uri == other._uri + + def hashCode(self) -> int: + return hash(self._uri) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# SipSession +# ═══════════════════════════════════════════════════════════════ + +class SipSession(Stub): + """SipSession — SIP 会话""" + + # --- State constants --- + STATE_READY_TO_CALL = 0 + STATE_INCOMING_CALL = 1 + STATE_INCOMING_CALL_ANSWERING = 2 + STATE_IN_CALL = 3 + STATE_OUTGOING_CALL = 4 + STATE_OUTGOING_CALL_RINGING = 5 + STATE_OUTGOING_CALL_CANCELING = 6 + STATE_PINGING = 7 + STATE_DEREGISTERING = 8 + STATE_CLOSED = 9 + + def __init__(self, profile: "SipProfile" = None): + super().__init__() + self._profile = profile + self._listener: Optional["SipSession.Listener"] = None + self._state = self.STATE_CLOSED + self._call_id = "" + self._peer_profile: Optional[SipProfile] = None + self._in_call = False + + def setListener(self, listener: "SipSession.Listener"): + """设置会话监听器""" + self._listener = listener + + def register(self, duration: int): + """注册 SIP""" + raise AndroidOnlyError("SipSession.register() 仅在 Android 设备上可用") + + def unregister(self): + """注销 SIP""" + raise AndroidOnlyError("SipSession.unregister() 仅在 Android 设备上可用") + + def makeAudioCall( + self, localProfile: "SipProfile", peerProfile: "SipProfile" + ): + """发起音频呼叫""" + raise AndroidOnlyError("SipSession.makeAudioCall() 仅在 Android 设备上可用") + + def answerCall(self, callId: str, sessionDescription: int): + """接听呼叫""" + raise AndroidOnlyError("SipSession.answerCall() 仅在 Android 设备上可用") + + def endCall(self): + """结束呼叫""" + raise AndroidOnlyError("SipSession.endCall() 仅在 Android 设备上可用") + + def changeCall(self, callId: str, sessionDescription: int): + """更改呼叫""" + raise AndroidOnlyError("SipSession.changeCall() 仅在 Android 设备上可用") + + def getCallId(self) -> str: + return self._call_id + + def getPeerProfile(self) -> Optional["SipProfile"]: + return self._peer_profile + + def getState(self) -> int: + return self._state + + def isInCall(self) -> bool: + return self._in_call + + def getLocalProfile(self) -> Optional["SipProfile"]: + return self._profile + + # ═══════════════════════════════════════════════════════════ + # Listener + # ═══════════════════════════════════════════════════════════ + + class Listener(Stub): + """SipSession.Listener — SIP 会话监听器""" + + def __init__(self): + super().__init__() + + def onCalling(self, session: "SipSession"): + """呼叫中回调""" + pass + + def onRinging(self, session: "SipSession", caller: "SipProfile"): + """响铃回调""" + pass + + def onCallEstablished(self, session: "SipSession", sessionDescription: str): + """呼叫建立回调""" + pass + + def onCallEnded(self, session: "SipSession"): + """呼叫结束回调""" + pass + + def onCallBusy(self, session: "SipSession"): + """呼叫忙碌回调""" + pass + + def onError(self, session: "SipSession", errorCode: int, errorMessage: str): + """错误回调""" + pass + + def onCallChangeFailed(self, session: "SipSession", errorCode: int, errorMessage: str): + """呼叫变更失败回调""" + pass + + def onRegistrationDone(self, session: "SipSession", duration: int): + """注册成功回调""" + pass + + def onRegistrationFailed( + self, session: "SipSession", errorCode: int, errorMessage: str + ): + """注册失败回调""" + pass + + def onRegistrationTimeout(self, session: "SipSession"): + """注册超时回调""" + pass + + def toString(self) -> str: + return f"SipSession.Listener@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def toString(self) -> str: + return f"SipSession(state={self._state})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# SipAudioCall +# ═══════════════════════════════════════════════════════════════ + +class SipAudioCall(Stub): + """SipAudioCall — SIP 音频呼叫""" + + def __init__(self, context: Any = None, localProfile: "SipProfile" = None): + super().__init__() + self._context = context + self._local_profile = localProfile + self._listener: Optional["SipAudioCall.Listener"] = None + self._muted = False + self._holding = False + self._in_call = False + + def setListener(self, listener: "SipAudioCall.Listener"): + """设置音频呼叫监听器""" + self._listener = listener + + def startAudio(self): + """开始音频""" + raise AndroidOnlyError("SipAudioCall.startAudio() 仅在 Android 设备上可用") + + def stopAudio(self): + """停止音频""" + raise AndroidOnlyError("SipAudioCall.stopAudio() 仅在 Android 设备上可用") + + def close(self): + """关闭音频呼叫""" + self._in_call = False + self._holding = False + + def isMuted(self) -> bool: + return self._muted + + def toggleMute(self): + """切换静音""" + self._muted = not self._muted + + def hold(self, hold: int): + """保持呼叫""" + self._holding = (hold != 0) + + def continueCall(self, sessionDescription: int): + """继续呼叫""" + raise AndroidOnlyError("SipAudioCall.continueCall() 仅在 Android 设备上可用") + + def makeCall( + self, localProfile: "SipProfile", peerProfile: "SipProfile", timeout: int + ): + """发起呼叫""" + raise AndroidOnlyError("SipAudioCall.makeCall() 仅在 Android 设备上可用") + + def answerCall(self, sessionDescription: int): + """接听呼叫""" + raise AndroidOnlyError("SipAudioCall.answerCall() 仅在 Android 设备上可用") + + def sendDtmf(self, code: int): + """发送 DTMF""" + pass + + # ═══════════════════════════════════════════════════════════ + # Listener + # ═══════════════════════════════════════════════════════════ + + class Listener(Stub): + """SipAudioCall.Listener — 音频呼叫监听器""" + + def __init__(self): + super().__init__() + + def onReadyToCall(self, call: "SipAudioCall"): + """准备呼叫回调""" + pass + + def onCallEstablished(self, call: "SipAudioCall", sessionDescription: str): + """呼叫建立回调""" + pass + + def onCallEnded(self, call: "SipAudioCall"): + """呼叫结束回调""" + pass + + def onCallHeld(self, call: "SipAudioCall"): + """呼叫保持回调""" + pass + + def onCallChanged(self, call: "SipAudioCall"): + """呼叫变更回调""" + pass + + def onError(self, call: "SipAudioCall", errorCode: int, errorMessage: str): + """错误回调""" + pass + + def onRingingBack(self, call: "SipAudioCall"): + """回铃回调""" + pass + + def onRinging(self, call: "SipAudioCall", caller: "SipProfile"): + """响铃回调""" + pass + + def toString(self) -> str: + return f"SipAudioCall.Listener@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def toString(self) -> str: + return f"SipAudioCall(inCall={self._in_call})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/speech.py b/android_pyapi/android/speech.py new file mode 100644 index 0000000..1429ae5 --- /dev/null +++ b/android_pyapi/android/speech.py @@ -0,0 +1,370 @@ +""" +Android Speech API — SpeechRecognizer, RecognitionListener, RecognitionService, RecognizerIntent +方法签名完全对齐 Android SDK +""" + +import sys +import threading +import time +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "SpeechRecognizer", + "RecognitionListener", + "RecognitionService", + "RecognizerIntent", + "RecognizerResultsIntent", +] + + +# ═══════════════════════════════════════════════════════════════ +# SpeechRecognizer +# ═══════════════════════════════════════════════════════════════ + +class SpeechRecognizer(Stub): + """SpeechRecognizer — 语音识别器 (桌面麦克风模拟)""" + + RESULTS_RECOGNITION = "results_recognition" + CONFIDENCE_SCORES = "confidence_scores" + + ERROR_NETWORK_TIMEOUT = 1 + ERROR_NETWORK = 2 + ERROR_AUDIO = 3 + ERROR_SERVER = 4 + ERROR_CLIENT = 5 + ERROR_SPEECH_TIMEOUT = 6 + ERROR_NO_MATCH = 7 + ERROR_RECOGNIZER_BUSY = 8 + ERROR_INSUFFICIENT_PERMISSIONS = 9 + ERROR_TOO_MANY_REQUESTS = 10 + ERROR_LANGUAGE_NOT_SUPPORTED = 11 + ERROR_LANGUAGE_UNAVAILABLE = 12 + ERROR_SERVER_DISCONNECTED = 13 + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._listener: Optional["RecognitionListener"] = None + self._is_listening = False + self._destroyed = False + self._simulate_timer: Optional[threading.Timer] = None + + @staticmethod + def createSpeechRecognizer(context: Any) -> "SpeechRecognizer": + """创建语音识别器""" + return SpeechRecognizer(context) + + @staticmethod + def isRecognitionAvailable(context: Any) -> bool: + """检查语音识别是否可用""" + try: + import speech_recognition as sr + return True + except ImportError: + try: + import whisper # noqa: F401 + return True + except ImportError: + pass + return False + + def setRecognitionListener(self, listener: "RecognitionListener"): + """设置识别监听器""" + self._listener = listener + + def startListening(self, intent: Any): + """开始语音识别""" + if self._destroyed: + return + + self._is_listening = True + + # 使用简单识别模拟 + if self._listener: + self._listener.onReadyForSpeech(None) + self._listener.onBeginningOfSpeech() + + # 模拟麦克风音量变化 + for i in range(3): + self._listener.onRmsChanged(5.0 + i * 2.0) + time.sleep(0.05) + + self._listener.onBufferReceived(bytearray(1024)) + self._listener.onEndOfSpeech() + + # 模拟识别结果 + results = {self.RESULTS_RECOGNITION: ["hello world"], self.CONFIDENCE_SCORES: [0.95]} + self._listener.onResults(results) + + def stopListening(self): + """停止语音识别""" + self._is_listening = False + if self._simulate_timer: + self._simulate_timer.cancel() + self._simulate_timer = None + + def cancel(self): + """取消语音识别""" + self._is_listening = False + if self._simulate_timer: + self._simulate_timer.cancel() + self._simulate_timer = None + if self._listener: + self._listener.onError(self.ERROR_NO_MATCH) + + def destroy(self): + """销毁语音识别器""" + self._destroyed = True + self._is_listening = False + if self._simulate_timer: + self._simulate_timer.cancel() + self._simulate_timer = None + self._listener = None + + def toString(self) -> str: + return f"SpeechRecognizer(listening={self._is_listening})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# RecognitionListener +# ═══════════════════════════════════════════════════════════════ + +class RecognitionListener(Stub): + """RecognitionListener — 语音识别监听器""" + + def __init__(self): + super().__init__() + + def onReadyForSpeech(self, params: Any): + """准备开始语音识别回调""" + pass + + def onBeginningOfSpeech(self): + """开始说话回调""" + pass + + def onRmsChanged(self, rmsdB: float): + """音量变化回调""" + pass + + def onBufferReceived(self, buffer: bytearray): + """接收音频缓冲区回调""" + pass + + def onEndOfSpeech(self): + """说话结束回调""" + pass + + def onError(self, error: int): + """错误回调""" + pass + + def onResults(self, results: Dict[str, Any]): + """识别结果回调""" + pass + + def onPartialResults(self, partialResults: Dict[str, Any]): + """部分识别结果回调""" + pass + + def onSegmentResults(self, segmentResults: Dict[str, Any]): + """分段结果回调""" + pass + + def onEndOfSegmentedSession(self, sessionId: int): + """分段会话结束回调""" + pass + + def onLanguageDetection(self, language: str): + """语言检测回调""" + pass + + def onEvent(self, eventType: int, params: Any): + """事件回调""" + pass + + def toString(self) -> str: + return f"RecognitionListener@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# RecognitionService +# ═══════════════════════════════════════════════════════════════ + +class RecognitionService(Stub): + """RecognitionService — 语音识别服务""" + + SERVICE_INTERFACE = "android.speech.RecognitionService" + SERVICE_META_DATA = "android.speech" + + def __init__(self): + super().__init__() + self._callback: Optional["RecognitionService.Callback"] = None + + def onStartListening(self, intent: Any, listener: "RecognitionService.Callback"): + """开始监听回调""" + pass + + def onCancel(self, listener: "RecognitionService.Callback"): + """取消回调""" + pass + + def onStopListening(self, listener: "RecognitionService.Callback"): + """停止监听回调""" + pass + + def onDestroy(self): + """销毁回调""" + pass + + def oneShotListening(self, intent: Any, listener: "RecognitionService.Callback"): + """一次性监听""" + if listener: + listener.readyForSpeech(None) + listener.beginningOfSpeech() + listener.endOfSpeech() + results = {SpeechRecognizer.RESULTS_RECOGNITION: ["hello"]} + listener.results(results) + + def getCurrentCallback(self) -> Optional["RecognitionService.Callback"]: + return self._callback + + # ═══════════════════════════════════════════════════════════ + # Callback + # ═══════════════════════════════════════════════════════════ + + class Callback(Stub): + """RecognitionService.Callback — 识别服务回调""" + + def __init__(self): + super().__init__() + + def results(self, results: Dict[str, Any]): + """返回识别结果""" + pass + + def partialResults(self, partialResults: Dict[str, Any]): + """返回部分结果""" + pass + + def error(self, error: int): + """返回错误""" + pass + + def beginningOfSpeech(self): + """开始说话回调""" + pass + + def bufferReceived(self, buffer: bytearray): + """接收音频缓冲区回调""" + pass + + def endOfSpeech(self): + """说话结束回调""" + pass + + def readyForSpeech(self, params: Any): + """准备开始语音识别""" + pass + + def rmsChanged(self, rmsdB: float): + """音量变化回调""" + pass + + def toString(self) -> str: + return f"RecognitionService.Callback@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def toString(self) -> str: + return f"RecognitionService@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# RecognizerIntent +# ═══════════════════════════════════════════════════════════════ + +class RecognizerIntent(Stub): + """RecognizerIntent — 识别器 Intent 常量""" + + ACTION_RECOGNIZE_SPEECH = "android.speech.action.RECOGNIZE_SPEECH" + ACTION_WEB_SEARCH = "android.speech.action.WEB_SEARCH" + ACTION_VOICE_SEARCH_HANDS_FREE = "android.speech.action.VOICE_SEARCH_HANDS_FREE" + ACTION_GET_LANGUAGE_DETAILS = "android.speech.action.GET_LANGUAGE_DETAILS" + + EXTRA_LANGUAGE_MODEL = "android.speech.extra.LANGUAGE_MODEL" + EXTRA_LANGUAGE = "android.speech.extra.LANGUAGE" + EXTRA_MAX_RESULTS = "android.speech.extra.MAX_RESULTS" + EXTRA_PARTIAL_RESULTS = "android.speech.extra.PARTIAL_RESULTS" + EXTRA_CALLING_PACKAGE = "android.speech.extra.CALLING_PACKAGE" + EXTRA_PROMPT = "android.speech.extra.PROMPT" + EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS = "android.speech.extra.SPEECH_INPUT_MINIMUM_LENGTH_MILLIS" + EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS = "android.speech.extra.SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS" + EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS = "android.speech.extra.SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS" + EXTRA_CONFIDENCE_SCORES = "android.speech.extra.CONFIDENCE_SCORES" + EXTRA_RESULTS = "android.speech.extra.RESULTS" + EXTRA_SECURE = "android.speech.extra.SECURE" + EXTRA_SUPPORTED_LANGUAGES = "android.speech.extra.SUPPORTED_LANGUAGES" + EXTRA_LANGUAGE_PREFERENCE = "android.speech.extra.LANGUAGE_PREFERENCE" + EXTRA_ONLY_RETURN_LANGUAGE_PREFERENCE = "android.speech.extra.ONLY_RETURN_LANGUAGE_PREFERENCE" + EXTRA_LANGUAGE_DETAILS = "android.speech.extra.LANGUAGE_DETAILS" + + LANGUAGE_MODEL_FREE_FORM = "free_form" + LANGUAGE_MODEL_WEB_SEARCH = "web_search" + + RESULT_AUDIO_ERROR = 1 + RESULT_CLIENT_ERROR = 2 + RESULT_NETWORK_ERROR = 3 + RESULT_SERVER_ERROR = 4 + RESULT_NO_MATCH = 5 + + DETAIL_META_DATA = "android.speech.DETAIL_META_DATA" + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# RecognizerResultsIntent +# ═══════════════════════════════════════════════════════════════ + +class RecognizerResultsIntent(Stub): + """RecognizerResultsIntent — 识别结果 Intent 常量""" + + ACTION_VOICE_SEARCH_RESULTS = "android.speech.action.VOICE_SEARCH_RESULTS" + EXTRA_VOICE_SEARCH_RESULT_STRINGS = "android.speech.extra.VOICE_SEARCH_RESULT_STRINGS" + EXTRA_VOICE_SEARCH_RESULT_URLS = "android.speech.extra.VOICE_SEARCH_RESULT_URLS" + EXTRA_VOICE_SEARCH_RESULT_HTML = "android.speech.extra.VOICE_SEARCH_RESULT_HTML" + EXTRA_VOICE_SEARCH_RESULT_HTML_BASE_URLS = "android.speech.extra.VOICE_SEARCH_RESULT_HTML_BASE_URLS" + URI_SCHEME_INLINE = "inline" + + def __init__(self): + super().__init__() \ No newline at end of file diff --git a/android_pyapi/android/system.py b/android_pyapi/android/system.py new file mode 100644 index 0000000..255c276 --- /dev/null +++ b/android_pyapi/android/system.py @@ -0,0 +1,592 @@ +""" +Android System 组件 — Os, StructStat, StructUtsname, OsConstants 等 +方法签名完全对齐 Android SDK (android.system 包) +""" + +import os as _os +import stat as _stat +import platform +import errno +from typing import Any, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# OsConstants +# ═══════════════════════════════════════════════════════════════ + +class OsConstants(Stub): + """OsConstants — 系统调用常量,映射自 os/stat 模块""" + + # 文件访问常量 + R_OK = _os.R_OK + W_OK = _os.W_OK + X_OK = _os.X_OK + F_OK = _os.F_OK + + # 文件权限常量 + S_IRWXU = _stat.S_IRWXU + S_IRUSR = _stat.S_IRUSR + S_IWUSR = _stat.S_IWUSR + S_IXUSR = _stat.S_IXUSR + S_IRWXG = _stat.S_IRWXG + S_IRGRP = _stat.S_IRGRP + S_IWGRP = _stat.S_IWGRP + S_IXGRP = _stat.S_IXGRP + S_IRWXO = _stat.S_IRWXO + S_IROTH = _stat.S_IROTH + S_IWOTH = _stat.S_IWOTH + S_IXOTH = _stat.S_IXOTH + S_ISUID = _stat.S_ISUID + S_ISGID = _stat.S_ISGID + S_ISVTX = _stat.S_ISVTX + + # 文件类型常量 + S_IFMT = _stat.S_IFMT + S_IFSOCK = _stat.S_IFSOCK + S_IFLNK = _stat.S_IFLNK + S_IFREG = _stat.S_IFREG + S_IFBLK = _stat.S_IFBLK + S_IFDIR = _stat.S_IFDIR + S_IFCHR = _stat.S_IFCHR + S_IFIFO = _stat.S_IFIFO + + # 标准文件描述符 + STDIN_FILENO = 0 + STDOUT_FILENO = 1 + STDERR_FILENO = 2 + + # 信号常量 + SIGHUP = 1 + SIGINT = 2 + SIGQUIT = 3 + SIGILL = 4 + SIGTRAP = 5 + SIGABRT = 6 + SIGBUS = 7 + SIGFPE = 8 + SIGKILL = 9 + SIGUSR1 = 10 + SIGSEGV = 11 + SIGUSR2 = 12 + SIGPIPE = 13 + SIGALRM = 14 + SIGTERM = 15 + SIGCHLD = 17 + SIGCONT = 18 + SIGSTOP = 19 + SIGTSTP = 20 + SIGTTIN = 21 + SIGTTOU = 22 + + # errno 常量 + EPERM = errno.EPERM + ENOENT = errno.ENOENT + ESRCH = errno.ESRCH + EINTR = errno.EINTR + EIO = errno.EIO + ENXIO = errno.ENXIO + E2BIG = errno.E2BIG + ENOEXEC = errno.ENOEXEC + EBADF = errno.EBADF + ECHILD = errno.ECHILD + EAGAIN = errno.EAGAIN + ENOMEM = errno.ENOMEM + EACCES = errno.EACCES + EFAULT = errno.EFAULT + ENOTBLK = errno.ENOTBLK + EBUSY = errno.EBUSY + EEXIST = errno.EEXIST + EXDEV = errno.EXDEV + ENODEV = errno.ENODEV + ENOTDIR = errno.ENOTDIR + EISDIR = errno.EISDIR + EINVAL = errno.EINVAL + ENFILE = errno.ENFILE + EMFILE = errno.EMFILE + ENOTTY = errno.ENOTTY + ETXTBSY = errno.ETXTBSY + EFBIG = errno.EFBIG + ENOSPC = errno.ENOSPC + ESPIPE = errno.ESPIPE + EROFS = errno.EROFS + EMLINK = errno.EMLINK + EPIPE = errno.EPIPE + EDOM = errno.EDOM + ERANGE = errno.ERANGE + EDEADLK = errno.EDEADLK + ENAMETOOLONG = errno.ENAMETOOLONG + ENOLCK = errno.ENOLCK + ENOSYS = errno.ENOSYS + ENOTEMPTY = errno.ENOTEMPTY + ELOOP = errno.ELOOP + ENOMSG = errno.ENOMSG + EIDRM = errno.EIDRM + ENOSTR = errno.ENOSTR + ENODATA = errno.ENODATA + ETIME = errno.ETIME + ENOSR = errno.ENOSR + EREMOTE = errno.EREMOTE + ENOLINK = errno.ENOLINK + EPROTO = errno.EPROTO + EMULTIHOP = errno.EMULTIHOP + EBADMSG = errno.EBADMSG + EOVERFLOW = errno.EOVERFLOW + EILSEQ = errno.EILSEQ + ENOTSOCK = errno.ENOTSOCK + EDESTADDRREQ = errno.EDESTADDRREQ + EMSGSIZE = errno.EMSGSIZE + EPROTOTYPE = errno.EPROTOTYPE + ENOPROTOOPT = errno.ENOPROTOOPT + EPROTONOSUPPORT = errno.EPROTONOSUPPORT + ESOCKTNOSUPPORT = errno.ESOCKTNOSUPPORT + EOPNOTSUPP = errno.EOPNOTSUPP + EPFNOSUPPORT = errno.EPFNOSUPPORT + EAFNOSUPPORT = errno.EAFNOSUPPORT + EADDRINUSE = errno.EADDRINUSE + EADDRNOTAVAIL = errno.EADDRNOTAVAIL + ENETDOWN = errno.ENETDOWN + ENETUNREACH = errno.ENETUNREACH + ENETRESET = errno.ENETRESET + ECONNABORTED = errno.ECONNABORTED + ECONNRESET = errno.ECONNRESET + ENOBUFS = errno.ENOBUFS + EISCONN = errno.EISCONN + ENOTCONN = errno.ENOTCONN + ESHUTDOWN = errno.ESHUTDOWN + ETOOMANYREFS = errno.ETOOMANYREFS + ETIMEDOUT = errno.ETIMEDOUT + ECONNREFUSED = errno.ECONNREFUSED + EHOSTDOWN = errno.EHOSTDOWN + EHOSTUNREACH = errno.EHOSTUNREACH + EALREADY = errno.EALREADY + EINPROGRESS = errno.EINPROGRESS + ESTALE = errno.ESTALE + EUCLEAN = errno.EUCLEAN + ENOTNAM = errno.ENOTNAM + ENAVAIL = errno.ENAVAIL + EISNAM = errno.EISNAM + EREMOTEIO = errno.EREMOTEIO + EDQUOT = errno.EDQUOT + ENOMEDIUM = errno.ENOMEDIUM + EMEDIUMTYPE = errno.EMEDIUMTYPE + ECANCELED = errno.ECANCELED + ENOKEY = errno.ENOKEY + EKEYEXPIRED = errno.EKEYEXPIRED + EKEYREVOKED = errno.EKEYREVOKED + EKEYREJECTED = errno.EKEYREJECTED + EOWNERDEAD = errno.EOWNERDEAD + ENOTRECOVERABLE = errno.ENOTRECOVERABLE + ERFKILL = errno.ERFKILL + EHWPOISON = getattr(errno, 'EHWPOISON', 1001) + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# StructStat +# ═══════════════════════════════════════════════════════════════ + +class StructStat(Stub): + """StructStat — stat() 返回的文件状态结构体""" + + def __init__(self, st_mode: int = 0, st_ino: int = 0, st_dev: int = 0, + st_nlink: int = 0, st_uid: int = 0, st_gid: int = 0, + st_size: int = 0, st_atime: int = 0, st_mtime: int = 0, + st_ctime: int = 0, st_blksize: int = 4096, + st_blocks: int = 0, st_rdev: int = 0): + super().__init__() + self.st_mode = st_mode + self.st_ino = st_ino + self.st_dev = st_dev + self.st_nlink = st_nlink + self.st_uid = st_uid + self.st_gid = st_gid + self.st_size = st_size + self.st_atime = st_atime + self.st_mtime = st_mtime + self.st_ctime = st_ctime + self.st_blksize = st_blksize + self.st_blocks = st_blocks + self.st_rdev = st_rdev + self.st_atime_ns = st_atime * 1_000_000_000 + self.st_mtime_ns = st_mtime * 1_000_000_000 + self.st_ctime_ns = st_ctime * 1_000_000_000 + + @staticmethod + def from_os_stat(st: _os.stat_result) -> "StructStat": + """从 Python os.stat_result 创建 StructStat""" + return StructStat( + st_mode=st.st_mode, + st_ino=st.st_ino, + st_dev=st.st_dev, + st_nlink=st.st_nlink, + st_uid=st.st_uid, + st_gid=st.st_gid, + st_size=st.st_size, + st_atime=int(st.st_atime), + st_mtime=int(st.st_mtime), + st_ctime=int(st.st_ctime), + st_blksize=st.st_blksize, + st_blocks=st.st_blocks, + st_rdev=st.st_rdev, + ) + + def toString(self) -> str: + return (f"StructStat(st_mode={oct(self.st_mode)}, st_size={self.st_size}, " + f"st_mtime={self.st_mtime}, st_uid={self.st_uid}, st_gid={self.st_gid})") + + def hashCode(self) -> int: + return hash((self.st_dev, self.st_ino, self.st_mode)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, StructStat): + return False + return (self.st_dev == other.st_dev and self.st_ino == other.st_ino + and self.st_mode == other.st_mode and self.st_size == other.st_size) + + +# ═══════════════════════════════════════════════════════════════ +# StructUtsname +# ═══════════════════════════════════════════════════════════════ + +class StructUtsname(Stub): + """StructUtsname — uname() 返回的系统信息结构体""" + + def __init__(self, sysname: str = "", nodename: str = "", + release: str = "", version: str = "", machine: str = ""): + super().__init__() + self.sysname = sysname or platform.system() + self.nodename = nodename or platform.node() + self.release = release or platform.release() + self.version = version or platform.version() + self.machine = machine or platform.machine() + + @staticmethod + def from_platform() -> "StructUtsname": + """从 Python platform 模块创建 StructUtsname""" + uname = platform.uname() + return StructUtsname( + sysname=uname.system, + nodename=uname.node, + release=uname.release, + version=uname.version, + machine=uname.machine, + ) + + def toString(self) -> str: + return (f"StructUtsname(sysname={self.sysname}, nodename={self.nodename}, " + f"release={self.release}, version={self.version}, machine={self.machine})") + + def hashCode(self) -> int: + return hash((self.sysname, self.nodename, self.release)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, StructUtsname): + return False + return (self.sysname == other.sysname and self.nodename == other.nodename + and self.release == other.release) + + +# ═══════════════════════════════════════════════════════════════ +# Os +# ═══════════════════════════════════════════════════════════════ + +class Os(Stub): + """Os — 系统调用包装器,代理到 Python os 模块""" + + def __init__(self): + super().__init__() + + # ── 文件操作 ── + + @staticmethod + def stat(path: str) -> StructStat: + """获取文件状态""" + st = _os.stat(path) + return StructStat.from_os_stat(st) + + @staticmethod + def lstat(path: str) -> StructStat: + """获取符号链接自身状态""" + st = _os.lstat(path) + return StructStat.from_os_stat(st) + + @staticmethod + def chmod(path: str, mode: int): + """修改文件权限""" + _os.chmod(path, mode) + + @staticmethod + def chown(path: str, uid: int, gid: int): + """修改文件所有者""" + _os.chown(path, uid, gid) + + @staticmethod + def link(oldpath: str, newpath: str): + """创建硬链接""" + _os.link(oldpath, newpath) + + @staticmethod + def symlink(target: str, linkpath: str): + """创建符号链接""" + _os.symlink(target, linkpath) + + @staticmethod + def readlink(path: str) -> str: + """读取符号链接目标""" + return _os.readlink(path) + + @staticmethod + def unlink(pathname: str): + """删除文件(解除链接)""" + _os.unlink(pathname) + + @staticmethod + def remove(path: str): + """删除文件""" + _os.remove(path) + + @staticmethod + def rename(oldpath: str, newpath: str): + """重命名文件""" + _os.rename(oldpath, newpath) + + @staticmethod + def mkdir(path: str, mode: int = 0o777): + """创建目录""" + _os.mkdir(path, mode) + + @staticmethod + def rmdir(path: str): + """删除空目录""" + _os.rmdir(path) + + @staticmethod + def listdir(path: str) -> List[str]: + """列出目录内容""" + return _os.listdir(path) + + @staticmethod + def truncate(path: str, length: int): + """截断文件到指定长度""" + with open(path, 'ab') as f: + f.truncate(length) + + @staticmethod + def access(path: str, mode: int) -> bool: + """检查文件访问权限""" + return _os.access(path, mode) + + # ── 文件描述符操作 ── + + @staticmethod + def dup(oldfd: int) -> int: + """复制文件描述符""" + return _os.dup(oldfd) + + @staticmethod + def dup2(oldfd: int, newfd: int): + """复制文件描述符到指定值""" + _os.dup2(oldfd, newfd) + + @staticmethod + def close(fd: int): + """关闭文件描述符""" + _os.close(fd) + + @staticmethod + def read(fd: int, count: int) -> bytes: + """从文件描述符读取""" + return _os.read(fd, count) + + @staticmethod + def write(fd: int, data: bytes) -> int: + """写入文件描述符""" + return _os.write(fd, data) + + @staticmethod + def lseek(fd: int, offset: int, whence: int) -> int: + """移动文件读写位置""" + return _os.lseek(fd, offset, whence) + + @staticmethod + def fsync(fd: int): + """同步文件描述符""" + _os.fsync(fd) + + @staticmethod + def fstat(fd: int) -> StructStat: + """获取文件描述符对应文件状态""" + st = _os.fstat(fd) + return StructStat.from_os_stat(st) + + @staticmethod + def isatty(fd: int) -> bool: + """判断文件描述符是否为终端""" + return _os.isatty(fd) + + # ── 进程相关 ── + + @staticmethod + def getpid() -> int: + """获取当前进程 ID""" + return _os.getpid() + + @staticmethod + def getppid() -> int: + """获取父进程 ID""" + return _os.getppid() + + @staticmethod + def getuid() -> int: + """获取当前用户 ID""" + return _os.getuid() + + @staticmethod + def getgid() -> int: + """获取当前组 ID""" + return _os.getgid() + + @staticmethod + def geteuid() -> int: + """获取有效用户 ID""" + return _os.geteuid() + + @staticmethod + def getegid() -> int: + """获取有效组 ID""" + return _os.getegid() + + @staticmethod + def setuid(uid: int): + """设置用户 ID""" + _os.setuid(uid) + + @staticmethod + def setgid(gid: int): + """设置组 ID""" + _os.setgid(gid) + + @staticmethod + def setsid() -> int: + """创建新会话""" + return _os.setsid() + + @staticmethod + def umask(mask: int) -> int: + """设置文件创建掩码""" + return _os.umask(mask) + + @staticmethod + def kill(pid: int, sig: int): + """发送信号给进程""" + _os.kill(pid, sig) + + @staticmethod + def getenv(name: str, default: Optional[str] = None) -> Optional[str]: + """获取环境变量""" + return _os.environ.get(name, default) + + @staticmethod + def setenv(name: str, value: str, overwrite: bool = True): + """设置环境变量""" + if overwrite or name not in _os.environ: + _os.environ[name] = value + + @staticmethod + def unsetenv(name: str): + """删除环境变量""" + _os.environ.pop(name, None) + + @staticmethod + def environ() -> Dict[str, str]: + """获取环境变量字典""" + return dict(_os.environ) + + # ── 系统信息 ── + + @staticmethod + def uname() -> StructUtsname: + """获取系统信息""" + return StructUtsname.from_platform() + + @staticmethod + def sysconf(name: str) -> int: + """获取系统配置值""" + conf_names = { + "SC_CLK_TCK": 100, + "SC_PAGE_SIZE": 4096, + "SC_NPROCESSORS_CONF": _os.cpu_count() or 1, + "SC_NPROCESSORS_ONLN": _os.cpu_count() or 1, + } + return conf_names.get(name, -1) + + @staticmethod + def getpagesize() -> int: + """获取内存页大小""" + return 4096 + + @staticmethod + def getloadavg() -> List[float]: + """获取系统负载平均值""" + try: + return list(_os.getloadavg()) + except AttributeError: + return [0.0, 0.0, 0.0] + + # ── 管道与 I/O ── + + @staticmethod + def pipe() -> Tuple[int, int]: + """创建管道""" + return _os.pipe() + + @staticmethod + def mkfifo(path: str, mode: int = 0o666): + """创建命名管道 (FIFO)""" + _os.mkfifo(path, mode) + + @staticmethod + def tmpfile() -> str: + """创建临时文件路径""" + import tempfile + return tempfile.mktemp() + + # ── 时间 ── + + @staticmethod + def time() -> int: + """获取当前时间戳 (秒)""" + import time + return int(time.time()) + + # ── 其他 ── + + @staticmethod + def strerror(errno: int) -> str: + """将错误码转为错误描述""" + return _os.strerror(errno) + + def toString(self) -> str: + return f"Os(sysname={platform.system()}, release={platform.release()})" + + def hashCode(self) -> int: + return hash("android.system.Os") & 0x7FFFFFFF + + def equals(self, other) -> bool: + return isinstance(other, Os) + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Os", + "StructStat", + "StructUtsname", + "OsConstants", +] \ No newline at end of file diff --git a/android_pyapi/android/telecom.py b/android_pyapi/android/telecom.py new file mode 100644 index 0000000..f25dab4 --- /dev/null +++ b/android_pyapi/android/telecom.py @@ -0,0 +1,737 @@ +""" +Android Telecom API — TelecomManager, Call, CallAudioState, DisconnectCause, PhoneAccount, Connection +方法签名完全对齐 Android SDK +""" + +import sys +import time +from typing import Any, Dict, List, Optional, Set, Tuple, Callable + +from .._core import Stub, AndroidOnlyError +from ..java.lang import RuntimeException + + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "TelecomManager", + "Call", + "CallAudioState", + "DisconnectCause", + "PhoneAccount", + "Connection", +] + + +# ═══════════════════════════════════════════════════════════════ +# TelecomManager +# ═══════════════════════════════════════════════════════════════ + +class TelecomManager(Stub): + """TelecomManager — 电信管理器 (桌面存根)""" + + ACTION_CHANGE_DEFAULT_DIALER = "android.telecom.action.CHANGE_DEFAULT_DIALER" + ACTION_SHOW_CALL_SETTINGS = "android.telecom.action.SHOW_CALL_SETTINGS" + ACTION_SETTINGS = "android.telecom.action.SETTINGS" + ACTION_SHOW_MISSED_CALLS_NOTIFICATION = "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION" + ACTION_CALL_FORWARDING = "android.telecom.action.CALL_FORWARDING" + ACTION_DEFAULT_CALL_SCREENING_APP = "android.telecom.action.DEFAULT_CALL_SCREENING_APP" + ACTION_DEFAULT_DIALER_CHANGED = "android.telecom.action.DEFAULT_DIALER_CHANGED" + ACTION_NEW_UNKNOWN_CALL = "android.telecom.action.NEW_UNKNOWN_CALL" + ACTION_TTY_PREFERRED = "android.telecom.action.TTY_PREFERRED" + ACTION_TTY_ENABLED = "android.telecom.action.TTY_ENABLED" + ACTION_TTY_DISABLED = "android.telecom.action.TTY_DISABLED" + ACTION_CONNECTION_SERVICE_CONFIGURE = "android.telecom.action.CONNECTION_SERVICE_CONFIGURE" + ACTION_CALL_DIAGNOSTIC_STATE_CHANGED = "android.telecom.action.CALL_DIAGNOSTIC_STATE_CHANGED" + DURATION_SHORT = "short" + DURATION_LONG = "long" + EXTRA_CALL_BACK_NUMBER = "android.telecom.extra.CALL_BACK_NUMBER" + EXTRA_CALL_CREATED_TIME_MILLIS = "android.telecom.extra.CALL_CREATED_TIME_MILLIS" + EXTRA_CALL_TECHNOLOGY_TYPE = "android.telecom.extra.CALL_TECHNOLOGY_TYPE" + EXTRA_CALL_NETWORK_TYPE = "android.telecom.extra.CALL_NETWORK_TYPE" + EXTRA_CALL_SUBSTATE = "android.telecom.extra.CALL_SUBSTATE" + EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME = "android.telecom.extra.CHANGE_DEFAULT_DIALER_PACKAGE_NAME" + EXTRA_CONNECTION_SERVICE = "android.telecom.extra.CONNECTION_SERVICE" + EXTRA_CALL_DISCONNECT_CAUSE = "android.telecom.extra.CALL_DISCONNECT_CAUSE" + EXTRA_DISCONNECT_CAUSE_DESCRIPTION = "android.telecom.extra.DISCONNECT_CAUSE_DESCRIPTION" + EXTRA_PHONE_ACCOUNT_HANDLE = "android.telecom.extra.PHONE_ACCOUNT_HANDLE" + EXTRA_PRIORITY = "android.telecom.extra.PRIORITY" + EXTRA_START_CALL_WITH_VIDEO_STATE = "android.telecom.extra.START_CALL_WITH_VIDEO_STATE" + EXTRA_HANDOVER_FROM = "android.telecom.extra.HANDOVER_FROM" + EXTRA_HANDOVER_TO = "android.telecom.extra.HANDOVER_TO" + EXTRA_IS_HANDOVER = "android.telecom.extra.IS_HANDOVER" + TTY_MODE_OFF = 0 + TTY_MODE_FULL = 1 + TTY_MODE_HCO = 2 + TTY_MODE_VCO = 3 + PRESENTATION_ALLOWED = 1 + PRESENTATION_RESTRICTED = 2 + PRESENTATION_UNKNOWN = 3 + PRESENTATION_PAYPHONE = 4 + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._default_dialer = "com.android.dialer" + self._phone_accounts: Dict[str, Any] = {} + + def getDefaultDialerPackage(self) -> str: + """获取默认拨号器包名""" + return self._default_dialer + + def getCurrentTtyMode(self) -> int: + """获取当前 TTY 模式""" + return self.TTY_MODE_OFF + + def getCallCapablePhoneAccounts(self) -> List[Any]: + """获取可通话的电话账户""" + return list(self._phone_accounts.values()) + + def getPhoneAccount(self, accountHandle: Any) -> Optional[Any]: + """获取指定电话账户""" + return self._phone_accounts.get(str(accountHandle)) + + def getPhoneAccountList(self) -> List[Any]: + """获取电话账户列表""" + return list(self._phone_accounts.values()) + + def getSimConfig(self, accountHandle: Any) -> Optional[Any]: + """获取 SIM 配置""" + return None + + def registerPhoneAccount(self, account: "PhoneAccount"): + """注册电话账户""" + self._phone_accounts[str(account.getAccountHandle())] = account + + def unregisterPhoneAccount(self, accountHandle: Any): + """注销电话账户""" + self._phone_accounts.pop(str(accountHandle), None) + + def clearPhoneAccounts(self): + """清除所有电话账户""" + self._phone_accounts.clear() + + def clearAccounts(self): + """清除所有账户""" + self._phone_accounts.clear() + + def showInCallScreen(self, showDialpad: bool): + """显示通话界面""" + pass + + def acceptRingingCall(self): + """接听响铃呼叫""" + pass + + def silenceRinger(self): + """静音铃声""" + pass + + def isInCall(self) -> bool: + """检查是否正在通话""" + return False + + def isInManagedCall(self) -> bool: + """检查是否在托管通话中""" + return False + + def addNewIncomingCall(self, phoneAccount: "PhoneAccount", extras: Any = None): + """添加新的来电""" + pass + + def addNewUnknownCall(self, phoneAccount: "PhoneAccount", extras: Any = None): + """添加新的未知呼叫""" + pass + + def handleCreateConferenceComplete( + self, callId: str, response: Any + ): + """处理创建会议完成""" + pass + + def transferCall(self, callId: str, targetPhoneAccount: Any): + """转移呼叫""" + pass + + def setUserSelectedOutgoingPhoneAccount( + self, accountHandle: Any + ): + """设置用户选择的呼出电话账户""" + pass + + def toString(self) -> str: + return f"TelecomManager@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# Call +# ═══════════════════════════════════════════════════════════════ + +class Call(Stub): + """Call — 通话 (桌面存根)""" + + STATE_ACTIVE = 1 + STATE_HOLDING = 2 + STATE_DIALING = 3 + STATE_RINGING = 4 + STATE_CONNECTING = 5 + STATE_DISCONNECTED = 6 + STATE_DISCONNECTING = 7 + STATE_SELECT_PHONE_ACCOUNT = 8 + STATE_NEW = 9 + STATE_PULLING_CALL = 10 + + def __init__(self): + super().__init__() + self._state = self.STATE_NEW + self._details: Optional["Call.Details"] = None + self._conferenceable_calls: List["Call"] = [] + self._callbacks: List[Callable] = [] + self._listeners: List[Callable] = [] + + def getState(self) -> int: + return self._state + + def getDetails(self) -> Optional["Call.Details"]: + return self._details + + def getConferenceableCalls(self) -> List["Call"]: + return list(self._conferenceable_calls) + + def registerCallback(self, callback: Callable): + if callback not in self._callbacks: + self._callbacks.append(callback) + + def unregisterCallback(self, callback: Callable): + if callback in self._callbacks: + self._callbacks.remove(callback) + + def addListener(self, listener: Callable): + if listener not in self._listeners: + self._listeners.append(listener) + + def removeListener(self, listener: Callable): + if listener in self._listeners: + self._listeners.remove(listener) + + def answer(self, videoState: int = 0): + """接听通话""" + self._state = self.STATE_ACTIVE + + def reject(self, rejectWithMessage: bool = False, textMessage: str = ""): + """拒绝通话""" + self._state = self.STATE_DISCONNECTED + + def disconnect(self): + """挂断通话""" + self._state = self.STATE_DISCONNECTED + + def hold(self): + """保持通话""" + self._state = self.STATE_HOLDING + + def unhold(self): + """取消保持""" + self._state = self.STATE_ACTIVE + + def playDtmfTone(self, digit: str): + """播放 DTMF 音""" + pass + + def stopDtmfTone(self): + """停止 DTMF 音""" + pass + + def postDialWait(self, remaining: str): + """后拨号等待""" + pass + + def mergeConference(self): + """合并会议""" + pass + + def splitFromConference(self): + """从会议分离""" + pass + + def sendCallEvent(self, event: str, extras: Any = None): + """发送通话事件""" + pass + + def putExtra(self, key: str, value: Any): + """设置额外信息""" + pass + + # ═══════════════════════════════════════════════════════════ + # Details + # ═══════════════════════════════════════════════════════════ + + class Details(Stub): + """Call.Details — 通话详细信息""" + + DIRECTION_INCOMING = 1 + DIRECTION_OUTGOING = 2 + + CAPABILITY_HOLD = 1 + CAPABILITY_SUPPORT_HOLD = 2 + CAPABILITY_MERGE_CONFERENCE = 4 + CAPABILITY_SWAP_CONFERENCE = 8 + CAPABILITY_RESPOND_VIA_TEXT = 16 + CAPABILITY_MUTE = 32 + CAPABILITY_MANAGE_CONFERENCE = 64 + CAPABILITY_SUPPORTS_VT_LOCAL = 128 + CAPABILITY_SUPPORTS_VT_REMOTE = 256 + CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL = 512 + CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL = 1024 + CAPABILITY_SUPPORTS_DOWNGRADE_VT_TO_AUDIO = 2048 + CAPABILITY_SUPPORTS_VIDEO_CALLING = 4096 + CAPABILITY_SUPPORTS_VT_LOCAL_RX = 8192 + CAPABILITY_SUPPORTS_VT_LOCAL_TX = 16384 + CAPABILITY_SUPPORTS_VT_REMOTE_RX = 32768 + CAPABILITY_SUPPORTS_VT_REMOTE_TX = 65536 + CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 131072 + CAPABILITY_CAN_PAUSE_VIDEO = 262144 + CAPABILITY_SELF_MANAGED = 524288 + CAPABILITY_SUPPORTS_VOIP = 1048576 + CAPABILITY_ADD_PARTICIPANT = 2097152 + CAPABILITY_CAN_UPGRADE_TO_VIDEO = 4194304 + + PROPERTY_HIGH_DEF_AUDIO = 1 + PROPERTY_WIFI = 2 + PROPERTY_EMERGENCY_CALLBACK = 4 + PROPERTY_IS_EXTERNAL_CALL = 8 + PROPERTY_HAS_CDMA_VOICE_PRIVACY = 16 + PROPERTY_SELF_MANAGED = 32 + PROPERTY_ASSISTED_DIALING = 64 + PROPERTY_NETWORK_IDENTIFIED = 128 + PROPERTY_CALL_AUTO_FORWARDED = 256 + PROPERTY_CALL_TYPE_UNKNOWN = 512 + PROPERTY_IS_VOIP_AUDIO_MODE = 1024 + PROPERTY_IS_DOWNGRADED_CALL = 2048 + PROPERTY_ENTITLEMENT_CHANNEL = 4096 + PROPERTY_PULLED_CALL = 8192 + PROPERTY_CROSS_SIM = 16384 + PROPERTY_VIDEO_CALL = 32768 + PROPERTY_RTT = 65536 + + def __init__(self): + super().__init__() + self._handle: Any = None + self._gateway_info: Any = None + self._caller_display_name: str = "" + self._call_direction = self.DIRECTION_INCOMING + self._account_handle: Any = None + self._call_capabilities = 0 + self._call_properties = 0 + self._caller_info: Any = None + self._creation_time_millis = 0 + self._connect_time_millis = 0 + self._disconnect_cause: Any = None + self._video_state = 0 + self._intent_extras: Any = None + self._extras: Any = None + + def getHandle(self) -> Optional[Any]: + return self._handle + + def getGatewayInfo(self) -> Optional[Any]: + return self._gateway_info + + def getCallerDisplayName(self) -> str: + return self._caller_display_name + + def getCallDirection(self) -> int: + return self._call_direction + + def getAccountHandle(self) -> Optional[Any]: + return self._account_handle + + def getCallCapabilities(self) -> int: + return self._call_capabilities + + def getCallProperties(self) -> int: + return self._call_properties + + def getCallerInfo(self) -> Optional[Any]: + return self._caller_info + + def getCreationTimeMillis(self) -> int: + return self._creation_time_millis + + def getConnectTimeMillis(self) -> int: + return self._connect_time_millis + + def getDisconnectCause(self) -> Optional["DisconnectCause"]: + return self._disconnect_cause + + def getVideoState(self) -> int: + return self._video_state + + def getIntentExtras(self) -> Optional[Any]: + return self._intent_extras + + def getExtras(self) -> Optional[Any]: + return self._extras + + def toString(self) -> str: + return f"Call.Details(handle={self._handle})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def toString(self) -> str: + return f"Call(state={self._state})" + + def equals(self, other) -> bool: + if not isinstance(other, Call): + return False + return self._state == other._state and self._details == other._details + + def hashCode(self) -> int: + return self._state & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# CallAudioState +# ═══════════════════════════════════════════════════════════════ + +class CallAudioState(Stub): + """CallAudioState — 通话音频状态""" + + ROUTE_EARPIECE = 1 + ROUTE_SPEAKER = 2 + ROUTE_BLUETOOTH = 4 + ROUTE_WIRED_HEADSET = 8 + ROUTE_WIRED_OR_EARPIECE = 9 + + def __init__(self): + super().__init__() + self._route = self.ROUTE_EARPIECE + self._supported_route_mask = ( + self.ROUTE_EARPIECE | self.ROUTE_SPEAKER | self.ROUTE_WIRED_HEADSET + ) + + def getRoute(self) -> int: + return self._route + + def getSupportedRouteMask(self) -> int: + return self._supported_route_mask + + def toString(self) -> str: + return f"CallAudioState(route={self._route})" + + def equals(self, other) -> bool: + if not isinstance(other, CallAudioState): + return False + return self._route == other._route + + def hashCode(self) -> int: + return self._route & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# DisconnectCause +# ═══════════════════════════════════════════════════════════════ + +class DisconnectCause(Stub): + """DisconnectCause — 断连原因""" + + LOCAL = 1 + REMOTE = 2 + BUSY = 3 + CANCELED = 4 + NO_ANSWER = 5 + MISSED = 6 + ERROR = 7 + REJECTED = 8 + NUMBER_UNREACHABLE = 9 + RESTRICTED = 10 + OTHER = 11 + ANSWERED_ELSEWHERE = 12 + CALL_PULLED = 13 + + def __init__(self, causeCode: int = OTHER): + super().__init__() + self._cause_code = causeCode + self._reason: str = "" + self._description: str = "" + + def getCode(self) -> int: + return self._cause_code + + def getReason(self) -> str: + return self._reason + + def getDescription(self) -> str: + return self._description + + def toString(self) -> str: + return f"DisconnectCause(code={self._cause_code})" + + def equals(self, other) -> bool: + if not isinstance(other, DisconnectCause): + return False + return self._cause_code == other._cause_code + + def hashCode(self) -> int: + return self._cause_code & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# PhoneAccount +# ═══════════════════════════════════════════════════════════════ + +class PhoneAccount(Stub): + """PhoneAccount — 电话账户""" + + CAP_HOLD = 1 + CAP_ADD_CALL = 2 + CAP_MULTI_ENDPOINT = 4 + CAP_CONFERENCE = 8 + CAP_VIDEO_CALLING = 16 + CAP_CALL_COMPOSER = 32 + CAP_CONNECTION_MANAGER = 64 + + EXTRA_LOG_SELF_MANAGED_CALLS = "android.telecom.extra.LOG_SELF_MANAGED_CALLS" + EXTRA_ADD_SELF_MANAGED_CALLS = "android.telecom.extra.ADD_SELF_MANAGED_CALLS" + + SCREENING_ALLOW = 0 + SCREENING_STOP = 1 + SCREENING_SILENCE = 2 + SCREENING_DISALLOW = 3 + + SCHEME_SIP = "sip" + SCHEME_TEL = "tel" + SCHEME_VOICEMAIL = "voicemail" + + class Builder(Stub): + """PhoneAccount.Builder — 电话账户构建器""" + + def __init__(self, accountHandle: Any, label: str): + super().__init__() + self._account = PhoneAccount() + self._account._account_handle = accountHandle + self._account._label = label + + def setCapabilities(self, capabilities: int) -> "PhoneAccount.Builder": + self._account._capabilities = capabilities + return self + + def setAddress(self, address: Any) -> "PhoneAccount.Builder": + self._account._address = address + return self + + def setSubscriptionAddress(self, address: Any) -> "PhoneAccount.Builder": + self._account._subscription_address = address + return self + + def setSupportedUriSchemes( + self, uriSchemes: List[str] + ) -> "PhoneAccount.Builder": + self._account._supported_uri_schemes = list(uriSchemes) + return self + + def setIcon(self, icon: Any) -> "PhoneAccount.Builder": + self._account._icon = icon + return self + + def setHighlightColor(self, color: int) -> "PhoneAccount.Builder": + self._account._highlight_color = color + return self + + def setShortDescription(self, description: str) -> "PhoneAccount.Builder": + self._account._short_description = description + return self + + def addSupportedUriScheme( + self, uriScheme: str + ) -> "PhoneAccount.Builder": + if uriScheme not in self._account._supported_uri_schemes: + self._account._supported_uri_schemes.append(uriScheme) + return self + + def setExtras(self, extras: Any) -> "PhoneAccount.Builder": + self._account._extras = extras + return self + + def build(self) -> "PhoneAccount": + return self._account + + def toString(self) -> str: + return f"PhoneAccount.Builder(label={self._account._label})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def __init__(self): + super().__init__() + self._account_handle: Any = None + self._label: str = "" + self._capabilities: int = 0 + self._address: Any = None + self._subscription_address: Any = None + self._supported_uri_schemes: List[str] = [self.SCHEME_TEL] + self._icon: Any = None + self._highlight_color: int = 0 + self._short_description: str = "" + self._extras: Any = None + + def getAccountHandle(self) -> Optional[Any]: + return self._account_handle + + def getLabel(self) -> str: + return self._label + + def getCapabilities(self) -> int: + return self._capabilities + + def getAddress(self) -> Optional[Any]: + return self._address + + def getSubscriptionAddress(self) -> Optional[Any]: + return self._subscription_address + + def getSupportedUriSchemes(self) -> List[str]: + return list(self._supported_uri_schemes) + + def getIcon(self) -> Optional[Any]: + return self._icon + + def getHighlightColor(self) -> int: + return self._highlight_color + + def getShortDescription(self) -> str: + return self._short_description + + def getExtras(self) -> Optional[Any]: + return self._extras + + def toString(self) -> str: + return f"PhoneAccount(label={self._label})" + + def equals(self, other) -> bool: + if not isinstance(other, PhoneAccount): + return False + return self._account_handle == other._account_handle + + def hashCode(self) -> int: + return hash(str(self._account_handle)) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# Connection +# ═══════════════════════════════════════════════════════════════ + +class Connection(Stub): + """Connection — 通话连接 (桌面存根)""" + + STATE_INITIALIZING = 0 + STATE_ACTIVE = 1 + STATE_DIALING = 2 + STATE_RINGING = 3 + STATE_HOLDING = 4 + STATE_DISCONNECTED = 5 + STATE_PULLING_CALL = 6 + + CAPABILITY_HOLD = 1 + CAPABILITY_SUPPORT_HOLD = 2 + CAPABILITY_MERGE_CONFERENCE = 4 + CAPABILITY_SWAP_CONFERENCE = 8 + CAPABILITY_RESPOND_VIA_TEXT = 16 + CAPABILITY_MUTE = 32 + CAPABILITY_MANAGE_CONFERENCE = 64 + CAPABILITY_SUPPORTS_VT_LOCAL = 128 + CAPABILITY_SUPPORTS_VT_REMOTE = 256 + CAPABILITY_SUPPORTS_VIDEO_CALLING = 4096 + CAPABILITY_SELF_MANAGED = 524288 + CAPABILITY_SUPPORTS_VOIP = 1048576 + CAPABILITY_ADD_PARTICIPANT = 2097152 + + PROPERTY_SELF_MANAGED = 32 + PROPERTY_IS_VOIP_AUDIO_MODE = 1024 + PROPERTY_HIGH_DEF_AUDIO = 1 + PROPERTY_WIFI = 2 + + def __init__(self): + super().__init__() + self._state = self.STATE_INITIALIZING + self._video_state = 0 + self._connection_capabilities = 0 + self._connection_properties = 0 + self._audio_mode_is_voip = False + self._conferenceable_connections: List["Connection"] = [] + self._extras: Dict[str, Any] = {} + + def setVideoState(self, videoState: int): + self._video_state = videoState + + def setActive(self): + self._state = self.STATE_ACTIVE + + def setRinging(self): + self._state = self.STATE_RINGING + + def setDialing(self): + self._state = self.STATE_DIALING + + def setHolding(self): + self._state = self.STATE_HOLDING + + def setDisconnected(self, disconnectCause: "DisconnectCause"): + self._state = self.STATE_DISCONNECTED + + def setPostDialWait(self, remaining: str): + pass + + def setConferenceableConnections( + self, conferenceable: List["Connection"] + ): + self._conferenceable_connections = list(conferenceable) + + def setAudioModeIsVoip(self, isVoip: bool): + self._audio_mode_is_voip = isVoip + + def setConnectionCapabilities(self, capabilities: int): + self._connection_capabilities = capabilities + + def setConnectionProperties(self, properties: int): + self._connection_properties = properties + + def putExtra(self, key: str, value: Any): + self._extras[key] = value + + def onShowIncomingCallUi(self): + pass + + def onCallAudioStateChanged(self, audioState: "CallAudioState"): + pass + + def destroy(self): + self._state = self.STATE_DISCONNECTED + + def onCallFilteringCompleted(self, completion: Any): + pass + + def onSilence(self): + pass + + def toString(self) -> str: + return f"Connection(state={self._state})" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF \ No newline at end of file diff --git a/android_pyapi/android/test.py b/android_pyapi/android/test.py new file mode 100644 index 0000000..e730422 --- /dev/null +++ b/android_pyapi/android/test.py @@ -0,0 +1,485 @@ +""" +Android Test 组件 — InstrumentationTestRunner, InstrumentationTestCase, +ActivityInstrumentationTestCase2, ActivityUnitTestCase, ServiceTestCase, +ProviderTestCase2, ApplicationTestCase, Instrumentation 等 +方法签名完全对齐 Android SDK (android.test 包) +""" + +import os +import sys +import time +import threading +from typing import Any, Callable, Dict, List, Optional, Tuple, Type + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# Instrumentation +# ═══════════════════════════════════════════════════════════════ + +class Instrumentation(Stub): + """Instrumentation — 测试仪器 (桌面版)""" + + # 报告标志 + REPORT_KEY_IDENTIFIER = "id" + REPORT_KEY_STREAM_RESULTS = "stream" + + def __init__(self): + super().__init__() + self._context: Optional[Any] = None + self._target_context: Optional[Any] = None + self._started = False + self._finished = False + self._automatic_performance_snapshots = False + self._instr_result: Dict[str, Any] = {} + + def onCreate(self, arguments: Any): + """创建仪器""" + self._started = True + + def onStart(self): + """启动仪器""" + pass + + def onDestroy(self): + """销毁仪器""" + self._finished = True + + def getContext(self) -> Optional[Any]: + """获取上下文""" + return self._context + + def getTargetContext(self) -> Optional[Any]: + """获取目标上下文""" + return self._target_context + + def setContext(self, context: Any): + self._context = context + + def setTargetContext(self, target_context: Any): + self._target_context = target_context + + def startActivitySync(self, intent: Any) -> Any: + """同步启动 Activity""" + return None + + def sendKeySync(self, key_event: Any): + """发送按键事件""" + pass + + def sendCharacterSync(self, character: int): + """发送字符事件""" + pass + + def sendStringSync(self, text: str): + """发送字符串""" + pass + + def sendPointerSync(self, motion_event: Any): + """发送指针事件""" + pass + + def sendTrackballEventSync(self, motion_event: Any): + """发送轨迹球事件""" + pass + + def runOnMainSync(self, runner: Callable): + """在主线程上同步运行""" + runner() + + def waitForIdle(self): + """等待空闲""" + pass + + def waitForIdleSync(self): + """同步等待空闲""" + pass + + def callActivityOnCreate(self, activity: Any, icicle: Any): + """调用 Activity.onCreate""" + if hasattr(activity, 'onCreate'): + activity.onCreate(icicle) + + def callActivityOnStart(self, activity: Any): + """调用 Activity.onStart""" + if hasattr(activity, 'onStart'): + activity.onStart() + + def callActivityOnResume(self, activity: Any): + """调用 Activity.onResume""" + if hasattr(activity, 'onResume'): + activity.onResume() + + def callActivityOnPause(self, activity: Any): + """调用 Activity.onPause""" + if hasattr(activity, 'onPause'): + activity.onPause() + + def callActivityOnStop(self, activity: Any): + """调用 Activity.onStop""" + if hasattr(activity, 'onStop'): + activity.onStop() + + def callActivityOnDestroy(self, activity: Any): + """调用 Activity.onDestroy""" + if hasattr(activity, 'onDestroy'): + activity.onDestroy() + + def callActivityOnRestart(self, activity: Any): + """调用 Activity.onRestart""" + if hasattr(activity, 'onRestart'): + activity.onRestart() + + def isProfiling(self) -> bool: + return False + + def startProfiling(self): + pass + + def stopProfiling(self): + pass + + def setInTouchMode(self, in_touch: bool): + pass + + def finish(self, result_code: int, results: Any = None): + """完成测试""" + self._finished = True + if results: + self._instr_result.update(results) + + def getComponentName(self) -> Any: + return None + + def getUiAutomation(self) -> Any: + return None + + def toString(self) -> str: + return (f"Instrumentation(started={self._started}, " + f"finished={self._finished})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# InstrumentationTestRunner +# ═══════════════════════════════════════════════════════════════ + +class InstrumentationTestRunner(Stub): + """InstrumentationTestRunner — 仪器化测试运行器""" + + REPORT_KEY_NAME = "name" + REPORT_KEY_CLASS = "class" + REPORT_KEY_TEST = "test" + REPORT_KEY_NUM_TOTAL = "numtests" + REPORT_KEY_NUM_CURRENT = "current" + REPORT_KEY_NUM_ITERATIONS = "numiterations" + + def __init__(self): + super().__init__() + self._instrumentation = Instrumentation() + self._test_results: List[Dict[str, Any]] = [] + + def onCreate(self, arguments: Any): + """创建测试运行器""" + self._instrumentation.onCreate(arguments) + + def onStart(self): + """开始测试""" + self._instrumentation.onStart() + + def onDestroy(self): + """销毁""" + self._instrumentation.onDestroy() + + def getInstrumentation(self) -> Instrumentation: + return self._instrumentation + + def runTest(self, test_case: Any): + """运行测试用例""" + try: + if hasattr(test_case, 'setUp'): + test_case.setUp() + if hasattr(test_case, 'runTest'): + test_case.runTest() + if hasattr(test_case, 'tearDown'): + test_case.tearDown() + self._test_results.append({"success": True, "test": str(test_case)}) + except Exception as e: + self._test_results.append({ + "success": False, "test": str(test_case), "error": str(e), + }) + + def getTestResults(self) -> List[Dict[str, Any]]: + return list(self._test_results) + + def toString(self) -> str: + return (f"InstrumentationTestRunner(" + f"results={len(self._test_results)})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + +# ═══════════════════════════════════════════════════════════════ +# InstrumentationTestCase +# ═══════════════════════════════════════════════════════════════ + +class InstrumentationTestCase(Stub): + """InstrumentationTestCase — 仪器化测试用例基类""" + + def __init__(self): + super().__init__() + self._instrumentation: Optional[Instrumentation] = None + self._test_name: str = "" + + def injectInstrumentation(self, instrumentation: Instrumentation): + """注入仪器""" + self._instrumentation = instrumentation + + def getInstrumentation(self) -> Optional[Instrumentation]: + return self._instrumentation + + def setUp(self): + """测试前准备""" + pass + + def tearDown(self): + """测试后清理""" + pass + + def runTest(self): + """执行测试""" + pass + + def sendKeys(self, *keys: str): + """发送按键""" + pass + + def touchDown(self, view: Any, x: float, y: float): + """触摸按下""" + pass + + def touchUp(self, view: Any, x: float, y: float): + """触摸抬起""" + pass + + def touchAndHold(self, view: Any, x: float, y: float): + """触摸并按住""" + pass + + def click(self, view: Any): + """点击""" + pass + + def typeText(self, view: Any, text: str): + """输入文本""" + pass + + def assertActivityRequiresFeature(self, activity_class: Any, + feature: str) -> bool: + return True + + def toString(self) -> str: + return f"InstrumentationTestCase(name={self._test_name})" + + def hashCode(self) -> int: + return hash(self._test_name) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, InstrumentationTestCase): + return False + return self._test_name == other._test_name + + +# ═══════════════════════════════════════════════════════════════ +# ActivityInstrumentationTestCase2 +# ═══════════════════════════════════════════════════════════════ + +class ActivityInstrumentationTestCase2(InstrumentationTestCase): + """ActivityInstrumentationTestCase2 — Activity 测试用例""" + + def __init__(self, pkg: str, activity_class: Type): + super().__init__() + self._pkg = pkg + self._activity_class = activity_class + self._activity: Optional[Any] = None + + def setActivityIntent(self, intent: Any): + """设置 Activity Intent""" + pass + + def getActivity(self) -> Optional[Any]: + """获取 Activity 实例""" + return self._activity + + def testActivityCreated(self) -> bool: + return self._activity is not None + + def testActivityResumed(self) -> bool: + return self._activity is not None + + def testActivityStarted(self) -> bool: + return self._activity is not None + + def toString(self) -> str: + return (f"ActivityInstrumentationTestCase2(" + f"pkg={self._pkg}, cls={self._activity_class})") + + +# ═══════════════════════════════════════════════════════════════ +# ActivityUnitTestCase +# ═══════════════════════════════════════════════════════════════ + +class ActivityUnitTestCase(InstrumentationTestCase): + """ActivityUnitTestCase — Activity 单元测试""" + + def __init__(self, activity_class: Type): + super().__init__() + self._activity_class = activity_class + self._activity: Optional[Any] = None + + def setActivityContext(self, context: Any): + """设置 Activity 上下文""" + if self._activity and hasattr(self._activity, 'setContext'): + self._activity.setContext(context) + + def setApplication(self, application: Any): + """设置 Application""" + if self._activity and hasattr(self._activity, 'setApplication'): + self._activity.setApplication(application) + + def getActivity(self) -> Optional[Any]: + return self._activity + + def startActivity(self, intent: Any, bundle: Any = None, + last_non_config_instance: Any = None) -> Any: + """启动 Activity""" + return self._activity + + def toString(self) -> str: + return f"ActivityUnitTestCase(cls={self._activity_class})" + + +# ═══════════════════════════════════════════════════════════════ +# ServiceTestCase +# ═══════════════════════════════════════════════════════════════ + +class ServiceTestCase(InstrumentationTestCase): + """ServiceTestCase — Service 测试用""" + + def __init__(self, service_class: Type): + super().__init__() + self._service_class = service_class + self._service: Optional[Any] = None + self._service_binder: Optional[Any] = None + self._service_created = False + + def getService(self) -> Optional[Any]: + return self._service + + def bindService(self, intent: Any) -> Any: + """绑定 Service""" + return self._service_binder + + def bindServiceNotNeeded(self): + """Service 不需要绑定""" + pass + + def setService(self, service: Any): + self._service = service + + def testServiceCreated(self) -> bool: + return self._service_created + + def testServiceBound(self) -> bool: + return self._service_binder is not None + + def testServiceStarted(self) -> bool: + return self._service is not None + + def testServiceStopped(self) -> bool: + return not self._service_created + + def toString(self) -> str: + return f"ServiceTestCase(cls={self._service_class})" + + +# ═══════════════════════════════════════════════════════════════ +# ProviderTestCase2 +# ═══════════════════════════════════════════════════════════════ + +class ProviderTestCase2(InstrumentationTestCase): + """ProviderTestCase2 — ContentProvider 测试用例""" + + def __init__(self, provider_class: Type, provider_authority: str): + super().__init__() + self._provider_class = provider_class + self._provider_authority = provider_authority + self._provider: Optional[Any] = None + self._resolver: Optional[Any] = None + + def getProvider(self) -> Optional[Any]: + return self._provider + + def getMockContentResolver(self) -> Optional[Any]: + return self._resolver + + def setProvider(self, provider: Any): + self._provider = provider + + def setMockContentResolver(self, resolver: Any): + self._resolver = resolver + + def toString(self) -> str: + return (f"ProviderTestCase2(cls={self._provider_class}, " + f"authority={self._provider_authority})") + + +# ═══════════════════════════════════════════════════════════════ +# ApplicationTestCase +# ═══════════════════════════════════════════════════════════════ + +class ApplicationTestCase(InstrumentationTestCase): + """ApplicationTestCase — Application 测试用例""" + + def __init__(self, application_class: Type): + super().__init__() + self._application_class = application_class + self._application: Optional[Any] = None + + def getApplication(self) -> Optional[Any]: + return self._application + + def setApplication(self, application: Any): + self._application = application + + def testApplicationCreated(self) -> bool: + return self._application is not None + + def toString(self) -> str: + return f"ApplicationTestCase(cls={self._application_class})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "InstrumentationTestRunner", + "InstrumentationTestCase", + "ActivityInstrumentationTestCase2", + "ActivityUnitTestCase", + "ServiceTestCase", + "ProviderTestCase2", + "ApplicationTestCase", + "Instrumentation", +] \ No newline at end of file diff --git a/android_pyapi/android/transition.py b/android_pyapi/android/transition.py new file mode 100644 index 0000000..f7064cc --- /dev/null +++ b/android_pyapi/android/transition.py @@ -0,0 +1,504 @@ +""" +Android Transition 组件 — Transition, TransitionManager, Scene, Fade, Slide, Explode 等 +方法签名完全对齐 Android SDK (android.transition 包) +""" + +import time +from typing import Any, Callable, Dict, List, Optional, Set, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# Scene +# ═══════════════════════════════════════════════════════════════ + +class Scene(Stub): + """Scene — 转场场景""" + + def __init__(self, root: Any = None, layout: Any = None): + super().__init__() + self._root = root + self._layout = layout + self._entered = False + + @staticmethod + def getSceneForLayout(root: Any, layout_id: int, context: Any) -> "Scene": + """从布局资源创建场景""" + return Scene(root, layout_id) + + def getRoot(self) -> Any: + return self._root + + def enter(self): + """进入场景""" + self._entered = True + + def exit(self): + """退出场景""" + self._entered = False + + def isEntered(self) -> bool: + return self._entered + + def setEnterAction(self, action: Callable): + """设置进入动作""" + self._enter_action = action + + def setExitAction(self, action: Callable): + """设置退出动作""" + self._exit_action = action + + def toString(self) -> str: + return f"Scene(root={self._root}, entered={self._entered})" + + def hashCode(self) -> int: + return hash(id(self._root)) & 0x7FFFFFFF + + def equals(self, other) -> bool: + if not isinstance(other, Scene): + return False + return self._root is other._root + + +# ═══════════════════════════════════════════════════════════════ +# Transition +# ═══════════════════════════════════════════════════════════════ + +class Transition(Stub): + """Transition — 转场动画基类""" + + # 匹配模式 + MATCH_VIEW_NAME = 0 + MATCH_ID = 1 + MATCH_INSTANCE = 2 + MATCH_ITEM_ID = 3 + + def __init__(self): + super().__init__() + self._duration: int = 300 + self._start_delay: int = 0 + self._interpolator: Optional[Any] = None + self._targets: Set[Any] = set() + self._target_ids: Set[int] = set() + self._target_names: Set[str] = set() + self._target_types: Set[type] = set() + self._exclude_targets: Set[Any] = set() + self._exclude_ids: Set[int] = set() + self._exclude_names: Set[str] = set() + self._exclude_types: Set[type] = set() + self._listeners: List[Any] = [] + self._name: str = "" + self._epicenter_callback: Optional[Callable] = None + self._path_motion: Optional[Any] = None + self._propagation: Optional[Any] = None + self._paused: bool = False + self._started: bool = False + self._ended: bool = False + + def setDuration(self, duration_ms: int) -> "Transition": + """设置持续时间 (毫秒)""" + self._duration = max(0, duration_ms) + return self + + def getDuration(self) -> int: + return self._duration + + def setStartDelay(self, start_delay: int) -> "Transition": + """设置起始延迟 (毫秒)""" + self._start_delay = max(0, start_delay) + return self + + def getStartDelay(self) -> int: + return self._start_delay + + def setInterpolator(self, interpolator: Any) -> "Transition": + """设置插值器""" + self._interpolator = interpolator + return self + + def getInterpolator(self) -> Any: + return self._interpolator + + def addTarget(self, target: Any) -> "Transition": + """添加动画目标""" + if isinstance(target, int): + self._target_ids.add(target) + elif isinstance(target, str): + self._target_names.add(target) + elif isinstance(target, type): + self._target_types.add(target) + else: + self._targets.add(target) + return self + + def removeTarget(self, target: Any) -> "Transition": + """移除动画目标""" + if isinstance(target, int): + self._target_ids.discard(target) + elif isinstance(target, str): + self._target_names.discard(target) + elif isinstance(target, type): + self._target_types.discard(target) + else: + self._targets.discard(target) + return self + + def getTargets(self) -> List[Any]: + return list(self._targets) + + def excludeTarget(self, target: Any, exclude: bool = True) -> "Transition": + """排除目标""" + if exclude: + if isinstance(target, int): + self._exclude_ids.add(target) + elif isinstance(target, str): + self._exclude_names.add(target) + elif isinstance(target, type): + self._exclude_types.add(target) + else: + self._exclude_targets.add(target) + else: + if isinstance(target, int): + self._exclude_ids.discard(target) + elif isinstance(target, str): + self._exclude_names.discard(target) + elif isinstance(target, type): + self._exclude_types.discard(target) + else: + self._exclude_targets.discard(target) + return self + + def addListener(self, listener: Any) -> "Transition": + """添加转场监听器""" + if listener not in self._listeners: + self._listeners.append(listener) + return self + + def removeListener(self, listener: Any) -> "Transition": + """移除转场监听器""" + if listener in self._listeners: + self._listeners.remove(listener) + return self + + def getListeners(self) -> List[Any]: + return list(self._listeners) + + def setName(self, name: str) -> "Transition": + self._name = name + return self + + def getName(self) -> str: + return self._name + + def setEpicenterCallback(self, callback: Callable) -> "Transition": + self._epicenter_callback = callback + return self + + def getEpicenterCallback(self) -> Optional[Callable]: + return self._epicenter_callback + + def setPathMotion(self, path_motion: Any) -> "Transition": + self._path_motion = path_motion + return self + + def getPathMotion(self) -> Any: + return self._path_motion + + def setPropagation(self, propagation: Any) -> "Transition": + self._propagation = propagation + return self + + def getPropagation(self) -> Any: + return self._propagation + + def isPaused(self) -> bool: + return self._paused + + def pause(self): + """暂停转场""" + self._paused = True + + def resume(self): + """恢复转场""" + self._paused = False + + def cancel(self): + """取消转场""" + self._started = False + self._ended = True + + def end(self): + """结束转场""" + self._ended = True + + def toString(self) -> str: + return (f"{self.__class__.__name__}(duration={self._duration}, " + f"startDelay={self._start_delay}, targets={len(self._targets)})") + + def hashCode(self) -> int: + return id(self) & 0x7FFFFFFF + + def equals(self, other) -> bool: + return self is other + + def captureStartValues(self, view: Any): + """捕获视图起始值""" + pass + + def captureEndValues(self, view: Any): + """捕获视图结束值""" + pass + + def createAnimator(self, view: Any, start_values: Any, + end_values: Any) -> Optional[Any]: + """创建动画器""" + return None + + +# ═══════════════════════════════════════════════════════════════ +# TransitionSet +# ═══════════════════════════════════════════════════════════════ + +class TransitionSet(Transition): + """TransitionSet — 转场合集""" + + ORDERING_TOGETHER = 0 + ORDERING_SEQUENTIAL = 1 + + def __init__(self): + super().__init__() + self._ordering = TransitionSet.ORDERING_TOGETHER + self._transitions: List[Transition] = [] + + def setOrdering(self, ordering: int) -> "TransitionSet": + """设置播放顺序""" + self._ordering = ordering + return self + + def getOrdering(self) -> int: + return self._ordering + + def addTransition(self, transition: Transition) -> "TransitionSet": + """添加子转场""" + self._transitions.append(transition) + return self + + def removeTransition(self, transition: Transition) -> "TransitionSet": + """移除子转场""" + if transition in self._transitions: + self._transitions.remove(transition) + return self + + def getTransitionAt(self, index: int) -> Optional[Transition]: + """获取指定索引的子转场""" + if 0 <= index < len(self._transitions): + return self._transitions[index] + return None + + def getTransitionCount(self) -> int: + return len(self._transitions) + + def toString(self) -> str: + return (f"TransitionSet(ordering={self._ordering}, " + f"count={len(self._transitions)})") + + +# ═══════════════════════════════════════════════════════════════ +# AutoTransition +# ═══════════════════════════════════════════════════════════════ + +class AutoTransition(TransitionSet): + """AutoTransition — 自动过渡转场""" + + def __init__(self): + super().__init__() + self.setOrdering(TransitionSet.ORDERING_TOGETHER) + self._duration = 300 + + def toString(self) -> str: + return f"AutoTransition(duration={self._duration})" + + +# ═══════════════════════════════════════════════════════════════ +# Fade +# ═══════════════════════════════════════════════════════════════ + +class Fade(Transition): + """Fade — 淡入淡出转场""" + + IN = 0x00000001 + OUT = 0x00000002 + + def __init__(self, fading_mode: int = IN | OUT): + super().__init__() + self._fading_mode = fading_mode + + def getFadingMode(self) -> int: + return self._fading_mode + + def setFadingMode(self, fading_mode: int): + self._fading_mode = fading_mode + + def toString(self) -> str: + return f"Fade(mode={self._fading_mode}, duration={self._duration})" + + +# ═══════════════════════════════════════════════════════════════ +# ChangeBounds +# ═══════════════════════════════════════════════════════════════ + +class ChangeBounds(Transition): + """ChangeBounds — 位置变化转场""" + + def __init__(self): + super().__init__() + self._resize_clip: bool = False + self._reparent: bool = False + self._reparent_with_overlay: bool = True + + def setResizeClip(self, resize_clip: bool): + self._resize_clip = resize_clip + + def getResizeClip(self) -> bool: + return self._resize_clip + + def setReparent(self, reparent: bool): + self._reparent = reparent + + def getReparent(self) -> bool: + return self._reparent + + def setReparentWithOverlay(self, reparent_with_overlay: bool): + self._reparent_with_overlay = reparent_with_overlay + + def getReparentWithOverlay(self) -> bool: + return self._reparent_with_overlay + + def toString(self) -> str: + return f"ChangeBounds(duration={self._duration}, resizeClip={self._resize_clip})" + + +# ═══════════════════════════════════════════════════════════════ +# ChangeTransform +# ═══════════════════════════════════════════════════════════════ + +class ChangeTransform(Transition): + """ChangeTransform — 变换属性转场""" + + def __init__(self): + super().__init__() + self._reparent: bool = False + self._reparent_with_overlay: bool = True + + def setReparent(self, reparent: bool): + self._reparent = reparent + + def getReparent(self) -> bool: + return self._reparent + + def setReparentWithOverlay(self, reparent_with_overlay: bool): + self._reparent_with_overlay = reparent_with_overlay + + def getReparentWithOverlay(self) -> bool: + return self._reparent_with_overlay + + def toString(self) -> str: + return f"ChangeTransform(duration={self._duration})" + + +# ═══════════════════════════════════════════════════════════════ +# Slide +# ═══════════════════════════════════════════════════════════════ + +class Slide(Transition): + """Slide — 滑动转场""" + + TOP = 0x00000001 + BOTTOM = 0x00000002 + START = 0x00000003 + END = 0x00000004 + RIGHT = 0x00000005 + LEFT = 0x00000006 + + def __init__(self, slide_edge: int = BOTTOM): + super().__init__() + self._slide_edge = slide_edge + + def getSlideEdge(self) -> int: + return self._slide_edge + + def setSlideEdge(self, slide_edge: int): + self._slide_edge = slide_edge + + def toString(self) -> str: + return f"Slide(edge={self._slide_edge}, duration={self._duration})" + + +# ═══════════════════════════════════════════════════════════════ +# Explode +# ═══════════════════════════════════════════════════════════════ + +class Explode(Transition): + """Explode — 爆炸/散开转场""" + + def __init__(self): + super().__init__() + + def toString(self) -> str: + return f"Explode(duration={self._duration})" + + +# ═══════════════════════════════════════════════════════════════ +# TransitionManager +# ═══════════════════════════════════════════════════════════════ + +class TransitionManager(Stub): + """TransitionManager — 转场管理器""" + + def __init__(self): + super().__init__() + + @staticmethod + def beginDelayedTransition(scene_root: Any, + transition: Optional[Transition] = None): + """开始延迟转场""" + pass + + @staticmethod + def endTransitions(scene_root: Any): + """结束转场""" + pass + + @staticmethod + def go(scene: Scene, transition: Optional[Transition] = None): + """执行场景转场""" + if scene: + scene.enter() + + def toString(self) -> str: + return "TransitionManager(desktop-stub)" + + def hashCode(self) -> int: + return hash("android.transition.TransitionManager") & 0x7FFFFFFF + + def equals(self, other) -> bool: + return isinstance(other, TransitionManager) + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Transition", + "TransitionManager", + "AutoTransition", + "Fade", + "ChangeBounds", + "ChangeTransform", + "Slide", + "Explode", + "TransitionSet", + "Scene", +] \ No newline at end of file diff --git a/android_pyapi/android/uwb.py b/android_pyapi/android/uwb.py new file mode 100644 index 0000000..5b9cde9 --- /dev/null +++ b/android_pyapi/android/uwb.py @@ -0,0 +1,420 @@ +""" +Android uwb 组件 — UwbManager, RangingResult, RangingParameters, UwbAddress 等 +方法签名完全对齐 Android SDK (UWB API) +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# UwbConstants +# ═══════════════════════════════════════════════════════════════ + +class UwbConstants(Stub): + """UwbConstants — UWB 常量""" + + STATUS_OK = 0 + STATUS_FAILURE = 1 + STATUS_INVALID_PARAMETERS = 2 + STATUS_SESSION_DOES_NOT_EXIST = 3 + STATUS_MAX_RANGING_ROUND_RETRY = 4 + STATUS_RANGING_TX_FAILED = 5 + STATUS_SESSION_STATE_CHANGE_EXCEPTION = 6 + STATUS_UNKNOWN_ERROR = 255 + + CONFIG_ID_0 = 0 + CONFIG_ID_1 = 1 + CONFIG_ID_2 = 2 + CONFIG_ID_3 = 3 + + MEASUREMENT_REPORT_TYPE_DEFERRED = 0 + MEASUREMENT_REPORT_TYPE_NTF = 1 + + RANGING_UPDATE_RATE_AUTO = 0 + RANGING_UPDATE_RATE_FREQUENCY_1_HZ = 1 + RANGING_UPDATE_RATE_FREQUENCY_2_HZ = 2 + RANGING_UPDATE_RATE_FREQUENCY_5_HZ = 5 + RANGING_UPDATE_RATE_FREQUENCY_10_HZ = 10 + RANGING_UPDATE_RATE_FREQUENCY_20_HZ = 20 + RANGING_UPDATE_RATE_FREQUENCY_100_HZ = 100 + RANGING_UPDATE_RATE_FREQUENCY_250_HZ = 250 + + +# ═══════════════════════════════════════════════════════════════ +# UwbAddress +# ═══════════════════════════════════════════════════════════════ + +class UwbAddress(Stub): + """UwbAddress — UWB 地址""" + + SHORT_MAC_ADDRESS = 0 + EXTENDED_MAC_ADDRESS = 1 + + def __init__(self, address: bytes = b"", type_: int = SHORT_MAC_ADDRESS): + super().__init__() + self._address = address + self._type = type_ + + def getAddress(self) -> bytes: + return self._address + + def getType(self) -> int: + return self._type + + def toString(self) -> str: + return f"UwbAddress(type={self._type}, addr={self._address.hex()})" + + +# ═══════════════════════════════════════════════════════════════ +# AngleOfArrival +# ═══════════════════════════════════════════════════════════════ + +class AngleOfArrival(Stub): + """AngleOfArrival — 到达角""" + + STATUS_SUCCESS = 0 + STATUS_FAILURE = 1 + + def __init__(self, azimuth: float = 0.0, elevation: float = 0.0, + azimuthConfidence: float = 0.0, elevationConfidence: float = 0.0, + status: int = STATUS_FAILURE): + super().__init__() + self._azimuth = azimuth + self._elevation = elevation + self._azimuth_confidence = azimuthConfidence + self._elevation_confidence = elevationConfidence + self._status = status + + def getAzimuth(self) -> float: + return self._azimuth + + def getElevation(self) -> float: + return self._elevation + + def getAzimuthConfidence(self) -> float: + return self._azimuth_confidence + + def getElevationConfidence(self) -> float: + return self._elevation_confidence + + +# ═══════════════════════════════════════════════════════════════ +# DistanceMeasurement +# ═══════════════════════════════════════════════════════════════ + +class DistanceMeasurement(Stub): + """DistanceMeasurement — 距离测量""" + + STATUS_SUCCESS = 0 + STATUS_FAILURE = 1 + + def __init__(self, meters: float = 0.0, confidence: float = 0.0, + standardDeviation: float = 0.0, status: int = STATUS_FAILURE): + super().__init__() + self._meters = meters + self._confidence = confidence + self._standard_deviation = standardDeviation + self._status = status + + def getMeters(self) -> float: + return self._meters + + def getConfidence(self) -> float: + return self._confidence + + def getStandardDeviation(self) -> float: + return self._standard_deviation + + +# ═══════════════════════════════════════════════════════════════ +# RangingResult +# ═══════════════════════════════════════════════════════════════ + +class RangingResult(Stub): + """RangingResult — 测距结果""" + + def __init__(self, uwbDevice: Any = None, remoteDevice: Any = None, + status: int = 0, nofRounds: int = 0, nofMeasurements: int = 0, + elapsedRealtimeNanos: int = 0, + distanceMeasurement: Optional[DistanceMeasurement] = None, + angleOfArrival: Optional[AngleOfArrival] = None, + rssi: float = -90.0): + super().__init__() + self._uwb_device = uwbDevice + self._remote_device = remoteDevice + self._status = status + self._nof_rounds = nofRounds + self._nof_measurements = nofMeasurements + self._elapsed_realtime_nanos = elapsedRealtimeNanos + self._distance_measurement = distanceMeasurement + self._angle_of_arrival = angleOfArrival + self._rssi = rssi + self._distance_reported = distanceMeasurement is not None + self._aoa_reported = angleOfArrival is not None + + def getUwbDevice(self) -> Any: + return self._uwb_device + + def getRemoteDevice(self) -> Any: + return self._remote_device + + def getStatus(self) -> int: + return self._status + + def getNofRounds(self) -> int: + return self._nof_rounds + + def getNofMeasurements(self) -> int: + return self._nof_measurements + + def getElapsedRealtimeNanos(self) -> int: + return self._elapsed_realtime_nanos + + def getDistanceMeasurement(self) -> Optional[DistanceMeasurement]: + return self._distance_measurement + + def getAngleOfArrival(self) -> Optional[AngleOfArrival]: + return self._angle_of_arrival + + def getAngleOfArrivalAzimuth(self) -> float: + if self._angle_of_arrival: + return self._angle_of_arrival.getAzimuth() + return 0.0 + + def getAngleOfArrivalElevation(self) -> float: + if self._angle_of_arrival: + return self._angle_of_arrival.getElevation() + return 0.0 + + def getAngleOfArrivalAzimuthConfidence(self) -> float: + if self._angle_of_arrival: + return self._angle_of_arrival.getAzimuthConfidence() + return 0.0 + + def getAngleOfArrivalElevationConfidence(self) -> float: + if self._angle_of_arrival: + return self._angle_of_arrival.getElevationConfidence() + return 0.0 + + def getRssi(self) -> float: + return self._rssi + + def isDistanceMeasurementReported(self) -> bool: + return self._distance_reported + + def isAngleOfArrivalReported(self) -> bool: + return self._aoa_reported + + +# ═══════════════════════════════════════════════════════════════ +# RangingParameters +# ═══════════════════════════════════════════════════════════════ + +class RangingParameters(Stub): + """RangingParameters — 测距参数""" + + class Builder(Stub): + """RangingParameters.Builder — 测距参数构建器""" + + def __init__(self): + super().__init__() + self._uwb_config_id = 0 + self._session_id = 0 + self._sub_session_id = 0 + self._ranging_update_rate = 1 + self._slot_duration = 0 + self._measurement_report_type = 0 + self._number_of_controlee = 1 + self._destination_addresses: List[UwbAddress] = [] + self._device_configs: List[Any] = [] + self._vendor_specific_data: List[Any] = [] + + def setUwbConfigId(self, configId: int) -> "RangingParameters.Builder": + self._uwb_config_id = configId + return self + + def setSessionId(self, sessionId: int) -> "RangingParameters.Builder": + self._session_id = sessionId + return self + + def setSubSessionId(self, subSessionId: int) -> "RangingParameters.Builder": + self._sub_session_id = subSessionId + return self + + def setRangingUpdateRate(self, rate: int) -> "RangingParameters.Builder": + self._ranging_update_rate = rate + return self + + def setSlotDuration(self, duration: int) -> "RangingParameters.Builder": + self._slot_duration = duration + return self + + def setMeasurementReportType(self, reportType: int) -> "RangingParameters.Builder": + self._measurement_report_type = reportType + return self + + def setNumberOfControlee(self, count: int) -> "RangingParameters.Builder": + self._number_of_controlee = count + return self + + def setDestinationAddresses(self, addrs: List[UwbAddress]) -> "RangingParameters.Builder": + self._destination_addresses = addrs + return self + + def setDeviceConfigs(self, configs: List[Any]) -> "RangingParameters.Builder": + self._device_configs = configs + return self + + def setVendorSpecificData(self, data: List[Any]) -> "RangingParameters.Builder": + self._vendor_specific_data = data + return self + + def build(self) -> "RangingParameters": + rp = RangingParameters() + rp._uwb_config_id = self._uwb_config_id + rp._session_id = self._session_id + rp._sub_session_id = self._sub_session_id + rp._ranging_update_rate = self._ranging_update_rate + rp._slot_duration = self._slot_duration + rp._measurement_report_type = self._measurement_report_type + rp._number_of_controlee = self._number_of_controlee + rp._destination_addresses = self._destination_addresses + rp._device_configs = self._device_configs + rp._vendor_specific_data = self._vendor_specific_data + return rp + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# UwbConfiguration / RangingConfig (stubs) +# ═══════════════════════════════════════════════════════════════ + +class UwbConfiguration(Stub): + """UwbConfiguration — UWB 配置""" + + def __init__(self): + super().__init__() + + +class RangingConfig(Stub): + """RangingConfig — 测距配置""" + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# RangingSession +# ═══════════════════════════════════════════════════════════════ + +class RangingSession(Stub): + """RangingSession — 测距会话""" + + def __init__(self): + super().__init__() + self._open = False + + def open(self): + self._open = True + + def close(self): + self._open = False + + def reconfigure(self, params: RangingParameters): + pass + + +# ═══════════════════════════════════════════════════════════════ +# UwbManager +# ═══════════════════════════════════════════════════════════════ + +class UwbManager(Stub): + """UwbManager — UWB 管理器""" + + # ═══════════════════════════════════════════════════════════ + # UwbCallback + # ═══════════════════════════════════════════════════════════ + + class UwbCallback(Stub): + """UwbManager.UwbCallback — UWB 回调""" + + def onRangingResult(self, sessionId: int, result: RangingResult): + pass + + def onRangingFailure(self, sessionId: int, reason: int): + pass + + def onControleeResult(self, sessionId: int, status: int): + pass + + _instance: Optional["UwbManager"] = None + + def __init__(self): + super().__init__() + + @staticmethod + def getInstance(context: Any = None) -> "UwbManager": + if UwbManager._instance is None: + UwbManager._instance = UwbManager() + return UwbManager._instance + + def open(self, sessionId: int) -> RangingSession: + session = RangingSession() + session.open() + return session + + def close(self, sessionId: int): + pass + + def range(self, sessionId: int, params: RangingParameters, + callback: UwbCallback): + if callback: + result = RangingResult( + status=UwbConstants.STATUS_OK, + distanceMeasurement=DistanceMeasurement( + meters=1.5, confidence=0.8, + standardDeviation=0.1, status=DistanceMeasurement.STATUS_SUCCESS + ), + rssi=-65.0 + ) + callback.onRangingResult(sessionId, result) + + def stopRange(self, sessionId: int): + pass + + def addControlee(self, sessionId: int, address: UwbAddress): + pass + + def removeControlee(self, sessionId: int, address: UwbAddress): + pass + + def setMeasurementData(self, sessionId: int, data: bytes): + pass + + def sendMeasurementData(self, sessionId: int, data: bytes): + pass + + def getUwbDevice(self) -> Any: + return None + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "UwbManager", + "UwbConstants", + "UwbAddress", + "AngleOfArrival", + "DistanceMeasurement", + "RangingResult", + "RangingParameters", + "UwbConfiguration", + "RangingConfig", + "RangingSession", +] \ No newline at end of file diff --git a/android_pyapi/android/view_autofill.py b/android_pyapi/android/view_autofill.py new file mode 100644 index 0000000..b721ea9 --- /dev/null +++ b/android_pyapi/android/view_autofill.py @@ -0,0 +1,313 @@ +""" +Android view.autofill 组件 — AutofillManager, AutofillValue, AutofillId 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# AutofillManager +# ═══════════════════════════════════════════════════════════════ + +class AutofillManager(Stub): + """AutofillManager — 自动填充管理器""" + + # ═══════════════════════════════════════════════════════════ + # AutofillCallback + # ═══════════════════════════════════════════════════════════ + + class AutofillCallback(Stub): + """AutofillManager.AutofillCallback — 自动填充回调""" + + def onAutofillEvent(self, view: Any, event: int): + pass + + def onAutofillUnavailableEvent(self, event: int): + pass + + def onAuthenticationShowing(self): + pass + + def __init__(self): + super().__init__() + self._enabled = False + self._callbacks: List[AutofillManager.AutofillCallback] = [] + + def isEnabled(self) -> bool: + return self._enabled + + def hasEnabledAutofillServices(self) -> bool: + return False + + def isAutofillSupported(self) -> bool: + return False + + def disableAutofillServices(self): + self._enabled = False + + def notifyValueChanged(self, view: Any): + pass + + def commit(self): + pass + + def cancel(self): + pass + + def requestAutofill(self, view: Any): + pass + + def getAutofillServiceComponentName(self) -> Any: + return None + + def getAutofillServiceLabel(self) -> str: + return "" + + def getAvailableFieldClassificationAlgorithms(self) -> List[str]: + return [] + + def getDefaultFieldClassificationAlgorithm(self) -> Optional[str]: + return None + + def getFillEventHistory(self) -> Optional["FillEventHistory"]: + return None + + def onAuthenticationResult(self, authenticationResult: Any): + pass + + def onAuthenticationFailed(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# AutofillValue +# ═══════════════════════════════════════════════════════════════ + +class AutofillValue(Stub): + """AutofillValue — 自动填充值""" + + def __init__(self): + super().__init__() + self._text_value: Optional[str] = None + self._date_value: Optional[int] = None + self._toggle_value: Optional[bool] = None + self._list_value: Optional[int] = None + self._is_text = False + self._is_date = False + self._is_toggle = False + self._is_list = False + + @staticmethod + def forText(value: str) -> "AutofillValue": + v = AutofillValue() + v._text_value = value + v._is_text = True + return v + + @staticmethod + def forDate(value: int) -> "AutofillValue": + v = AutofillValue() + v._date_value = value + v._is_date = True + return v + + @staticmethod + def forToggle(value: bool) -> "AutofillValue": + v = AutofillValue() + v._toggle_value = value + v._is_toggle = True + return v + + @staticmethod + def forList(value: int) -> "AutofillValue": + v = AutofillValue() + v._list_value = value + v._is_list = True + return v + + def getTextValue(self) -> Optional[str]: + return self._text_value + + def getDateValue(self) -> Optional[int]: + return self._date_value + + def getToggleValue(self) -> Optional[bool]: + return self._toggle_value + + def getListValue(self) -> Optional[int]: + return self._list_value + + def isText(self) -> bool: + return self._is_text + + def isDate(self) -> bool: + return self._is_date + + def isToggle(self) -> bool: + return self._is_toggle + + def isList(self) -> bool: + return self._is_list + + def equals(self, other: Any) -> bool: + if not isinstance(other, AutofillValue): + return False + return (self._text_value == other._text_value + and self._date_value == other._date_value + and self._toggle_value == other._toggle_value + and self._list_value == other._list_value) + + def hashCode(self) -> int: + return hash((self._text_value, self._date_value, + self._toggle_value, self._list_value)) & 0x7FFFFFFF + + def toString(self) -> str: + if self._is_text: + return f"AutofillValue(text={self._text_value})" + if self._is_date: + return f"AutofillValue(date={self._date_value})" + if self._is_toggle: + return f"AutofillValue(toggle={self._toggle_value})" + if self._is_list: + return f"AutofillValue(list={self._list_value})" + return "AutofillValue(empty)" + + +# ═══════════════════════════════════════════════════════════════ +# AutofillId +# ═══════════════════════════════════════════════════════════════ + +class AutofillId(Stub): + """AutofillId — 自动填充 ID""" + + FLAG_AUTOFILL_SESSION_SUPPORTS_INDEXING = 1 + FLAG_AUTOFILL_SESSION_SUPPORTS_AUGMENTED_AUTOFILL = 2 + + def __init__(self, viewId: int = 0, sessionId: int = 0, + virtualChildId: int = -1, virtualIntId: int = -1): + super().__init__() + self._view_id = viewId + self._session_id = sessionId + self._virtual_child_id = virtualChildId + self._virtual_int_id = virtualIntId + + def getViewId(self) -> int: + return self._view_id + + def getSessionId(self) -> int: + return self._session_id + + def getVirtualChildId(self) -> int: + return self._virtual_child_id + + def getVirtualIntId(self) -> int: + return self._virtual_int_id + + def equals(self, other: Any) -> bool: + if not isinstance(other, AutofillId): + return False + return (self._view_id == other._view_id + and self._session_id == other._session_id + and self._virtual_child_id == other._virtual_child_id) + + def hashCode(self) -> int: + return hash((self._view_id, self._session_id, + self._virtual_child_id)) & 0x7FFFFFFF + + def toString(self) -> str: + return f"AutofillId(viewId={self._view_id}, sessionId={self._session_id})" + + +# ═══════════════════════════════════════════════════════════════ +# FillEventHistory +# ═══════════════════════════════════════════════════════════════ + +class FillEventHistory(Stub): + """FillEventHistory — 填充事件历史""" + + def __init__(self): + super().__init__() + self._events: List[Any] = [] + self._client_state: Any = None + + def getEvents(self) -> List[Any]: + return self._events + + def getClientState(self) -> Any: + return self._client_state + + +# ═══════════════════════════════════════════════════════════════ +# FillContext +# ═══════════════════════════════════════════════════════════════ + +class FillContext(Stub): + """FillContext — 填充上下文""" + + def __init__(self, requestId: int = 0, autofillId: Optional[AutofillId] = None, + focusedId: Optional[AutofillId] = None, + focusedVirtualId: int = -1, extras: Any = None, + autofillValue: Optional[AutofillValue] = None, + visibility: Optional[int] = None, + options: Optional[List[Any]] = None): + super().__init__() + self._request_id = requestId + self._autofill_id = autofillId + self._focused_id = focusedId + self._focused_virtual_id = focusedVirtualId + self._extras = extras + self._autofill_value = autofillValue + self._visibility = visibility + self._options = options + + def getRequestId(self) -> int: + return self._request_id + + def getAutofillId(self) -> Optional[AutofillId]: + return self._autofill_id + + def getFocusedId(self) -> Optional[AutofillId]: + return self._focused_id + + def getFocusedVirtualId(self) -> int: + return self._focused_virtual_id + + def getExtras(self) -> Any: + return self._extras + + def getAutofillValue(self) -> Optional[AutofillValue]: + return self._autofill_value + + def getVisibility(self) -> Optional[int]: + return self._visibility + + def getOptions(self) -> Optional[List[Any]]: + return self._options + + +# ═══════════════════════════════════════════════════════════════ +# AugmentedAutofillManager (stub) +# ═══════════════════════════════════════════════════════════════ + +class AugmentedAutofillManager(Stub): + """AugmentedAutofillManager — 增强自动填充管理器""" + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "AutofillManager", + "AutofillValue", + "AutofillId", + "FillEventHistory", + "FillContext", + "AugmentedAutofillManager", +] \ No newline at end of file diff --git a/android_pyapi/android/view_textclassifier.py b/android_pyapi/android/view_textclassifier.py new file mode 100644 index 0000000..dbc19e2 --- /dev/null +++ b/android_pyapi/android/view_textclassifier.py @@ -0,0 +1,426 @@ +""" +Android view.textclassifier 组件 — TextClassifier, TextClassification, TextLinks, TextSelection 等 +方法签名完全对齐 Android SDK +""" + +import re +from typing import Any, Callable, Dict, List, Optional, Set, Tuple + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# EntityConfig +# ═══════════════════════════════════════════════════════════════ + +class EntityConfig(Stub): + """EntityConfig — 实体配置""" + + class Builder(Stub): + """EntityConfig.Builder — 实体配置构建器""" + + def __init__(self): + super().__init__() + self._include_types: Optional[Set[str]] = None + self._exclude_types: Optional[Set[str]] = None + self._include_from_textclassifier = True + + def includeTypesFromTextClassifier(self) -> "EntityConfig.Builder": + self._include_from_textclassifier = True + return self + + def excludeTypes(self, varargs: List[str]) -> "EntityConfig.Builder": + self._exclude_types = set(varargs) + return self + + def includeTypes(self, varargs: List[str]) -> "EntityConfig.Builder": + self._include_types = set(varargs) + return self + + def build(self) -> "EntityConfig": + config = EntityConfig() + config._include_types = self._include_types + config._exclude_types = self._exclude_types + config._include_from_textclassifier = self._include_from_textclassifier + return config + + def getEntityTypes(self) -> Optional[List[str]]: + if self._include_types: + return list(self._include_types) + return None + + def __init__(self): + super().__init__() + self._include_types: Optional[Set[str]] = None + self._exclude_types: Optional[Set[str]] = None + self._include_from_textclassifier = True + + def getEntityTypes(self) -> Optional[List[str]]: + if self._include_types: + return list(self._include_types) + return None + + +# ═══════════════════════════════════════════════════════════════ +# TextClassifier +# ═══════════════════════════════════════════════════════════════ + +class TextClassifier(Stub): + """TextClassifier — 文本分类器""" + + TYPE_UNKNOWN = "unknown" + TYPE_ADDRESS = "address" + TYPE_DATE = "date" + TYPE_DATE_TIME = "datetime" + TYPE_DURATION = "duration" + TYPE_EMAIL = "email" + TYPE_FLIGHT_NUMBER = "flight" + TYPE_IDENTIFIER = "identifier" + TYPE_LINK = "link" + TYPE_LIST = "list" + TYPE_LOCATION = "location" + TYPE_MEASUREMENT = "measurement" + TYPE_MONEY = "money" + TYPE_OTHER = "other" + TYPE_PERIODIC = "periodic" + TYPE_PHONE = "phone" + TYPE_PRODUCT = "product" + TYPE_RECIPE = "recipe" + TYPE_TRACK_AIRLINE = "track_airline" + TYPE_TRACK_FLIGHT = "track_flight" + TYPE_TRACK_FOOD = "track_food" + TYPE_TRACK_SPORT = "track_sport" + TYPE_TRACK_WEATHER = "track_weather" + TYPE_URL = "url" + TYPE_VEHICLE = "vehicle" + + WIDGET_TYPE_CLIPBOARD = "clipboard" + WIDGET_TYPE_CUT_COPY = "cutcopy" + WIDGET_TYPE_EDIT_TEXT = "edittext" + WIDGET_TYPE_LINKIFY = "linkify" + WIDGET_TYPE_NOTIFICATION = "notification" + WIDGET_TYPE_OTHER = "other" + WIDGET_TYPE_SELECTION = "selection" + WIDGET_TYPE_TEXT = "text" + WIDGET_TYPE_UNSELECTED_TEXT = "unselected_text" + + # ═══════════════════════════════════════════════════════════ + # Request + # ═══════════════════════════════════════════════════════════ + + class Request(Stub): + """TextClassifier.Request — 分类请求""" + + def __init__(self): + super().__init__() + self._calling_package_name: Optional[str] = None + self._text: str = "" + self._start_index: int = 0 + self._end_index: int = 0 + self._extras: Any = None + self._default_locales: Any = None + self._dark_launch_allowed = False + + def getCallingPackageName(self) -> Optional[str]: + return self._calling_package_name + + def getText(self) -> str: + return self._text + + def getStartIndex(self) -> int: + return self._start_index + + def getEndIndex(self) -> int: + return self._end_index + + def getExtras(self) -> Any: + return self._extras + + def getDefaultLocales(self) -> Any: + return self._default_locales + + def isDarkLaunchAllowed(self) -> bool: + return self._dark_launch_allowed + + def __init__(self): + super().__init__() + + def classifyText(self, request: Request) -> "TextClassification": + text = request.getText() + start = request.getStartIndex() + end = request.getEndIndex() + if end > len(text): + end = len(text) + selected = text[start:end] if start < end else text + + entity_type = TextClassifier.TYPE_UNKNOWN + confidence = 0.0 + + # Basic regex-based classification + if re.match(r'^https?://', selected): + entity_type = TextClassifier.TYPE_URL + confidence = 0.95 + elif re.match(r'^[\w.+-]+@[\w-]+\.[\w.-]+$', selected): + entity_type = TextClassifier.TYPE_EMAIL + confidence = 0.95 + elif re.match(r'^\+?[\d\s\-\(\)]{7,15}$', selected): + entity_type = TextClassifier.TYPE_PHONE + confidence = 0.85 + elif re.match(r'^\d{1,4}[-/]\d{1,2}[-/]\d{1,4}$', selected): + entity_type = TextClassifier.TYPE_DATE + confidence = 0.80 + elif re.match(r'^\d{1,2}:\d{2}(\s?[AP]M)?$', selected, re.IGNORECASE): + entity_type = TextClassifier.TYPE_DATE_TIME + confidence = 0.80 + + tc = TextClassification() + tc._text = selected + tc._entity_type = entity_type + tc._confidence_score = confidence + tc._entity_count = 1 + return tc + + def suggestConversationActions(self, request: Request) -> List["ConversationAction"]: + return [] + + def detectLanguage(self, text: str) -> "TextClassification": + tc = TextClassification() + tc._text = text + tc._entity_type = TextClassifier.TYPE_UNKNOWN + return tc + + def onSelectionEvent(self, event: Any): + pass + + def onTextChanged(self, text: Any): + pass + + def onTextSelectionAction(self, action: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# TextClassification +# ═══════════════════════════════════════════════════════════════ + +class TextClassification(Stub): + """TextClassification — 文本分类结果""" + + def __init__(self): + super().__init__() + self._text: str = "" + self._entity_type: str = TextClassifier.TYPE_UNKNOWN + self._confidence_score: float = 0.0 + self._entity_count: int = 0 + self._icon: Any = None + self._label: str = "" + self._actions: List[Any] = [] + self._extras: Any = None + self._id: Optional[str] = None + + def getText(self) -> str: + return self._text + + def getEntityCount(self) -> int: + return self._entity_count + + def getEntity(self, index: int) -> Optional[str]: + if index == 0: + return self._entity_type + return None + + def getEntityType(self) -> str: + return self._entity_type + + def getConfidenceScore(self, entityType: str) -> float: + if entityType == self._entity_type: + return self._confidence_score + return 0.0 + + def getIcon(self) -> Any: + return self._icon + + def getLabel(self) -> str: + return self._label + + def getActions(self) -> List[Any]: + return self._actions + + def getExtras(self) -> Any: + return self._extras + + def getId(self) -> Optional[str]: + return self._id + + def toString(self) -> str: + return f"TextClassification(type={self._entity_type}," \ + f" confidence={self._confidence_score:.2f})" + + +# ═══════════════════════════════════════════════════════════════ +# TextLinks +# ═══════════════════════════════════════════════════════════════ + +class TextLinks(Stub): + """TextLinks — 文本链接""" + + def __init__(self): + super().__init__() + self._links: List[Any] = [] + self._extras: Any = None + + def getLinks(self) -> List[Any]: + return self._links + + def getEntityCount(self) -> int: + return len(self._links) + + def getEntity(self, index: int) -> Optional[str]: + if 0 <= index < len(self._links): + return self._links[index] + return None + + def getEntityType(self) -> str: + return TextClassifier.TYPE_LINK + + def getConfidenceScore(self, entityType: str) -> float: + return 0.0 + + def getExtras(self) -> Any: + return self._extras + + +# ═══════════════════════════════════════════════════════════════ +# TextSelection +# ═══════════════════════════════════════════════════════════════ + +class TextSelection(Stub): + """TextSelection — 文本选择结果""" + + def __init__(self): + super().__init__() + self._start_index: int = 0 + self._end_index: int = 0 + self._entity_type: str = TextClassifier.TYPE_UNKNOWN + self._text: str = "" + self._entity_count: int = 0 + self._confidence_score: float = 0.0 + self._extras: Any = None + self._id: Optional[str] = None + + def getSelectionStartIndex(self) -> int: + return self._start_index + + def getSelectionEndIndex(self) -> int: + return self._end_index + + def getEntityType(self) -> str: + return self._entity_type + + def getText(self) -> str: + return self._text + + def getEntityCount(self) -> int: + return self._entity_count + + def getEntity(self, index: int) -> Optional[str]: + if index == 0: + return self._entity_type + return None + + def getConfidenceScore(self, entityType: str) -> float: + if entityType == self._entity_type: + return self._confidence_score + return 0.0 + + def getExtras(self) -> Any: + return self._extras + + def getId(self) -> Optional[str]: + return self._id + + +# ═══════════════════════════════════════════════════════════════ +# ConversationAction +# ═══════════════════════════════════════════════════════════════ + +class ConversationAction(Stub): + """ConversationAction — 对话操作""" + + TYPE_CALL = "call" + TYPE_CREATE_REMINDER = "create_reminder" + TYPE_OPEN_URL = "open_url" + TYPE_SEND_EMAIL = "send_email" + TYPE_SEND_SMS = "send_sms" + TYPE_SHARE_LOCATION = "share_location" + TYPE_TEXT_REPLY = "text_reply" + TYPE_VIEW_CALENDAR = "view_calendar" + TYPE_VIEW_MAP = "view_map" + + def __init__(self, text: str = "", confidenceScore: float = 0.0, + action: str = "", extras: Any = None, + icon: Any = None, label: str = ""): + super().__init__() + self._text = text + self._confidence_score = confidenceScore + self._action = action + self._extras = extras + self._icon = icon + self._label = label + + def getText(self) -> str: + return self._text + + def getConfidenceScore(self) -> float: + return self._confidence_score + + def getAction(self) -> str: + return self._action + + def getExtras(self) -> Any: + return self._extras + + def getIcon(self) -> Any: + return self._icon + + def getLabel(self) -> str: + return self._label + + +# ═══════════════════════════════════════════════════════════════ +# TextClassificationContext +# ═══════════════════════════════════════════════════════════════ + +class TextClassificationContext(Stub): + """TextClassificationContext — 文本分类上下文""" + + def __init__(self, packageName: str = "", widgetType: str = "", + widgetVersion: str = ""): + super().__init__() + self._package_name = packageName + self._widget_type = widgetType + self._widget_version = widgetVersion + + def getPackageName(self) -> str: + return self._package_name + + def getWidgetType(self) -> str: + return self._widget_type + + def getWidgetVersion(self) -> str: + return self._widget_version + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "TextClassifier", + "TextClassification", + "TextLinks", + "TextSelection", + "ConversationAction", + "TextClassificationContext", + "EntityConfig", +] \ No newline at end of file diff --git a/android_pyapi/android/view_translation.py b/android_pyapi/android/view_translation.py new file mode 100644 index 0000000..2a0c6fd --- /dev/null +++ b/android_pyapi/android/view_translation.py @@ -0,0 +1,435 @@ +""" +Android view.translation 组件 — TranslationManager, TranslationRequest, TranslationResponse 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Callable, Dict, List, Optional + +from .._core import Stub, AndroidOnlyError + + +# ═══════════════════════════════════════════════════════════════ +# TranslationSpec +# ═══════════════════════════════════════════════════════════════ + +class TranslationSpec(Stub): + """TranslationSpec — 翻译规格""" + + DATA_TYPE_TEXT = 0 + DATA_TYPE_UI = 1 + + def __init__(self): + super().__init__() + self._locale: Optional[str] = None + self._data_type = TranslationSpec.DATA_TYPE_TEXT + + def setLocale(self, locale: str) -> "TranslationSpec": + self._locale = locale + return self + + def setDataType(self, dataType: int) -> "TranslationSpec": + self._data_type = dataType + return self + + def getLocale(self) -> Optional[str]: + return self._locale + + def getDataType(self) -> int: + return self._data_type + + +# ═══════════════════════════════════════════════════════════════ +# TranslationContext +# ═══════════════════════════════════════════════════════════════ + +class TranslationContext(Stub): + """TranslationContext — 翻译上下文""" + + def __init__(self, sourceLocale: str = "", targetLocale: str = "", + translationFlags: int = 0): + super().__init__() + self._source_locale = sourceLocale + self._target_locale = targetLocale + self._translation_flags = translationFlags + + def getSourceLocale(self) -> str: + return self._source_locale + + def getTargetLocale(self) -> str: + return self._target_locale + + def getTranslationFlags(self) -> int: + return self._translation_flags + + +# ═══════════════════════════════════════════════════════════════ +# TranslationCapability +# ═══════════════════════════════════════════════════════════════ + +class TranslationCapability(Stub): + """TranslationCapability — 翻译能力""" + + STATE_AVAILABLE = 0 + STATE_UNAVAILABLE = 1 + STATE_CAPABLE = 2 + STATE_NOT_CAPABLE = 3 + STATE_DOWNLOADING = 4 + STATE_NOT_ALLOWED = 5 + + def __init__(self, state: int = STATE_UNAVAILABLE, + sourceLocale: str = "", targetLocale: str = "", + uiTranslationEnabled: bool = False, + textTranslationEnabled: bool = False, + translationService: str = "desktop"): + super().__init__() + self._state = state + self._source_locale = sourceLocale + self._target_locale = targetLocale + self._ui_translation_enabled = uiTranslationEnabled + self._text_translation_enabled = textTranslationEnabled + self._translation_service = translationService + + def getState(self) -> int: + return self._state + + def getSourceLocale(self) -> str: + return self._source_locale + + def getTargetLocale(self) -> str: + return self._target_locale + + def isUiTranslationEnabled(self) -> bool: + return self._ui_translation_enabled + + def isTextTranslationEnabled(self) -> bool: + return self._text_translation_enabled + + def getTranslationService(self) -> str: + return self._translation_service + + +# ═══════════════════════════════════════════════════════════════ +# TranslationResult +# ═══════════════════════════════════════════════════════════════ + +class TranslationResult(Stub): + """TranslationResult — 翻译结果""" + + def __init__(self, translatedText: str = "", sourceText: str = "", + translationStatus: int = 0): + super().__init__() + self._translated_text = translatedText + self._source_text = sourceText + self._translation_status = translationStatus + + def getTranslatedText(self) -> str: + return self._translated_text + + def getSourceText(self) -> str: + return self._source_text + + def getTranslationStatus(self) -> int: + return self._translation_status + + +# ═══════════════════════════════════════════════════════════════ +# TranslationRequest +# ═══════════════════════════════════════════════════════════════ + +class TranslationRequest(Stub): + """TranslationRequest — 翻译请求""" + + class Builder(Stub): + """TranslationRequest.Builder — 翻译请求构建器""" + + def __init__(self): + super().__init__() + self._source_text: str = "" + self._source_locale: str = "" + self._target_locale: str = "" + self._translation_flags: int = 0 + self._ui_translation_result: Any = None + self._request_id: int = 0 + self._translation_config: Any = None + + def setSourceText(self, text: str) -> "TranslationRequest.Builder": + self._source_text = text + return self + + def setSourceLocale(self, locale: str) -> "TranslationRequest.Builder": + self._source_locale = locale + return self + + def setTargetLocale(self, locale: str) -> "TranslationRequest.Builder": + self._target_locale = locale + return self + + def setTranslationFlags(self, flags: int) -> "TranslationRequest.Builder": + self._translation_flags = flags + return self + + def setUiTranslationResult(self, result: Any) -> "TranslationRequest.Builder": + self._ui_translation_result = result + return self + + def setRequestId(self, id: int) -> "TranslationRequest.Builder": + self._request_id = id + return self + + def setTranslationConfig(self, config: Any) -> "TranslationRequest.Builder": + self._translation_config = config + return self + + def build(self) -> "TranslationRequest": + req = TranslationRequest() + req._source_text = self._source_text + req._source_locale = self._source_locale + req._target_locale = self._target_locale + req._translation_flags = self._translation_flags + req._ui_translation_result = self._ui_translation_result + req._request_id = self._request_id + req._translation_config = self._translation_config + return req + + def __init__(self): + super().__init__() + self._source_text: str = "" + self._source_locale: str = "" + self._target_locale: str = "" + self._request_id: int = 0 + self._translation_flags: int = 0 + self._ui_translation_result: Any = None + self._translation_config: Any = None + + def getSourceText(self) -> str: + return self._source_text + + def getSourceLocale(self) -> str: + return self._source_locale + + def getTargetLocale(self) -> str: + return self._target_locale + + def getRequestId(self) -> int: + return self._request_id + + def getTranslationFlags(self) -> int: + return self._translation_flags + + +# ═══════════════════════════════════════════════════════════════ +# TranslationResponse +# ═══════════════════════════════════════════════════════════════ + +class TranslationResponse(Stub): + """TranslationResponse — 翻译响应""" + + class Builder(Stub): + """TranslationResponse.Builder — 翻译响应构建器""" + + def __init__(self): + super().__init__() + self._translation_result: Optional[TranslationResult] = None + self._status: int = 0 + + def setTranslationResult(self, result: TranslationResult) -> "TranslationResponse.Builder": + self._translation_result = result + return self + + def setStatus(self, status: int) -> "TranslationResponse.Builder": + self._status = status + return self + + def build(self) -> "TranslationResponse": + resp = TranslationResponse() + resp._translation_result = self._translation_result + resp._status = self._status + return resp + + def __init__(self): + super().__init__() + self._translation_result: Optional[TranslationResult] = None + self._status: int = 0 + + def getTranslationResult(self) -> Optional[TranslationResult]: + return self._translation_result + + def getStatus(self) -> int: + return self._status + + +# ═══════════════════════════════════════════════════════════════ +# Translator +# ═══════════════════════════════════════════════════════════════ + +class Translator(Stub): + """Translator — 翻译器""" + + def __init__(self): + super().__init__() + self._supported_locales: List[str] = ["en", "zh", "ja", "ko", "fr", "de", "es"] + + def translate(self, text: str, sourceLocale: str, + targetLocale: str) -> str: + # Basic stub: return original text with a prefix + if sourceLocale == targetLocale: + return text + return f"[{sourceLocale}->{targetLocale}] {text}" + + def destroy(self): + pass + + def getSupportedLocales(self) -> List[str]: + return list(self._supported_locales) + + +# ═══════════════════════════════════════════════════════════════ +# TranslationManager +# ═══════════════════════════════════════════════════════════════ + +class TranslationManager(Stub): + """TranslationManager — 翻译管理器""" + + EXTRA_CAPABILITIES_FROM_FRAMEWORK = "android.view.translation.extra.CAPABILITIES_FROM_FRAMEWORK" + EXTRA_TRANSLATION_REQUEST_RESULT = "android.view.translation.extra.TRANSLATION_REQUEST_RESULT" + + def __init__(self): + super().__init__() + self._translators: Dict[str, Translator] = {} + + def startTranslation(self, request: TranslationRequest) -> TranslationResponse: + builder = TranslationResponse.Builder() + try: + source = request.getSourceLocale() + target = request.getTargetLocale() + text = request.getSourceText() + + translator = self._get_or_create_translator() + translated = translator.translate(text, source, target) + + result = TranslationResult( + translatedText=translated, + sourceText=text, + translationStatus=0 + ) + builder.setTranslationResult(result) + builder.setStatus(0) + except Exception as e: + builder.setStatus(1) + return builder.build() + + def createOnDeviceTranslation(self) -> Translator: + translator = Translator() + return translator + + def getTranslationCapabilities(self, sourceLocale: str, + targetLocale: str) -> List[TranslationCapability]: + return [ + TranslationCapability( + state=TranslationCapability.STATE_AVAILABLE, + sourceLocale=sourceLocale, + targetLocale=targetLocale, + textTranslationEnabled=True + ) + ] + + def onTranslationRequest(self, request: TranslationRequest): + pass + + def isTranslationSupported(self) -> bool: + return True + + def isOnDeviceTranslationSupported(self) -> bool: + return True + + def isULTRADeviceSupported(self) -> bool: + return False + + def _get_or_create_translator(self) -> Translator: + return Translator() + + +# ═══════════════════════════════════════════════════════════════ +# ViewTranslation / ViewTranslationRequest / ViewTranslationResponse (stubs) +# ═══════════════════════════════════════════════════════════════ + +class ViewTranslation(Stub): + """ViewTranslation — 视图翻译""" + + def __init__(self, autofillId: Any = None, value: str = "", + translationResult: Optional[TranslationResult] = None): + super().__init__() + self._autofill_id = autofillId + self._value = value + self._translation_result = translationResult + + def getAutofillId(self) -> Any: + return self._autofill_id + + def getValue(self) -> str: + return self._value + + def getTranslationResult(self) -> Optional[TranslationResult]: + return self._translation_result + + def setTranslationResult(self, result: TranslationResult): + self._translation_result = result + + +class ViewTranslationRequest(Stub): + """ViewTranslationRequest — 视图翻译请求""" + + def __init__(self, autofillId: Any = None, value: str = "", + requestId: int = 0): + super().__init__() + self._autofill_id = autofillId + self._value = value + self._request_id = requestId + + def getAutofillId(self) -> Any: + return self._autofill_id + + def getValue(self) -> str: + return self._value + + def getRequestId(self) -> int: + return self._request_id + + +class ViewTranslationResponse(Stub): + """ViewTranslationResponse — 视图翻译响应""" + + def __init__(self, autofillId: Any = None, value: str = "", + extras: Any = None): + super().__init__() + self._autofill_id = autofillId + self._value = value + self._extras = extras + + def getAutofillId(self) -> Any: + return self._autofill_id + + def getValue(self) -> str: + return self._value + + def getExtras(self) -> Any: + return self._extras + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "TranslationManager", + "TranslationRequest", + "TranslationResponse", + "TranslationResult", + "TranslationCapability", + "Translator", + "TranslationContext", + "TranslationSpec", + "ViewTranslation", + "ViewTranslationRequest", + "ViewTranslationResponse", +] \ No newline at end of file