commit bfe27fdc08346cc6a135f2c5880068f888703414 Author: shuiyu1123 Date: Wed Jun 24 11:16:16 2026 +0000 feat: Android-pyApi v1.0 - 全面翻译 Android SDK API 为 Python 52 个模块覆盖 Android + Java + org.json 标准库 diff --git a/android_pyapi/__init__.py b/android_pyapi/__init__.py new file mode 100644 index 0000000..7101a52 --- /dev/null +++ b/android_pyapi/__init__.py @@ -0,0 +1,258 @@ +""" +Android-pyApi — Android API 全面 Python 翻译 +============================================== +方法签名完全对齐 Android SDK,跨平台运行于 Windows/macOS/Linux。 + +用法: + from android_pyapi import Intent, Context, Uri, Bitmap, View + from android_pyapi.java.io import File, FileInputStream + from android_pyapi.java.util import ArrayList, HashMap + from android_pyapi.org.json import JSONObject, JSONArray + +设计原则: + 1. 方法名、参数名、返回类型完全对齐 Android SDK + 2. 能桌面实现的 → 真实实现 (基于 PIL/sqlite3/socket 等) + 3. 无法桌面实现的 → 返回合理默认值或抛出 AndroidOnlyError + 4. 所有常量值与 Android SDK 一致 +""" + +from ._core import ( + AndroidOnlyError, + Stub, + android_context, + get_context, + _sdcard_root, + _app_data_dir, + _package_name, +) + +# android.* 包 +from .android.app import * +from .android.content import * +from .android.content_pm import * +from .android.os import * +from .android.view import * +from .android.view_accessibility import * +from .android.view_animation import * +from .android.view_inputmethod import * +from .android.widget import * +from .android.graphics import * +from .android.graphics_drawable import * +from .android.graphics_pdf import * +from .android.net import * +from .android.net_wifi import * +from .android.media import * +from .android.hardware import * +from .android.hardware_camera2 import * +from .android.hardware_biometrics import * +from .android.location import * +from .android.database import * +from .android.database_sqlite import * +from .android.provider import * +from .android.telephony import * +from .android.bluetooth import * +from .android.bluetooth_le import * +from .android.text import * +from .android.util import * +from .android.preference import * +from .android.webkit import * +from .android.nfc import * +from .android.speech_tts import * +from .android.inputmethod import * + +# java.* 包 +from .java.io import * +from .java.net import * +from .java.util import * +from .java.util_regex import * +from .java.util_zip import * +from .java.util_concurrent import * +from .java.lang import * +from .java.math import * +from .java.text import * +from .java.time import * +from .java.nio import * +from .java.nio_file import * +from .java.security import * +from .java.crypto import * + +# org.* 包 +from .org.json import * + +# Base64 (标准 Android API) +import base64 as _base64 + +class Base64: + DEFAULT = 0 + NO_PADDING = 1 + NO_WRAP = 2 + CRLF = 4 + URL_SAFE = 8 + NO_CLOSE = 16 + + @staticmethod + def encodeToString(input_data, flags=0): + if isinstance(input_data, str): + input_data = input_data.encode('utf-8') + return _base64.b64encode(input_data).decode('ascii') + + @staticmethod + def decode(str_val, flags=0): + return _base64.b64decode(str_val) + + @staticmethod + def encode(input_data, flags=0): + if isinstance(input_data, str): + input_data = input_data.encode('utf-8') + return _base64.b64encode(input_data) + +# Scanner (java.util) +class Scanner: + def __init__(self, source): + if isinstance(source, str): + self._source = source + else: + self._source = str(source) + self._pos = 0 + + def next(self): + while self._pos < len(self._source) and self._source[self._pos].isspace(): + self._pos += 1 + start = self._pos + while self._pos < len(self._source) and not self._source[self._pos].isspace(): + self._pos += 1 + if start == self._pos: + raise Exception("NoSuchElementException") + return self._source[start:self._pos] + + def hasNext(self): + pos = self._pos + while pos < len(self._source) and self._source[pos].isspace(): + pos += 1 + return pos < len(self._source) + + def nextInt(self): + return int(self.next()) + + def close(self): + pass + +# Timer (java.util) +class Timer: + def __init__(self, name=None, daemon=False): + import threading + self._thread = None + self._tasks = [] + + def schedule(self, task, delay=None, period=None): + import threading + if delay is None: + self._run_once(task) + elif period is None: + self._run_delayed(task, delay) + else: + self._run_periodic(task, delay, period) + + def _run_once(self, task): + t = threading.Thread(target=task.run) + t.daemon = True + self._tasks.append(t) + t.start() + + def _run_delayed(self, task, delay_ms): + import threading + def _run(): + import time + time.sleep(delay_ms / 1000.0) + task.run() + t = threading.Thread(target=_run) + t.daemon = True + self._tasks.append(t) + t.start() + + def _run_periodic(self, task, delay_ms, period_ms): + import threading + def _run(): + import time + time.sleep(delay_ms / 1000.0) + while True: + task.run() + time.sleep(period_ms / 1000.0) + t = threading.Thread(target=_run) + t.daemon = True + self._tasks.append(t) + t.start() + + def cancel(self): + pass + + def purge(self): + return 0 + + +class TimerTask: + def __init__(self): + self._cancelled = False + + def run(self): + pass + + def cancel(self): + self._cancelled = True + + def scheduledExecutionTime(self): + return 0 + +__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", + "InputMethodManager", + "WebView", + "TextToSpeech", + # java + "File", "FileInputStream", "FileOutputStream", + "BufferedReader", "BufferedWriter", + "InputStreamReader", "OutputStreamWriter", + "URL", "HttpURLConnection", "Socket", "ServerSocket", + "ArrayList", "HashMap", "HashSet", "LinkedList", + "Collections", "Arrays", "Calendar", "Date", "Random", + "UUID", "Scanner", "Timer", "TimerTask", + "Pattern", "Matcher", + "ZipFile", "GZIPInputStream", "GZIPOutputStream", + "ExecutorService", "Executors", "Future", "Callable", + "String", "StringBuilder", "StringBuffer", + "Integer", "Math", "System", "Thread", + "BigInteger", "BigDecimal", + "SimpleDateFormat", "DecimalFormat", + "LocalDateTime", "LocalDate", "LocalTime", "Instant", "Duration", "Period", + "ChronoUnit", "ChronoField", "DayOfWeek", "Month", "ZoneId", "ZoneOffset", + "DateTimeFormatter", + "ByteBuffer", "Files", "Path", "Paths", + "MessageDigest", "SecureRandom", + "Cipher", "Mac", "KeyGenerator", + # org + "JSONObject", "JSONArray", "JSONException", +] diff --git a/android_pyapi/__pycache__/__init__.cpython-310.pyc b/android_pyapi/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..d8d2749 Binary files /dev/null and b/android_pyapi/__pycache__/__init__.cpython-310.pyc differ diff --git a/android_pyapi/__pycache__/_core.cpython-310.pyc b/android_pyapi/__pycache__/_core.cpython-310.pyc new file mode 100644 index 0000000..61eda8f Binary files /dev/null and b/android_pyapi/__pycache__/_core.cpython-310.pyc differ diff --git a/android_pyapi/_core.py b/android_pyapi/_core.py new file mode 100644 index 0000000..d8b2990 --- /dev/null +++ b/android_pyapi/_core.py @@ -0,0 +1,186 @@ +""" +Android-pyApi 核心基础设施 +""" + +import os +import sys +import platform +import tempfile +import json +import hashlib +import random +import time +from typing import Any, Dict, List, Optional, Callable + + +class AndroidOnlyError(Exception): + """当调用仅 Android 可用的 API 时抛出""" + pass + + +class Stub: + """ + 通用 Android API 存根基类。 + 子类可以覆盖具体方法实现真实逻辑; + 未覆盖的方法调用会返回合理默认值。 + """ + + _android_only = False + + def __init__(self, *args, **kwargs): + pass + + def __getattr__(self, name: str): + if name.startswith("_"): + raise AttributeError(name) + + def _stub_method(*args, **kwargs): + if self._android_only: + raise AndroidOnlyError( + f"{self.__class__.__name__}.{name}() 仅在 Android 设备上可用" + ) + return None + + return _stub_method + + def toString(self) -> str: + return f"{self.__class__.__name__}@{hex(id(self))}" + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return hash(self) & 0x7FFFFFFF + + def getClass(self) -> type: + return self.__class__ + + +# ═══════════════════════════════════════════════════════════════ +# 全局路径和环境 +# ═══════════════════════════════════════════════════════════════ + +_package_name = "com.ai.assistance.operit" +_sdcard_root = os.path.join(os.path.expanduser("~"), "Android-compat", "sdcard") +_app_data_dir = os.path.join(os.path.expanduser("~"), "Android-compat", "app-data") + + +def _ensure_dirs(): + dirs = [ + _sdcard_root, + os.path.join(_sdcard_root, "Download"), + os.path.join(_sdcard_root, "DCIM"), + os.path.join(_sdcard_root, "DCIM", "Screenshots"), + os.path.join(_sdcard_root, "Documents"), + os.path.join(_sdcard_root, "Music"), + os.path.join(_sdcard_root, "Movies"), + os.path.join(_sdcard_root, "Pictures"), + os.path.join(_sdcard_root, "Android", "data"), + os.path.join(_sdcard_root, "Android", "data", _package_name, "files"), + os.path.join(_sdcard_root, "Android", "data", _package_name, "cache"), + _app_data_dir, + os.path.join(_app_data_dir, "files"), + os.path.join(_app_data_dir, "cache"), + os.path.join(_app_data_dir, "shared_prefs"), + ] + for d in dirs: + os.makedirs(d, exist_ok=True) + + +_ensure_dirs() + + +def resolve_android_path(path: str) -> str: + """将 Android 路径解析为桌面路径""" + if path.startswith("/sdcard"): + return os.path.join(_sdcard_root, path[7:].lstrip("/")) + if path.startswith("/data/data/"): + return os.path.join(_app_data_dir, path[11:].lstrip("/")) + if path.startswith("content://"): + # 简单解析 content URI + from urllib.parse import urlparse + parsed = urlparse(path) + return parsed.path or path + if path.startswith("file://"): + from urllib.parse import unquote + return unquote(path[7:]) + return path + + +def to_android_path(desktop_path: str) -> str: + """将桌面路径映射回 Android 路径""" + if desktop_path.startswith(_sdcard_root): + rel = os.path.relpath(desktop_path, _sdcard_root) + return f"/sdcard/{rel}" + if desktop_path.startswith(_app_data_dir): + rel = os.path.relpath(desktop_path, _app_data_dir) + return f"/data/data/{_package_name}/{rel}" + return desktop_path + + +# ═══════════════════════════════════════════════════════════════ +# 全局 Context 单例 +# ═══════════════════════════════════════════════════════════════ + +class _GlobalContext: + """全局应用上下文占位符,在 android.content 模块中被替换为真实 Context""" + + @property + def packageName(self): + return _package_name + + def getPackageName(self): + return _package_name + + def getFilesDir(self): + return os.path.join(_app_data_dir, "files") + + def getCacheDir(self): + return os.path.join(_app_data_dir, "cache") + + def getExternalFilesDir(self, type_=None): + base = os.path.join(_sdcard_root, "Android", "data", _package_name, "files") + if type_: + base = os.path.join(base, type_) + os.makedirs(base, exist_ok=True) + return base + + def getExternalStorageDirectory(self): + return _sdcard_root + + def getSystemService(self, name): + return None # 由具体模块填充 + + +android_context = _GlobalContext() + + +def get_context(): + """获取全局 Android Context""" + return android_context + + +# ═══════════════════════════════════════════════════════════════ +# 可选依赖检测 +# ═══════════════════════════════════════════════════════════════ + +def _check_optional(name: str) -> bool: + """检查可选依赖是否可用""" + try: + __import__(name) + return True + except ImportError: + return False + + +HAS_PIL = _check_optional("PIL") +HAS_REQUESTS = _check_optional("requests") +HAS_HTTPX = _check_optional("httpx") +HAS_PYAUTOGUI = _check_optional("pyautogui") +HAS_SCREENINFO = _check_optional("screeninfo") +HAS_PSUTIL = _check_optional("psutil") +HAS_PYMUPDF = _check_optional("fitz") +HAS_PDF2IMAGE = _check_optional("pdf2image") +HAS_CRYPTOGRAPHY = _check_optional("cryptography") +HAS_PYPERCLIP = _check_optional("pyperclip") +HAS_JPYPE = _check_optional("jpype") diff --git a/android_pyapi/android/__init__.py b/android_pyapi/android/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/android_pyapi/android/__pycache__/__init__.cpython-310.pyc b/android_pyapi/android/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3a68c8d Binary files /dev/null and b/android_pyapi/android/__pycache__/__init__.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/app.cpython-310.pyc b/android_pyapi/android/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..6a63f0a Binary files /dev/null and b/android_pyapi/android/__pycache__/app.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/bluetooth.cpython-310.pyc b/android_pyapi/android/__pycache__/bluetooth.cpython-310.pyc new file mode 100644 index 0000000..fa39f0d Binary files /dev/null and b/android_pyapi/android/__pycache__/bluetooth.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/bluetooth_le.cpython-310.pyc b/android_pyapi/android/__pycache__/bluetooth_le.cpython-310.pyc new file mode 100644 index 0000000..5a808d5 Binary files /dev/null and b/android_pyapi/android/__pycache__/bluetooth_le.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/content.cpython-310.pyc b/android_pyapi/android/__pycache__/content.cpython-310.pyc new file mode 100644 index 0000000..926dfb8 Binary files /dev/null and b/android_pyapi/android/__pycache__/content.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/content_pm.cpython-310.pyc b/android_pyapi/android/__pycache__/content_pm.cpython-310.pyc new file mode 100644 index 0000000..1f22a49 Binary files /dev/null and b/android_pyapi/android/__pycache__/content_pm.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/database.cpython-310.pyc b/android_pyapi/android/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000..06c9cd6 Binary files /dev/null and b/android_pyapi/android/__pycache__/database.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/database_sqlite.cpython-310.pyc b/android_pyapi/android/__pycache__/database_sqlite.cpython-310.pyc new file mode 100644 index 0000000..8fcf089 Binary files /dev/null and b/android_pyapi/android/__pycache__/database_sqlite.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/graphics.cpython-310.pyc b/android_pyapi/android/__pycache__/graphics.cpython-310.pyc new file mode 100644 index 0000000..461bd74 Binary files /dev/null and b/android_pyapi/android/__pycache__/graphics.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/graphics_drawable.cpython-310.pyc b/android_pyapi/android/__pycache__/graphics_drawable.cpython-310.pyc new file mode 100644 index 0000000..f9910b8 Binary files /dev/null and b/android_pyapi/android/__pycache__/graphics_drawable.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/graphics_pdf.cpython-310.pyc b/android_pyapi/android/__pycache__/graphics_pdf.cpython-310.pyc new file mode 100644 index 0000000..8b88a40 Binary files /dev/null and b/android_pyapi/android/__pycache__/graphics_pdf.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/hardware.cpython-310.pyc b/android_pyapi/android/__pycache__/hardware.cpython-310.pyc new file mode 100644 index 0000000..b19a992 Binary files /dev/null and b/android_pyapi/android/__pycache__/hardware.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/hardware_biometrics.cpython-310.pyc b/android_pyapi/android/__pycache__/hardware_biometrics.cpython-310.pyc new file mode 100644 index 0000000..4a6d409 Binary files /dev/null and b/android_pyapi/android/__pycache__/hardware_biometrics.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/hardware_camera2.cpython-310.pyc b/android_pyapi/android/__pycache__/hardware_camera2.cpython-310.pyc new file mode 100644 index 0000000..356cf77 Binary files /dev/null and b/android_pyapi/android/__pycache__/hardware_camera2.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/inputmethod.cpython-310.pyc b/android_pyapi/android/__pycache__/inputmethod.cpython-310.pyc new file mode 100644 index 0000000..ca4e1a6 Binary files /dev/null and b/android_pyapi/android/__pycache__/inputmethod.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/location.cpython-310.pyc b/android_pyapi/android/__pycache__/location.cpython-310.pyc new file mode 100644 index 0000000..0d2c629 Binary files /dev/null and b/android_pyapi/android/__pycache__/location.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/media.cpython-310.pyc b/android_pyapi/android/__pycache__/media.cpython-310.pyc new file mode 100644 index 0000000..9fa6504 Binary files /dev/null and b/android_pyapi/android/__pycache__/media.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/net.cpython-310.pyc b/android_pyapi/android/__pycache__/net.cpython-310.pyc new file mode 100644 index 0000000..2fb0de8 Binary files /dev/null and b/android_pyapi/android/__pycache__/net.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/net_wifi.cpython-310.pyc b/android_pyapi/android/__pycache__/net_wifi.cpython-310.pyc new file mode 100644 index 0000000..9d09da7 Binary files /dev/null and b/android_pyapi/android/__pycache__/net_wifi.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/nfc.cpython-310.pyc b/android_pyapi/android/__pycache__/nfc.cpython-310.pyc new file mode 100644 index 0000000..05c2339 Binary files /dev/null and b/android_pyapi/android/__pycache__/nfc.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/os.cpython-310.pyc b/android_pyapi/android/__pycache__/os.cpython-310.pyc new file mode 100644 index 0000000..320a595 Binary files /dev/null and b/android_pyapi/android/__pycache__/os.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/preference.cpython-310.pyc b/android_pyapi/android/__pycache__/preference.cpython-310.pyc new file mode 100644 index 0000000..d2b640f Binary files /dev/null and b/android_pyapi/android/__pycache__/preference.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/provider.cpython-310.pyc b/android_pyapi/android/__pycache__/provider.cpython-310.pyc new file mode 100644 index 0000000..9d79ed6 Binary files /dev/null and b/android_pyapi/android/__pycache__/provider.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/speech_tts.cpython-310.pyc b/android_pyapi/android/__pycache__/speech_tts.cpython-310.pyc new file mode 100644 index 0000000..29aaf0c Binary files /dev/null and b/android_pyapi/android/__pycache__/speech_tts.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/telephony.cpython-310.pyc b/android_pyapi/android/__pycache__/telephony.cpython-310.pyc new file mode 100644 index 0000000..6803018 Binary files /dev/null and b/android_pyapi/android/__pycache__/telephony.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/text.cpython-310.pyc b/android_pyapi/android/__pycache__/text.cpython-310.pyc new file mode 100644 index 0000000..b2c4c5c Binary files /dev/null and b/android_pyapi/android/__pycache__/text.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/util.cpython-310.pyc b/android_pyapi/android/__pycache__/util.cpython-310.pyc new file mode 100644 index 0000000..ba70168 Binary files /dev/null and b/android_pyapi/android/__pycache__/util.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/view.cpython-310.pyc b/android_pyapi/android/__pycache__/view.cpython-310.pyc new file mode 100644 index 0000000..c71cbc0 Binary files /dev/null and b/android_pyapi/android/__pycache__/view.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/view_accessibility.cpython-310.pyc b/android_pyapi/android/__pycache__/view_accessibility.cpython-310.pyc new file mode 100644 index 0000000..5a0ca4f Binary files /dev/null and b/android_pyapi/android/__pycache__/view_accessibility.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/view_animation.cpython-310.pyc b/android_pyapi/android/__pycache__/view_animation.cpython-310.pyc new file mode 100644 index 0000000..68fd2ed Binary files /dev/null and b/android_pyapi/android/__pycache__/view_animation.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/view_inputmethod.cpython-310.pyc b/android_pyapi/android/__pycache__/view_inputmethod.cpython-310.pyc new file mode 100644 index 0000000..288ce25 Binary files /dev/null and b/android_pyapi/android/__pycache__/view_inputmethod.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/webkit.cpython-310.pyc b/android_pyapi/android/__pycache__/webkit.cpython-310.pyc new file mode 100644 index 0000000..fad0fbe Binary files /dev/null and b/android_pyapi/android/__pycache__/webkit.cpython-310.pyc differ diff --git a/android_pyapi/android/__pycache__/widget.cpython-310.pyc b/android_pyapi/android/__pycache__/widget.cpython-310.pyc new file mode 100644 index 0000000..756a67a Binary files /dev/null and b/android_pyapi/android/__pycache__/widget.cpython-310.pyc differ diff --git a/android_pyapi/android/app.py b/android_pyapi/android/app.py new file mode 100644 index 0000000..801a800 --- /dev/null +++ b/android_pyapi/android/app.py @@ -0,0 +1,2001 @@ +""" +Android App 组件 — Activity, Service, Intent, Notification 等 +方法签名完全对齐 Android SDK +""" + +import json +import os +import time +import re +import sys +import threading +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple, Callable, Union +from urllib.parse import urlparse, urlunparse + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# Intent +# ═══════════════════════════════════════════════════════════════ + +class Intent(Stub): + """Android Intent — 组件间消息传递""" + + # Action 常量 + ACTION_MAIN = "android.intent.action.MAIN" + ACTION_VIEW = "android.intent.action.VIEW" + ACTION_EDIT = "android.intent.action.EDIT" + ACTION_SEND = "android.intent.action.SEND" + ACTION_CALL = "android.intent.action.CALL" + ACTION_DIAL = "android.intent.action.DIAL" + ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH" + ACTION_PICK = "android.intent.action.PICK" + ACTION_GET_CONTENT = "android.intent.action.GET_CONTENT" + ACTION_INSTALL_PACKAGE = "android.intent.action.INSTALL_PACKAGE" + ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED" + ACTION_POWER_CONNECTED = "android.intent.action.ACTION_POWER_CONNECTED" + ACTION_POWER_DISCONNECTED = "android.intent.action.ACTION_POWER_DISCONNECTED" + ACTION_BATTERY_LOW = "android.intent.action.BATTERY_LOW" + ACTION_BATTERY_OKAY = "android.intent.action.BATTERY_OKAY" + ACTION_TIME_TICK = "android.intent.action.TIME_TICK" + ACTION_DATE_CHANGED = "android.intent.action.DATE_CHANGED" + ACTION_SCREEN_ON = "android.intent.action.SCREEN_ON" + ACTION_SCREEN_OFF = "android.intent.action.SCREEN_OFF" + ACTION_AIRPLANE_MODE_CHANGED = "android.intent.action.AIRPLANE_MODE_CHANGED" + + # Flag 常量 + FLAG_ACTIVITY_NEW_TASK = 0x10000000 + FLAG_ACTIVITY_CLEAR_TOP = 0x04000000 + FLAG_ACTIVITY_SINGLE_TOP = 0x20000000 + FLAG_ACTIVITY_NO_HISTORY = 0x40000000 + FLAG_FROM_BACKGROUND = 0x00004000 + FLAG_RECEIVER_FOREGROUND = 0x10000000 + FLAG_INCLUDE_STOPPED_PACKAGES = 0x00000020 + + # Category 常量 + CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER" + CATEGORY_DEFAULT = "android.intent.category.DEFAULT" + CATEGORY_HOME = "android.intent.category.HOME" + CATEGORY_BROWSABLE = "android.intent.category.BROWSABLE" + CATEGORY_OPENABLE = "android.intent.category.OPENABLE" + + def __init__(self, action: Optional[str] = None, uri: Optional[str] = None): + super().__init__() + self._action = action + self._uri = uri + self._data_str = None + self._type = None + self._extras: Dict[str, Any] = {} + self._flags = 0 + self._categories: List[str] = [] + self._package = None + self._component: Optional[Tuple[str, str]] = None # (pkg, cls) + self._clip_data = None + + def setClass(self, package_context: Any, cls: type) -> "Intent": + self._package = package_context.packageName if hasattr(package_context, 'packageName') else _package_name + self._component = (self._package, cls.__name__ if isinstance(cls, type) else str(cls)) + return self + + def setAction(self, action: str) -> "Intent": + self._action = action + return self + + def getAction(self) -> Optional[str]: + return self._action + + def setData(self, uri: str) -> "Intent": + self._data_str = uri + return self + + def getData(self) -> Optional[str]: + return self._data_str + + def setType(self, type_: str) -> "Intent": + self._type = type_ + return self + + def getType(self) -> Optional[str]: + return self._type + + def setDataAndType(self, uri: str, type_: str) -> "Intent": + self._data_str = uri + self._type = type_ + return self + + def putExtra(self, name: str, value: Any) -> "Intent": + self._extras[name] = value + return self + + def getStringExtra(self, name: str) -> Optional[str]: + return self._extras.get(name) + + def getIntExtra(self, name: str, default: int = 0) -> int: + return self._extras.get(name, default) + + def getBooleanExtra(self, name: str, default: bool = False) -> bool: + return self._extras.get(name, default) + + def getLongExtra(self, name: str, default: int = 0) -> int: + return self._extras.get(name, default) + + def getFloatExtra(self, name: str, default: float = 0.0) -> float: + return self._extras.get(name, default) + + def getDoubleExtra(self, name: str, default: float = 0.0) -> float: + return self._extras.get(name, default) + + def getSerializableExtra(self, name: str): + return self._extras.get(name) + + def getIntegerArrayListExtra(self, name: str) -> Optional[List[int]]: + return self._extras.get(name) + + def getStringArrayListExtra(self, name: str) -> Optional[List[str]]: + return self._extras.get(name) + + def getExtras(self) -> Dict[str, Any]: + return self._extras + + def hasExtra(self, name: str) -> bool: + return name in self._extras + + def removeExtra(self, name: str): + self._extras.pop(name, None) + + def addCategory(self, category: str) -> "Intent": + if category not in self._categories: + self._categories.append(category) + return self + + def getCategories(self) -> Optional[List[str]]: + return self._categories if self._categories else None + + def hasCategory(self, category: str) -> bool: + return category in self._categories + + def setFlags(self, flags: int) -> "Intent": + self._flags = flags + return self + + def getFlags(self) -> int: + return self._flags + + def addFlags(self, flags: int) -> "Intent": + self._flags |= flags + return self + + def setPackage(self, package_name: str) -> "Intent": + self._package = package_name + return self + + def getPackage(self) -> Optional[str]: + return self._package + + def setClassName(self, package_name: str, class_name: str) -> "Intent": + self._component = (package_name, class_name) + return self + + def resolveActivity(self, pm) -> Optional["ComponentName"]: + from .content import ComponentName + if self._component: + return ComponentName(self._component[0], self._component[1]) + return None + + def createChooser(self, title: str) -> "Intent": + """Create a chooser intent""" + return self + + def toURI(self) -> str: + return str(self._uri or self._data_str or "") + + def toString(self) -> str: + return ( + f"Intent {{ act={self._action} " + f"pkg={self._package} " + f"cmp={self._component} " + f"hasExtras={bool(self._extras)} }}" + ) + + def clone(self) -> "Intent": + cloned = Intent(self._action, self._uri) + cloned._data_str = self._data_str + cloned._type = self._type + cloned._extras = dict(self._extras) + cloned._flags = self._flags + cloned._categories = list(self._categories) + cloned._package = self._package + cloned._component = self._component + return cloned + + def fillIn(self, other: "Intent", flags: int = 0) -> int: + filled = 0 + if other._action and not self._action: + self._action = other._action; filled |= 1 + if other._data_str and not self._data_str: + self._data_str = other._data_str; filled |= 2 + if other._type and not self._type: + self._type = other._type; filled |= 4 + if other._categories: + for c in other._categories: + if c not in self._categories: + self._categories.append(c) + filled |= 8 + return filled + + def filterEquals(self, other: "Intent") -> bool: + return ( + self._action == other._action + and self._data_str == other._data_str + and self._type == other._type + and self._package == other._package + and self._component == other._component + ) + + def filterHashCode(self) -> int: + return hash((self._action, self._data_str, self._type, self._package, self._component)) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# ComponentName (forward reference) +# ═══════════════════════════════════════════════════════════════ + +class ComponentName(Stub): + """Identifies a specific component class within a package""" + + def __init__(self, pkg: str, cls: str): + super().__init__() + self._package = pkg + self._classname = cls + + def getPackageName(self) -> str: + return self._package + + def getClassName(self) -> str: + return self._classname + + def getShortClassName(self) -> str: + return self._classname.split(".")[-1] if "." in self._classname else self._classname + + def flattenToString(self) -> str: + return f"{self._package}/{self._classname}" + + def flattenToShortString(self) -> str: + return f"{self._package}/{self.getShortClassName()}" + + @staticmethod + def unflattenFromString(str_: str) -> Optional["ComponentName"]: + if "/" in str_: + parts = str_.split("/", 1) + return ComponentName(parts[0], parts[1]) + return None + + def toString(self) -> str: + return f"ComponentInfo{{{self._package}/{self._classname}}}" + + def __repr__(self): + return self.toString() + + +# ═══════════════════════════════════════════════════════════════ +# PendingIntent +# ═══════════════════════════════════════════════════════════════ + +class PendingIntent(Stub): + """A description of an Intent and target action to perform with it""" + + FLAG_ONE_SHOT = 1 << 30 + FLAG_NO_CREATE = 1 << 29 + FLAG_CANCEL_CURRENT = 1 << 28 + FLAG_UPDATE_CURRENT = 1 << 27 + FLAG_IMMUTABLE = 1 << 26 + + _pending_intents: Dict[int, "PendingIntent"] = {} + _next_id = 1000 + + def __init__(self, intent: Intent, request_code: int, flags: int): + super().__init__() + self._intent = intent + self._request_code = request_code + self._flags = flags + self._id = PendingIntent._next_id + PendingIntent._next_id += 1 + PendingIntent._pending_intents[self._id] = self + + @staticmethod + def getActivity(context, request_code: int, intent: Intent, flags: int) -> "PendingIntent": + pi = PendingIntent(intent, request_code, flags) + pi._type = "activity" + return pi + + @staticmethod + def getBroadcast(context, request_code: int, intent: Intent, flags: int) -> "PendingIntent": + pi = PendingIntent(intent, request_code, flags) + pi._type = "broadcast" + return pi + + @staticmethod + def getService(context, request_code: int, intent: Intent, flags: int) -> "PendingIntent": + pi = PendingIntent(intent, request_code, flags) + pi._type = "service" + return pi + + @staticmethod + def getActivities(context, request_code: int, intents: List[Intent], flags: int) -> "PendingIntent": + pi = PendingIntent(intents[0] if intents else Intent(), request_code, flags) + pi._type = "activities" + pi._intents = intents + return pi + + def send(self): + """Simulate sending the pending intent""" + pass + + def cancel(self): + PendingIntent._pending_intents.pop(self._id, None) + + def getIntent(self) -> Intent: + return self._intent + + def getTargetPackage(self) -> str: + return self._intent.getPackage() or "" + + def toString(self) -> str: + return f"PendingIntent{{{hex(id(self))}}}" + + +# ═══════════════════════════════════════════════════════════════ +# AlarmManager +# ═══════════════════════════════════════════════════════════════ + +class AlarmManager(Stub): + """AlarmManager — 调度定时操作""" + + RTC_WAKEUP = 0 + RTC = 1 + ELAPSED_REALTIME = 3 + ELAPSED_REALTIME_WAKEUP = 2 + INTERVAL_FIFTEEN_MINUTES = 900000 + INTERVAL_HALF_HOUR = 1800000 + INTERVAL_HOUR = 3600000 + INTERVAL_DAY = 86400000 + + def __init__(self): + super().__init__() + self._alarms: Dict[int, Dict] = {} + + def set(self, type_: int, trigger_at_ms: int, operation: PendingIntent): + self._alarms[id(operation)] = { + "type": type_, + "trigger_at": trigger_at_ms, + "operation": operation, + "repeating": False, + } + + def setRepeating(self, type_: int, trigger_at_ms: int, interval_ms: int, operation: PendingIntent): + self._alarms[id(operation)] = { + "type": type_, + "trigger_at": trigger_at_ms, + "interval": interval_ms, + "operation": operation, + "repeating": True, + } + + def setExact(self, type_: int, trigger_at_ms: int, operation: PendingIntent): + self.set(type_, trigger_at_ms, operation) + + def setAndAllowWhileIdle(self, type_: int, trigger_at_ms: int, operation: PendingIntent): + self.set(type_, trigger_at_ms, operation) + + def setExactAndAllowWhileIdle(self, type_: int, trigger_at_ms: int, operation: PendingIntent): + self.set(type_, trigger_at_ms, operation) + + def cancel(self, operation: PendingIntent): + self._alarms.pop(id(operation), None) + + def getNextAlarmClock(self): + return None + + +# ═══════════════════════════════════════════════════════════════ +# NotificationChannel +# ═══════════════════════════════════════════════════════════════ + +class NotificationChannel(Stub): + """Notification channel for Android O+""" + + IMPORTANCE_NONE = 0 + IMPORTANCE_MIN = 1 + IMPORTANCE_LOW = 2 + IMPORTANCE_DEFAULT = 3 + IMPORTANCE_HIGH = 4 + IMPORTANCE_MAX = 5 + IMPORTANCE_UNSPECIFIED = -1000 + + def __init__(self, channel_id: str, name: str, importance: int): + super().__init__() + self._id = channel_id + self._name = name + self._importance = importance + self._description = "" + self._enabled = True + self._show_badge = True + self._sound = None + self._lights = False + self._vibration = True + self._bypass_dnd = False + self._lockscreen_visibility = 0 + self._group = None + self._can_bubble = False + + def getId(self) -> str: + return self._id + + def getName(self) -> str: + return self._name + + def getImportance(self) -> int: + return self._importance + + def setDescription(self, description: str): + self._description = description + + def getDescription(self) -> str: + return self._description + + def setShowBadge(self, show_badge: bool): + self._show_badge = show_badge + + def canShowBadge(self) -> bool: + return self._show_badge + + def enableLights(self, lights: bool): + self._lights = lights + + def shouldShowLights(self) -> bool: + return self._lights + + def enableVibration(self, vibration: bool): + self._vibration = vibration + + def shouldVibrate(self) -> bool: + return self._vibration + + def setBypassDnd(self, bypass_dnd: bool): + self._bypass_dnd = bypass_dnd + + def canBypassDnd(self) -> bool: + return self._bypass_dnd + + def setSound(self, sound: Any, audio_attributes: Any = None): + self._sound = sound + + def getSound(self): + return self._sound + + def setGroup(self, group_id: str): + self._group = group_id + + def getGroup(self) -> Optional[str]: + return self._group + + def setLockScreenVisibility(self, visibility: int): + self._lockscreen_visibility = visibility + + def getLockScreenVisibility(self) -> int: + return self._lockscreen_visibility + + def toString(self) -> str: + return f"NotificationChannel(id={self._id}, name={self._name}, importance={self._importance})" + + +class NotificationChannelGroup(Stub): + """Group of notification channels""" + + def __init__(self, group_id: str, name: str): + super().__init__() + self._id = group_id + self._name = name + self._channels: List[NotificationChannel] = [] + + def getId(self) -> str: + return self._id + + def getName(self) -> str: + return self._name + + def getChannels(self) -> List[NotificationChannel]: + return list(self._channels) + + +# ═══════════════════════════════════════════════════════════════ +# Notification +# ═══════════════════════════════════════════════════════════════ + +class Notification(Stub): + """Android Notification""" + + DEFAULT_ALL = -1 + DEFAULT_SOUND = 1 + DEFAULT_VIBRATE = 2 + DEFAULT_LIGHTS = 4 + EXTRA_TITLE = "android.title" + EXTRA_TEXT = "android.text" + EXTRA_SUB_TEXT = "android.subText" + EXTRA_INFO_TEXT = "android.infoText" + EXTRA_SUMMARY_TEXT = "android.summaryText" + EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID" + EXTRA_BIG_TEXT = "android.bigText" + EXTRA_PICTURE = "android.picture" + PRIORITY_MIN = -2 + PRIORITY_LOW = -1 + PRIORITY_DEFAULT = 0 + PRIORITY_HIGH = 1 + PRIORITY_MAX = 2 + VISIBILITY_PRIVATE = 0 + VISIBILITY_PUBLIC = 1 + VISIBILITY_SECRET = -1 + + class Builder(Stub): + """Notification builder pattern""" + + def __init__(self, context, channel_id: Optional[str] = None): + super().__init__() + self._context = context + self._channel_id = channel_id or "" + self._title = "" + self._text = "" + self._sub_text = "" + self._info_text = "" + self._summary_text = "" + self._big_text = "" + self._small_icon = 0 + self._large_icon = None + self._color = 0 + self._priority = Notification.PRIORITY_DEFAULT + self._defaults = 0 + self._auto_cancel = False + self._ongoing = False + self._when = 0 + self._number = 0 + self._content_intent = None + self._delete_intent = None + self._full_screen_intent = None + self._ticker = "" + self._visibility = Notification.VISIBILITY_PRIVATE + self._category = "" + self._group = "" + self._group_summary = False + self._public_version = None + self._extras: Dict[str, Any] = {} + self._actions: List[Dict] = [] + self._style = None + self._badge_icon_type = 0 + self._settings_text = None + self._uses_channel_defaults = False + + def setChannelId(self, channel_id: str) -> "Notification.Builder": + self._channel_id = channel_id + return self + + def setContentTitle(self, title: str) -> "Notification.Builder": + self._title = title + return self + + def setContentText(self, text: str) -> "Notification.Builder": + self._text = text + return self + + def setSubText(self, text: str) -> "Notification.Builder": + self._sub_text = text + return self + + def setContentInfo(self, info: str) -> "Notification.Builder": + self._info_text = info + return self + + def setSummaryText(self, text: str) -> "Notification.Builder": + self._summary_text = text + return self + + def setSmallIcon(self, icon: int) -> "Notification.Builder": + self._small_icon = icon + return self + + def setLargeIcon(self, icon: Any) -> "Notification.Builder": + self._large_icon = icon + return self + + def setColor(self, color: int) -> "Notification.Builder": + self._color = color + return self + + def setPriority(self, priority: int) -> "Notification.Builder": + self._priority = priority + return self + + def setDefaults(self, defaults: int) -> "Notification.Builder": + self._defaults = defaults + return self + + def setAutoCancel(self, auto_cancel: bool) -> "Notification.Builder": + self._auto_cancel = auto_cancel + return self + + def setOngoing(self, ongoing: bool) -> "Notification.Builder": + self._ongoing = ongoing + return self + + def setWhen(self, when: int) -> "Notification.Builder": + self._when = when + return self + + def setNumber(self, number: int) -> "Notification.Builder": + self._number = number + return self + + def setContentIntent(self, intent: PendingIntent) -> "Notification.Builder": + self._content_intent = intent + return self + + def setDeleteIntent(self, intent: PendingIntent) -> "Notification.Builder": + self._delete_intent = intent + return self + + def setFullScreenIntent(self, intent: PendingIntent, high_priority: bool) -> "Notification.Builder": + self._full_screen_intent = intent + return self + + def setTicker(self, ticker: str) -> "Notification.Builder": + self._ticker = ticker + return self + + def setVisibility(self, visibility: int) -> "Notification.Builder": + self._visibility = visibility + return self + + def setCategory(self, category: str) -> "Notification.Builder": + self._category = category + return self + + def setGroup(self, group: str) -> "Notification.Builder": + self._group = group + return self + + def setGroupSummary(self, is_group_summary: bool) -> "Notification.Builder": + self._group_summary = is_group_summary + return self + + def setPublicVersion(self, notification: "Notification") -> "Notification.Builder": + self._public_version = notification + return self + + def addAction(self, icon: int, title: str, intent: PendingIntent) -> "Notification.Builder": + self._actions.append({"icon": icon, "title": title, "intent": intent}) + return self + + def setStyle(self, style: Any) -> "Notification.Builder": + self._style = style + return self + + def setBadgeIconType(self, icon_type: int) -> "Notification.Builder": + self._badge_icon_type = icon_type + return self + + def setSettingsText(self, text: str) -> "Notification.Builder": + self._settings_text = text + return self + + def addExtras(self, extras: Dict[str, Any]) -> "Notification.Builder": + if extras: + self._extras.update(extras) + return self + + def extend(self, builder: Any) -> "Notification.Builder": + return self + + def build(self) -> "Notification": + n = Notification() + n._channel_id = self._channel_id + n._title = self._title + n._text = self._text + n._sub_text = self._sub_text + n._info_text = self._info_text + n._summary_text = self._summary_text + n._big_text = self._big_text + n._small_icon = self._small_icon + n._large_icon = self._large_icon + n._color = self._color + n._priority = self._priority + n._defaults = self._defaults + n._auto_cancel = self._auto_cancel + n._ongoing = self._ongoing + n._when = self._when or int(time.time() * 1000) + n._number = self._number + n._content_intent = self._content_intent + n._delete_intent = self._delete_intent + n._full_screen_intent = self._full_screen_intent + n._ticker = self._ticker + n._visibility = self._visibility + n._category = self._category + n._group = self._group + n._group_summary = self._group_summary + n._public_version = self._public_version + n._actions = list(self._actions) + n._style = self._style + n._extras = dict(self._extras) + return n + + def __init__(self): + super().__init__() + self._channel_id = "" + self._title = "" + self._text = "" + self._sub_text = "" + self._info_text = "" + self._summary_text = "" + self._big_text = "" + self._small_icon = 0 + self._large_icon = None + self._color = 0 + self._priority = self.PRIORITY_DEFAULT + self._defaults = 0 + self._auto_cancel = False + self._ongoing = False + self._when = 0 + self._number = 0 + self._content_intent = None + self._delete_intent = None + self._full_screen_intent = None + self._ticker = "" + self._visibility = self.VISIBILITY_PRIVATE + self._category = "" + self._group = "" + self._group_summary = False + self._public_version = None + self._extras: Dict[str, Any] = {} + self._actions: List[Dict] = [] + self._style = None + + def getChannelId(self) -> str: + return self._channel_id + + def toString(self) -> str: + return f"Notification(channel={self._channel_id}, title={self._title})" + + +class NotificationCompat(Stub): + """NotificationCompat — 兼容版本""" + + class Builder(Stub): + def __init__(self, context, channel_id: Optional[str] = None): + super().__init__() + self._builder = Notification.Builder(context, channel_id) + + def setContentTitle(self, title: str) -> "NotificationCompat.Builder": + self._builder.setContentTitle(title) + return self + + def setContentText(self, text: str) -> "NotificationCompat.Builder": + self._builder.setContentText(text) + return self + + def setSmallIcon(self, icon: int) -> "NotificationCompat.Builder": + self._builder.setSmallIcon(icon) + return self + + def setPriority(self, priority: int) -> "NotificationCompat.Builder": + self._builder.setPriority(priority) + return self + + def setAutoCancel(self, auto_cancel: bool) -> "NotificationCompat.Builder": + self._builder.setAutoCancel(auto_cancel) + return self + + def setContentIntent(self, intent: PendingIntent) -> "NotificationCompat.Builder": + self._builder.setContentIntent(intent) + return self + + def setDefaults(self, defaults: int) -> "NotificationCompat.Builder": + self._builder.setDefaults(defaults) + return self + + def build(self) -> Notification: + return self._builder.build() + + +# ═══════════════════════════════════════════════════════════════ +# NotificationManager +# ═══════════════════════════════════════════════════════════════ + +class NotificationManager(Stub): + """NotificationManager — 发送通知""" + + IMPORTANCE_NONE = 0 + IMPORTANCE_MIN = 1 + IMPORTANCE_LOW = 2 + IMPORTANCE_DEFAULT = 3 + IMPORTANCE_HIGH = 4 + IMPORTANCE_MAX = 5 + IMPORTANCE_UNSPECIFIED = -1000 + + def __init__(self): + super().__init__() + self._notifications: Dict[int, Notification] = {} + self._channels: Dict[str, NotificationChannel] = {} + self._groups: Dict[str, NotificationChannelGroup] = {} + self._notification_id_counter = 0 + + def notify(self, id_: int, notification: Notification): + self._notifications[id_] = notification + sys.stderr.write( + f"[NotificationManager] notify id={id_}: " + f"title={notification._title} text={notification._text}\n" + ) + + def notifyAsPackage(self, package_name: str, tag: Optional[str], id_: int, notification: Notification): + self.notify(id_, notification) + + def cancel(self, id_: int): + self._notifications.pop(id_, None) + + def cancelAll(self): + self._notifications.clear() + + def cancelAsPackage(self, package_name: str, tag: Optional[str], id_: int): + self.cancel(id_) + + def createNotificationChannel(self, channel: NotificationChannel): + self._channels[channel.getId()] = channel + sys.stderr.write( + f"[NotificationManager] create channel: {channel.getId()}\n" + ) + + def createNotificationChannels(self, channels: List[NotificationChannel]): + for c in channels: + self.createNotificationChannel(c) + + def getNotificationChannel(self, channel_id: str) -> Optional[NotificationChannel]: + return self._channels.get(channel_id) + + def getNotificationChannels(self) -> List[NotificationChannel]: + return list(self._channels.values()) + + def deleteNotificationChannel(self, channel_id: str): + self._channels.pop(channel_id, None) + + def createNotificationChannelGroup(self, group: NotificationChannelGroup): + self._groups[group.getId()] = group + + def getNotificationChannelGroups(self) -> List[NotificationChannelGroup]: + return list(self._groups.values()) + + def getActiveNotifications(self) -> List[Notification]: + return list(self._notifications.values()) + + def areNotificationsEnabled(self) -> bool: + return True + + def isNotificationPolicyAccessGranted(self) -> bool: + return True + + def matchesCallFilter(self, address: Any) -> bool: + return True + + def toString(self) -> str: + return f"NotificationManager(notifications={len(self._notifications)})" + + +# ═══════════════════════════════════════════════════════════════ +# Dialog / AlertDialog / ProgressDialog / DatePickerDialog / TimePickerDialog +# ═══════════════════════════════════════════════════════════════ + +class Dialog(Stub): + """Base dialog class""" + + def __init__(self, context, theme_res_id: int = 0): + super().__init__() + self._context = context + self._theme_res_id = theme_res_id + self._showing = False + self._cancelable = True + self._on_cancel_listener = None + self._on_dismiss_listener = None + self._on_show_listener = None + self._title = "" + self._window = None + + def show(self): + self._showing = True + sys.stderr.write(f"[Dialog] show: {self.__class__.__name__}\n") + + def dismiss(self): + self._showing = False + sys.stderr.write(f"[Dialog] dismiss: {self.__class__.__name__}\n") + + def isShowing(self) -> bool: + return self._showing + + def cancel(self): + self.dismiss() + if self._on_cancel_listener: + self._on_cancel_listener.onCancel(self) + + def setCancelable(self, flag: bool): + self._cancelable = flag + + def setCanceledOnTouchOutside(self, cancel: bool): + pass + + def setTitle(self, title: str): + self._title = title + + def getContext(self): + return self._context + + def setOnCancelListener(self, listener): + self._on_cancel_listener = listener + + def setOnDismissListener(self, listener): + self._on_dismiss_listener = listener + + def setOnShowListener(self, listener): + self._on_show_listener = listener + + def getWindow(self): + return self._window + + def findViewById(self, id_: int): + return None + + def onBackPressed(self): + if self._cancelable: + self.cancel() + + +class AlertDialog(Dialog): + """Alert dialog with builder pattern""" + + BUTTON_POSITIVE = -1 + BUTTON_NEGATIVE = -2 + BUTTON_NEUTRAL = -3 + + def __init__(self, context, theme_res_id: int = 0): + super().__init__(context, theme_res_id) + self._message = "" + self._buttons: Dict[int, Dict] = {} + self._items: List[str] = [] + self._single_choice_items = None + self._multi_choice_items = None + self._selected_index = -1 + self._selected_indices: List[int] = [] + self._view = None + self._icon = 0 + self._custom_title = None + + class Builder(Stub): + """AlertDialog builder""" + def __init__(self, context, theme_res_id: int = 0): + super().__init__() + self._context = context + self._theme_res_id = theme_res_id + self._title = "" + self._message = "" + self._positive_text = "" + self._positive_listener = None + self._negative_text = "" + self._negative_listener = None + self._neutral_text = "" + self._neutral_listener = None + self._cancelable = True + self._on_cancel_listener = None + self._items: List[str] = [] + self._on_item_click_listener = None + self._single_choice_items = None + self._single_choice_selected = -1 + self._single_choice_listener = None + self._multi_choice_items = None + self._multi_choice_selected = None + self._multi_choice_listener = None + self._icon = 0 + self._view = None + self._custom_title_view = None + + def setTitle(self, title: str) -> "AlertDialog.Builder": + self._title = title + return self + + def setMessage(self, message: str) -> "AlertDialog.Builder": + self._message = message + return self + + def setPositiveButton(self, text: str, listener=None) -> "AlertDialog.Builder": + self._positive_text = text + self._positive_listener = listener + return self + + def setNegativeButton(self, text: str, listener=None) -> "AlertDialog.Builder": + self._negative_text = text + self._negative_listener = listener + return self + + def setNeutralButton(self, text: str, listener=None) -> "AlertDialog.Builder": + self._neutral_text = text + self._neutral_listener = listener + return self + + def setCancelable(self, cancelable: bool) -> "AlertDialog.Builder": + self._cancelable = cancelable + return self + + def setOnCancelListener(self, listener) -> "AlertDialog.Builder": + self._on_cancel_listener = listener + return self + + def setItems(self, items: List[str], listener=None) -> "AlertDialog.Builder": + self._items = list(items) + self._on_item_click_listener = listener + return self + + def setSingleChoiceItems(self, items: List[str], checked_item: int, listener=None) -> "AlertDialog.Builder": + self._single_choice_items = list(items) + self._single_choice_selected = checked_item + self._single_choice_listener = listener + return self + + def setMultiChoiceItems(self, items: List[str], checked_items: List[bool], listener=None) -> "AlertDialog.Builder": + self._multi_choice_items = list(items) + self._multi_choice_selected = checked_items or [] + self._multi_choice_listener = listener + return self + + def setIcon(self, icon: int) -> "AlertDialog.Builder": + self._icon = icon + return self + + def setView(self, view: Any) -> "AlertDialog.Builder": + self._view = view + return self + + def setCustomTitle(self, view: Any) -> "AlertDialog.Builder": + self._custom_title_view = view + return self + + def create(self) -> "AlertDialog": + dialog = AlertDialog(self._context, self._theme_res_id) + dialog._title = self._title + dialog._message = self._message + dialog._cancelable = self._cancelable + dialog._icon = self._icon + dialog._view = self._view + if self._positive_text: + dialog._buttons[AlertDialog.BUTTON_POSITIVE] = { + "text": self._positive_text, + "listener": self._positive_listener, + } + if self._negative_text: + dialog._buttons[AlertDialog.BUTTON_NEGATIVE] = { + "text": self._negative_text, + "listener": self._negative_listener, + } + if self._neutral_text: + dialog._buttons[AlertDialog.BUTTON_NEUTRAL] = { + "text": self._neutral_text, + "listener": self._neutral_listener, + } + dialog._items = list(self._items) + dialog._single_choice_items = self._single_choice_items + dialog._multi_choice_items = self._multi_choice_items + return dialog + + def show(self) -> "AlertDialog": + dialog = self.create() + dialog.show() + return dialog + + +class ProgressDialog(AlertDialog): + """Progress dialog with a progress bar""" + + STYLE_SPINNER = 0 + STYLE_HORIZONTAL = 1 + + def __init__(self, context, theme_res_id: int = 0): + super().__init__(context, theme_res_id) + self._progress_style = ProgressDialog.STYLE_SPINNER + self._progress = 0 + self._max = 100 + self._indeterminate = True + self._progress_number_format = "%d/%d" + self._progress_percent_format = "%d%%" + + def setProgressStyle(self, style: int): + self._progress_style = style + + def setProgress(self, value: int): + self._progress = value + + def getProgress(self) -> int: + return self._progress + + def setMax(self, max_: int): + self._max = max_ + + def getMax(self) -> int: + return self._max + + def setIndeterminate(self, indeterminate: bool): + self._indeterminate = indeterminate + + def isIndeterminate(self) -> bool: + return self._indeterminate + + def incrementProgressBy(self, diff: int): + self._progress = min(self._max, max(0, self._progress + diff)) + + @staticmethod + def show(context, title: str, message: str, indeterminate: bool = True, cancelable: bool = False) -> "ProgressDialog": + dialog = ProgressDialog(context) + dialog.setTitle(title) + dialog._message = message + dialog._indeterminate = indeterminate + dialog.setCancelable(cancelable) + dialog.show() + return dialog + + +class DatePickerDialog(AlertDialog): + """Date picker dialog""" + + def __init__(self, context, listener=None, year: int = 2024, month: int = 0, day_of_month: int = 1, theme_res_id: int = 0): + super().__init__(context, theme_res_id) + self._listener = listener + self._year = year + self._month = month + self._day_of_month = day_of_month + self._date_picker = None + + def getDatePicker(self): + return self._date_picker + + def updateDate(self, year: int, month: int, day_of_month: int): + self._year = year + self._month = month + self._day_of_month = day_of_month + + def onDateChanged(self, view, year: int, month: int, day: int): + self._year = year + self._month = month + self._day_of_month = day + + class OnDateSetListener: + def onDateSet(self, view, year: int, month: int, day_of_month: int): + pass + + +class TimePickerDialog(AlertDialog): + """Time picker dialog""" + + def __init__(self, context, listener=None, hour_of_day: int = 12, minute: int = 0, is24_hour_view: bool = True, theme_res_id: int = 0): + super().__init__(context, theme_res_id) + self._listener = listener + self._hour_of_day = hour_of_day + self._minute = minute + self._is24_hour_view = is24_hour_view + + class OnTimeSetListener: + def onTimeSet(self, view, hour_of_day: int, minute: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# Fragment / FragmentManager / FragmentTransaction +# ═══════════════════════════════════════════════════════════════ + +class Fragment(Stub): + """Fragment — UI 片段""" + + def __init__(self): + super().__init__() + self._arguments: Optional[Bundle] = None + self._activity = None + self._view = None + self._fragment_manager = None + self._state = 0 + self._tag = None + self._id = -1 + self._container_id = -1 + self._menu = True + self._retain_instance = False + self._target_fragment = None + self._target_request_code = -1 + self._user_visible_hint = True + + def setArguments(self, args: "Bundle"): + self._arguments = args + + def getArguments(self) -> Optional["Bundle"]: + return self._arguments + + def getActivity(self): + return self._activity + + def getContext(self): + return self._activity + + def getFragmentManager(self) -> Optional["FragmentManager"]: + return self._fragment_manager + + def getView(self): + return self._view + + def getResources(self): + if self._activity: + return getattr(self._activity, 'getResources', lambda: None)() + return None + + def getString(self, res_id: int, *args) -> str: + return str(res_id) + + def getTag(self) -> Optional[str]: + return self._tag + + def setId(self, id_: int): + self._id = id_ + + def getId(self) -> int: + return self._id + + def onAttach(self, context): + pass + + def onCreate(self, savedInstanceState: Optional["Bundle"] = None): + pass + + def onCreateView(self, inflater, container, savedInstanceState): + return None + + def onViewCreated(self, view, savedInstanceState: Optional["Bundle"] = None): + pass + + def onStart(self): + pass + + def onResume(self): + pass + + def onPause(self): + pass + + def onStop(self): + pass + + def onDestroyView(self): + pass + + def onDestroy(self): + pass + + def onDetach(self): + pass + + def onActivityCreated(self, savedInstanceState: Optional["Bundle"] = None): + pass + + def onSaveInstanceState(self, out_state: "Bundle"): + pass + + def onViewStateRestored(self, savedInstanceState: Optional["Bundle"] = None): + pass + + def isAdded(self) -> bool: + return self._activity is not None + + def isDetached(self) -> bool: + return self._activity is None + + def isVisible(self) -> bool: + return False + + def isRemoving(self) -> bool: + return False + + def isInLayout(self) -> bool: + return False + + def isResumed(self) -> bool: + return self._state >= 7 + + def setHasOptionsMenu(self, has_menu: bool): + self._menu = has_menu + + def setRetainInstance(self, retain: bool): + self._retain_instance = retain + + def getRetainInstance(self) -> bool: + return self._retain_instance + + def setTargetFragment(self, fragment: "Fragment", request_code: int): + self._target_fragment = fragment + self._target_request_code = request_code + + def getTargetFragment(self) -> Optional["Fragment"]: + return self._target_fragment + + def getTargetRequestCode(self) -> int: + return self._target_request_code + + def setUserVisibleHint(self, is_visible_to_user: bool): + self._user_visible_hint = is_visible_to_user + + def getUserVisibleHint(self) -> bool: + return self._user_visible_hint + + def registerForContextMenu(self, view): + pass + + def unregisterForContextMenu(self, view): + pass + + +class FragmentTransaction(Stub): + """FragmentTransaction — 管理 Fragment 事务""" + + TRANSIT_ENTER_MASK = 0x1000 + TRANSIT_EXIT_MASK = 0x2000 + TRANSIT_FRAGMENT_FADE = 0x1001 + TRANSIT_FRAGMENT_OPEN = 0x1003 + TRANSIT_FRAGMENT_CLOSE = 0x1004 + TRANSIT_NONE = 0 + TRANSIT_UNSET = -1 + + def __init__(self): + super().__init__() + self._ops: List[Dict] = [] + self._transition = FragmentTransaction.TRANSIT_NONE + self._transition_style = 0 + self._allow_state_loss = False + self._commit_allowing_state_loss = False + + def add(self, container_view_id: int, fragment: Fragment, tag: Optional[str] = None) -> "FragmentTransaction": + self._ops.append({"type": "add", "id": container_view_id, "fragment": fragment, "tag": tag}) + return self + + def replace(self, container_view_id: int, fragment: Fragment, tag: Optional[str] = None) -> "FragmentTransaction": + self._ops.append({"type": "replace", "id": container_view_id, "fragment": fragment, "tag": tag}) + return self + + def remove(self, fragment: Fragment) -> "FragmentTransaction": + self._ops.append({"type": "remove", "fragment": fragment}) + return self + + def hide(self, fragment: Fragment) -> "FragmentTransaction": + self._ops.append({"type": "hide", "fragment": fragment}) + return self + + def show(self, fragment: Fragment) -> "FragmentTransaction": + self._ops.append({"type": "show", "fragment": fragment}) + return self + + def detach(self, fragment: Fragment) -> "FragmentTransaction": + self._ops.append({"type": "detach", "fragment": fragment}) + return self + + def attach(self, fragment: Fragment) -> "FragmentTransaction": + self._ops.append({"type": "attach", "fragment": fragment}) + return self + + def addToBackStack(self, name: Optional[str] = None) -> "FragmentTransaction": + self._ops.append({"type": "addToBackStack", "name": name}) + return self + + def setTransition(self, transit: int) -> "FragmentTransaction": + self._transition = transit + return self + + def setTransitionStyle(self, style: int) -> "FragmentTransaction": + self._transition_style = style + return self + + def setCustomAnimations(self, enter: int, exit: int, pop_enter: int = 0, pop_exit: int = 0) -> "FragmentTransaction": + return self + + def disallowAddToBackStack(self) -> "FragmentTransaction": + return self + + def setBreadCrumbShortTitle(self, title: str) -> "FragmentTransaction": + return self + + def setBreadCrumbTitle(self, title: str) -> "FragmentTransaction": + return self + + def commit(self) -> int: + return 0 + + def commitAllowingStateLoss(self) -> int: + return 0 + + def commitNow(self) -> int: + return 0 + + def commitNowAllowingStateLoss(self) -> int: + return 0 + + def isAddToBackStackAllowed(self) -> bool: + return True + + +class FragmentManager(Stub): + """FragmentManager — Fragment 管理器""" + + POP_BACK_STACK_INCLUSIVE = 1 + + def __init__(self): + super().__init__() + self._fragments: Dict[str, Fragment] = {} + self._back_stack: List[Dict] = [] + + def beginTransaction(self) -> FragmentTransaction: + return FragmentTransaction() + + def findFragmentById(self, id_: int) -> Optional[Fragment]: + for f in self._fragments.values(): + if f.getId() == id_: + return f + return None + + def findFragmentByTag(self, tag: str) -> Optional[Fragment]: + return self._fragments.get(tag) + + def popBackStack(self): + if self._back_stack: + self._back_stack.pop() + + def popBackStackImmediate(self, name: str, flags: int) -> bool: + return True + + def getBackStackEntryCount(self) -> int: + return len(self._back_stack) + + def getFragment(self, bundle: "Bundle", key: str) -> Optional[Fragment]: + return None + + def getFragments(self) -> List[Fragment]: + return list(self._fragments.values()) + + def saveFragmentInstanceState(self, fragment: Fragment): + return None + + def isDestroyed(self) -> bool: + return False + + def isStateSaved(self) -> bool: + return False + + def putFragment(self, bundle: "Bundle", key: str, fragment: Fragment): + pass + + def executePendingTransactions(self) -> bool: + return True + + def clearFragmentResult(self, request_key: str): + pass + + def setFragmentResult(self, request_key: str, result: "Bundle"): + pass + + def setFragmentResultListener(self, lifecycle: Any, listener: Any): + pass + + def registerFragmentLifecycleCallbacks(self, cb: Any, recursive: bool): + pass + + def unregisterFragmentLifecycleCallbacks(self, cb: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# Activity +# ═══════════════════════════════════════════════════════════════ + +class Activity(Stub): + """Activity — 应用界面入口""" + + # 结果码 + RESULT_OK = -1 + RESULT_CANCELED = 0 + RESULT_FIRST_USER = 1 + + def __init__(self): + super().__init__() + self._created = False + self._started = False + self._resumed = False + self._destroyed = False + self._intent: Optional[Intent] = None + self._callbacks: Dict[int, Tuple[int, Intent]] = {} + self._fragment_manager = FragmentManager() + self._application = None + self._title = "" + self._request_code_counter = 0 + + def onCreate(self, savedInstanceState: Optional["Bundle"] = None): + self._created = True + sys.stderr.write(f"[Activity] onCreate: {self.__class__.__name__}\n") + + def onStart(self): + self._started = True + sys.stderr.write(f"[Activity] onStart: {self.__class__.__name__}\n") + + def onResume(self): + self._resumed = True + sys.stderr.write(f"[Activity] onResume: {self.__class__.__name__}\n") + + def onPause(self): + self._resumed = False + sys.stderr.write(f"[Activity] onPause: {self.__class__.__name__}\n") + + def onStop(self): + self._started = False + sys.stderr.write(f"[Activity] onStop: {self.__class__.__name__}\n") + + def onDestroy(self): + self._destroyed = True + self._created = False + sys.stderr.write(f"[Activity] onDestroy: {self.__class__.__name__}\n") + + def onRestart(self): + sys.stderr.write(f"[Activity] onRestart: {self.__class__.__name__}\n") + + def onBackPressed(self): + sys.stderr.write("[Activity] onBackPressed\n") + + def onSaveInstanceState(self, out_state: "Bundle"): + pass + + def onRestoreInstanceState(self, savedInstanceState: "Bundle"): + pass + + def onNewIntent(self, intent: Intent): + self._intent = intent + + def onActivityResult(self, request_code: int, result_code: int, data: Optional[Intent]): + pass + + def onRequestPermissionsResult(self, request_code: int, permissions: List[str], grant_results: List[int]): + pass + + def setContentView(self, layout_res_id: int): + sys.stderr.write(f"[Activity] setContentView: {layout_res_id}\n") + + def findViewById(self, id_: int) -> Any: + return None + + def startActivity(self, intent: Intent): + sys.stderr.write( + f"[Activity] startActivity: action={intent.getAction()} " + f"pkg={intent.getPackage()}\n" + ) + + def startActivityForResult(self, intent: Intent, request_code: int, options: Optional["Bundle"] = None): + sys.stderr.write( + f"[Activity] startActivityForResult: code={request_code} " + f"action={intent.getAction()}\n" + ) + + def setResult(self, result_code: int, data: Optional[Intent] = None): + sys.stderr.write(f"[Activity] setResult: code={result_code}\n") + + def finish(self): + sys.stderr.write(f"[Activity] finish: {self.__class__.__name__}\n") + self.onPause() + self.onStop() + self.onDestroy() + + def finishActivity(self, request_code: int): + pass + + def getIntent(self) -> Optional[Intent]: + return self._intent + + def setIntent(self, intent: Intent): + self._intent = intent + + def getString(self, res_id: int, *args) -> str: + return str(res_id) + + def getResources(self): + return None + + def getApplication(self): + return self._application + + def getFragmentManager(self) -> FragmentManager: + return self._fragment_manager + + def getSupportFragmentManager(self) -> FragmentManager: + return self._fragment_manager + + def getPackageManager(self): + from .content_pm import PackageManager + return PackageManager() + + def getPackageName(self) -> str: + return _package_name + + def getContentResolver(self): + from .content import ContentResolver + return ContentResolver() + + def getSystemService(self, name: str): + return android_context.getSystemService(name) + + def isFinishing(self) -> bool: + return self._destroyed + + def isDestroyed(self) -> bool: + return self._destroyed + + def recreate(self): + self.onDestroy() + self.onCreate(None) + self.onStart() + self.onResume() + + def getPreferences(self, mode: int): + from .content import SharedPreferences + return SharedPreferences(_package_name + "_preferences") + + def getSharedPreferences(self, name: str, mode: int): + from .content import SharedPreferences + return SharedPreferences(name) + + def getApplicationContext(self): + return android_context + + def getBaseContext(self): + return android_context + + def getCallingActivity(self) -> Optional[ComponentName]: + return None + + def getCallingPackage(self) -> Optional[str]: + return None + + def getTaskId(self) -> int: + return 1 + + def hasWindowFocus(self) -> bool: + return True + + def isInMultiWindowMode(self) -> bool: + return False + + def isInPictureInPictureMode(self) -> bool: + return False + + def moveTaskToBack(self, non_root: bool) -> bool: + return True + + def onLowMemory(self): + pass + + def onTrimMemory(self, level: int): + pass + + def onConfigurationChanged(self, config): + pass + + def onMultiWindowModeChanged(self, is_in_multi_window_mode: bool, new_config): + pass + + def onPictureInPictureModeChanged(self, is_in_pip_mode: bool, new_config): + pass + + +# ═══════════════════════════════════════════════════════════════ +# Service +# ═══════════════════════════════════════════════════════════════ + +class Service(Stub): + """Service — 后台服务""" + + START_CONTINUATION_MASK = 0xF + START_FLAG_REDELIVERY = 1 + START_FLAG_RETRY = 2 + START_NOT_STICKY = 2 + START_REDELIVER_INTENT = 3 + START_STICKY = 1 + START_STICKY_COMPATIBILITY = 0 + STOP_FOREGROUND_DETACH = 1 + STOP_FOREGROUND_REMOVE = 2 + + def __init__(self): + super().__init__() + self._started = False + + def onCreate(self): + self._started = True + sys.stderr.write(f"[Service] onCreate: {self.__class__.__name__}\n") + + def onStartCommand(self, intent: Optional[Intent], flags: int, start_id: int) -> int: + return Service.START_NOT_STICKY + + def onBind(self, intent: Intent): + return None + + def onUnbind(self, intent: Intent) -> bool: + return False + + def onRebind(self, intent: Intent): + pass + + def onDestroy(self): + self._started = False + sys.stderr.write(f"[Service] onDestroy: {self.__class__.__name__}\n") + + def stopSelf(self): + self.onDestroy() + + def stopSelfResult(self, start_id: int) -> bool: + self.onDestroy() + return True + + def startForeground(self, id_: int, notification: Notification): + pass + + def stopForeground(self, flags: int): + pass + + def getApplication(self): + return None + + def getClass(self): + return self.__class__ + + +# ═══════════════════════════════════════════════════════════════ +# Application +# ═══════════════════════════════════════════════════════════════ + +class Application(Stub): + """Application — 应用级别上下文""" + + def __init__(self): + super().__init__() + self._activity_lifecycle_callbacks: List = [] + self._on_low_memory_listeners: List = [] + self._on_trim_memory_listeners: List = [] + + def onCreate(self): + sys.stderr.write(f"[Application] onCreate: {self.__class__.__name__}\n") + + def onTerminate(self): + pass + + def onConfigurationChanged(self, new_config): + pass + + def onLowMemory(self): + pass + + def onTrimMemory(self, level: int): + pass + + def registerActivityLifecycleCallbacks(self, callback): + self._activity_lifecycle_callbacks.append(callback) + + def unregisterActivityLifecycleCallbacks(self, callback): + try: + self._activity_lifecycle_callbacks.remove(callback) + except ValueError: + pass + + def getApplicationContext(self): + return android_context + + def getPackageManager(self): + from .content_pm import PackageManager + return PackageManager() + + def getContentResolver(self): + from .content import ContentResolver + return ContentResolver() + + def getSystemService(self, name: str): + return android_context.getSystemService(name) + + def getSharedPreferences(self, name: str, mode: int): + from .content import SharedPreferences + return SharedPreferences(name) + + def getFilesDir(self): + return os.path.join(_app_data_dir, "files") + + def getCacheDir(self): + return os.path.join(_app_data_dir, "cache") + + def getBaseContext(self): + return android_context + + +# ═══════════════════════════════════════════════════════════════ +# ActivityManager +# ═══════════════════════════════════════════════════════════════ + +class ActivityManager(Stub): + """ActivityManager — 管理 Activity 运行状态""" + + RECENT_IGNORE_UNAVAILABLE = 0x02 + RECENT_WITH_EXCLUDED = 0x01 + MOVE_TASK_WITH_HOME = 0x01 + MOVE_TASK_NO_USER_ACTION = 0x02 + + class RunningAppProcessInfo(Stub): + """Information about a running process""" + IMPORTANCE_FOREGROUND = 100 + IMPORTANCE_VISIBLE = 200 + IMPORTANCE_SERVICE = 300 + IMPORTANCE_BACKGROUND = 400 + IMPORTANCE_EMPTY = 500 + IMPORTANCE_FOREGROUND_SERVICE = 125 + IMPORTANCE_TOP_SLEEPING = 150 + IMPORTANCE_CACHED = 400 + IMPORTANCE_GONE = 1000 + + def __init__(self): + super().__init__() + self.processName = "" + self.pid = 0 + self.uid = 0 + self.importance = self.IMPORTANCE_FOREGROUND + self.lru = 0 + self.importanceReasonCode = 0 + self.importanceReasonPid = 0 + self.importanceReasonComponent = None + self.pkgList: List[str] = [] + + class AppTask(Stub): + """Represents a task managed by ActivityManager""" + def finishAndRemoveTask(self): + pass + def getTaskInfo(self): + return None + def moveToFront(self): + pass + def setExcludeFromRecents(self, exclude: bool): + pass + + class MemoryInfo(Stub): + """Memory information""" + def __init__(self): + super().__init__() + self.availMem = 0 + self.totalMem = 0 + self.threshold = 0 + self.lowMemory = False + + def __init__(self): + super().__init__() + self._memory_info = self.MemoryInfo() + + def getMemoryInfo(self) -> "ActivityManager.MemoryInfo": + return self._memory_info + + def getRunningAppProcesses(self) -> Optional[List["ActivityManager.RunningAppProcessInfo"]]: + return [] + + def killBackgroundProcesses(self, package_name: str): + pass + + def clearApplicationUserData(self) -> bool: + return True + + def getRecentTasks(self, max_num: int, flags: int) -> List: + return [] + + def getAppTasks(self) -> List["ActivityManager.AppTask"]: + return [] + + def moveTaskToFront(self, task_id: int, flags: int): + pass + + def isBackgroundRestricted(self) -> bool: + return False + + def isLowRamDevice(self) -> bool: + return False + + def getLauncherLargeIconSize(self) -> int: + return 48 + + +# ═══════════════════════════════════════════════════════════════ +# TaskStackBuilder +# ═══════════════════════════════════════════════════════════════ + +class TaskStackBuilder(Stub): + """Builder for synthetic back stack""" + + def __init__(self, context): + super().__init__() + self._context = context + self._intents: List[Intent] = [] + + @staticmethod + def create(context) -> "TaskStackBuilder": + return TaskStackBuilder(context) + + def addNextIntent(self, intent: Intent) -> "TaskStackBuilder": + self._intents.append(intent) + return self + + def addNextIntentWithParentStack(self, intent: Intent) -> "TaskStackBuilder": + self._intents.append(intent) + return self + + def addParentStack(self, activity_cls: type) -> "TaskStackBuilder": + return self + + def startActivities(self, options: Optional["Bundle"] = None): + for intent in self._intents: + sys.stderr.write(f"[TaskStackBuilder] start activity: {intent}\n") + + def startActivitiesIfNeeded(self, options: Optional["Bundle"] = None): + self.startActivities(options) + + def getIntent(self, request_code: int) -> Optional[Intent]: + if self._intents: + return self._intents[-1] + return None + + def editIntentAt(self, index: int) -> Optional[Intent]: + if 0 <= index < len(self._intents): + return self._intents[index] + return None + + def getIntentCount(self) -> int: + return len(self._intents) + + +# ═══════════════════════════════════════════════════════════════ +# Bundle (forward reference for Fragment uses) +# ═══════════════════════════════════════════════════════════════ + +from .os import Bundle + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Intent", + "ComponentName", + "PendingIntent", + "AlarmManager", + "Notification", + "NotificationChannel", + "NotificationChannelGroup", + "NotificationManager", + "NotificationCompat", + "Dialog", + "AlertDialog", + "ProgressDialog", + "DatePickerDialog", + "TimePickerDialog", + "Fragment", + "FragmentManager", + "FragmentTransaction", + "Activity", + "Service", + "Application", + "ActivityManager", + "TaskStackBuilder", +] \ No newline at end of file diff --git a/android_pyapi/android/bluetooth.py b/android_pyapi/android/bluetooth.py new file mode 100644 index 0000000..14c28c5 --- /dev/null +++ b/android_pyapi/android/bluetooth.py @@ -0,0 +1,791 @@ +""" +Android Bluetooth 组件 — BluetoothAdapter, BluetoothDevice, BluetoothGatt 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import uuid +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothProfile +# ═══════════════════════════════════════════════════════════════ + +class BluetoothProfile(Stub): + """BluetoothProfile — 蓝牙配置档案""" + + A2DP = 2 + HEALTH = 3 + HEADSET = 1 + GATT = 7 + GATT_SERVER = 8 + A2DP_SINK = 11 + AVRC = 12 + HEARING_AID = 21 + LE_AUDIO = 22 + LE_AUDIO_BROADCAST = 26 + + STATE_DISCONNECTED = 0 + STATE_CONNECTING = 1 + STATE_CONNECTED = 2 + STATE_DISCONNECTING = 3 + + def getConnectedDevices(self) -> List[Any]: + return [] + + def getDevicesMatchingConnectionState(self, state: int) -> List[Any]: + return [] + + def getConnectionState(self, device: Any) -> int: + return self.STATE_DISCONNECTED + + +class BluetoothProfileServiceListener(Stub): + """BluetoothProfile.ServiceListener""" + + def onServiceConnected(self, profile_id: int, proxy: Any): + pass + + def onServiceDisconnected(self, profile_id: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothClass +# ═══════════════════════════════════════════════════════════════ + +class BluetoothClass(Stub): + """BluetoothClass — 蓝牙设备类别""" + + class Device(Stub): + COMPUTER_DESKTOP = 0x100 + COMPUTER_LAPTOP = 0x10C + PHONE_SMART = 0x200 + PHONE_UNCATEGORIZED = 0x204 + AUDIO_VIDEO_WEARABLE_HEADSET = 0x404 + AUDIO_VIDEO_HANDSFREE = 0x408 + + class Major(Stub): + MISC = 0x0000 + COMPUTER = 0x0100 + PHONE = 0x0200 + NETWORKING = 0x0300 + AUDIO_VIDEO = 0x0400 + PERIPHERAL = 0x0500 + IMAGING = 0x0600 + WEARABLE = 0x0700 + TOY = 0x0800 + HEALTH = 0x0900 + UNCATEGORIZED = 0x1F00 + + def __init__(self): + super().__init__() + self._device_class = 0 + self._major_class = 0 + + def getDeviceClass(self) -> int: + return self._device_class + + def getMajorDeviceClass(self) -> int: + return self._major_class + + def hasService(self, service: int) -> bool: + return False + + def toString(self) -> str: + return f"BluetoothClass(device=0x{self._device_class:04X})" + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothSocket +# ═══════════════════════════════════════════════════════════════ + +class BluetoothSocket(Stub): + """BluetoothSocket — 蓝牙套接字""" + + TYPE_RFCOMM = 1 + TYPE_L2CAP = 3 + TYPE_SCO = 2 + + def __init__(self, type_: int = TYPE_RFCOMM): + super().__init__() + self._type = type_ + self._closed = False + self._connected = False + self._device: Optional["BluetoothDevice"] = None + + def connect(self): + self._connected = True + + def bind(self): + pass + + def getInputStream(self) -> Any: + return None + + def getOutputStream(self) -> Any: + return None + + def close(self): + self._closed = True + self._connected = False + + def getRemoteDevice(self) -> Optional["BluetoothDevice"]: + return self._device + + def getMaxReceivePacketSize(self) -> int: + return 0 + + def isConnected(self) -> bool: + return self._connected + + def isClosed(self) -> bool: + return self._closed + + +class BluetoothServerSocket(Stub): + """BluetoothServerSocket — 蓝牙服务端套接字""" + + def __init__(self): + super().__init__() + self._closed = False + + def accept(self, timeout: int = 0) -> Optional[BluetoothSocket]: + return None + + def close(self): + self._closed = True + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothDevice +# ═══════════════════════════════════════════════════════════════ + +class BluetoothDevice(Stub): + """BluetoothDevice — 蓝牙设备""" + + BOND_NONE = 10 + BOND_BONDING = 11 + BOND_BONDED = 12 + + DEVICE_TYPE_DUAL = 3 + DEVICE_TYPE_LE = 2 + DEVICE_TYPE_BREDR = 1 + DEVICE_TYPE_UNKNOWN = 0 + + ACTION_FOUND = "android.bluetooth.device.action.FOUND" + ACTION_NAME_CHANGED = "android.bluetooth.device.action.NAME_CHANGED" + ACTION_BOND_STATE_CHANGED = "android.bluetooth.device.action.BOND_STATE_CHANGED" + EXTRA_DEVICE = "android.bluetooth.device.extra.DEVICE" + EXTRA_NAME = "android.bluetooth.device.extra.NAME" + EXTRA_RSSI = "android.bluetooth.device.extra.RSSI" + EXTRA_BOND_STATE = "android.bluetooth.device.extra.BOND_STATE" + EXTRA_PREVIOUS_BOND_STATE = "android.bluetooth.device.extra.PREVIOUS_BOND_STATE" + + def __init__(self, address: str = "00:00:00:00:00:00"): + super().__init__() + self._address = address + self._name = "Unknown" + self._bond_state = self.BOND_NONE + self._device_type = self.DEVICE_TYPE_UNKNOWN + + def getAddress(self) -> str: + return self._address + + def getName(self) -> str: + return self._name + + def setName(self, name: str): + self._name = name + + def getBondState(self) -> int: + return self._bond_state + + def setBondState(self, state: int): + self._bond_state = state + + def getType(self) -> int: + return self._device_type + + def untergehende(self) -> bool: + return self._bond_state == self.BOND_BONDED + + def createRfcommSocketToServiceRecord(self, uuid_str: str) -> BluetoothSocket: + sock = BluetoothSocket(BluetoothSocket.TYPE_RFCOMM) + sock._device = self + return sock + + def createInsecureRfcommSocketToServiceRecord(self, uuid_str: str) -> BluetoothSocket: + sock = BluetoothSocket(BluetoothSocket.TYPE_RFCOMM) + sock._device = self + return sock + + def createL2capChannel(self, psm: int) -> BluetoothSocket: + sock = BluetoothSocket(BluetoothSocket.TYPE_L2CAP) + sock._device = self + return sock + + def createInsecureL2capChannel(self, psm: int) -> BluetoothSocket: + sock = BluetoothSocket(BluetoothSocket.TYPE_L2CAP) + sock._device = self + return sock + + def fetchUuidsWithSdp(self) -> bool: + return False + + def uuidEquals(self, uuid_str: str) -> bool: + return False + + def setPairingConfirmation(self, accept: bool): + pass + + def setPin(self, pin: bytes) -> bool: + return True + + def toString(self) -> str: + return f"BluetoothDevice(address={self._address}, name={self._name})" + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothGattDescriptor +# ═══════════════════════════════════════════════════════════════ + +class BluetoothGattDescriptor(Stub): + """BluetoothGattDescriptor — GATT 描述符""" + + ENABLE_NOTIFICATION_VALUE = bytes([0x01, 0x00]) + ENABLE_INDICATION_VALUE = bytes([0x02, 0x00]) + DISABLE_NOTIFICATION_VALUE = bytes([0x00, 0x00]) + + def __init__(self, uuid_str: str, permissions: int = 0): + super().__init__() + self._uuid = uuid_str + self._value: Optional[bytes] = None + self._permissions = permissions + self._characteristic: Optional["BluetoothGattCharacteristic"] = None + + def getUuid(self) -> str: + return self._uuid + + def getValue(self) -> Optional[bytes]: + return self._value + + def setValue(self, value: bytes): + self._value = value + + def getCharacteristic(self) -> Optional["BluetoothGattCharacteristic"]: + return self._characteristic + + def getPermissions(self) -> int: + return self._permissions + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothGattCharacteristic +# ═══════════════════════════════════════════════════════════════ + +class BluetoothGattCharacteristic(Stub): + """BluetoothGattCharacteristic — GATT 特征""" + + PROPERTY_BROADCAST = 1 + PROPERTY_READ = 2 + PROPERTY_WRITE_NO_RESPONSE = 4 + PROPERTY_WRITE = 8 + PROPERTY_NOTIFY = 16 + PROPERTY_INDICATE = 32 + PROPERTY_SIGNED_WRITE = 64 + PROPERTY_EXTENDED_PROPS = 128 + + WRITE_TYPE_NO_RESPONSE = 1 + WRITE_TYPE_DEFAULT = 2 + WRITE_TYPE_SIGNED = 4 + + PERMISSION_READ = 1 + PERMISSION_READ_ENCRYPTED = 2 + PERMISSION_READ_ENCRYPTED_MITM = 4 + PERMISSION_WRITE = 16 + PERMISSION_WRITE_ENCRYPTED = 32 + PERMISSION_WRITE_ENCRYPTED_MITM = 64 + + FORMAT_FLOAT = 0x20 + FORMAT_SFLOAT = 0x10 + FORMAT_SINT16 = 0x02 + FORMAT_SINT32 = 0x04 + FORMAT_SINT8 = 0x01 + FORMAT_UINT16 = 0x12 + FORMAT_UINT32 = 0x14 + FORMAT_UINT8 = 0x11 + + def __init__(self, uuid_str: str, properties: int = 0, permissions: int = 0): + super().__init__() + self._uuid = uuid_str + self._properties = properties + self._permissions = permissions + self._write_type = self.WRITE_TYPE_DEFAULT + self._value: Optional[bytes] = None + self._service: Optional["BluetoothGattService"] = None + self._descriptors: List[BluetoothGattDescriptor] = [] + + def getUuid(self) -> str: + return self._uuid + + def getService(self) -> Optional["BluetoothGattService"]: + return self._service + + def getPermissions(self) -> int: + return self._permissions + + def getProperties(self) -> int: + return self._properties + + def getWriteType(self) -> int: + return self._write_type + + def setWriteType(self, write_type: int): + self._write_type = write_type + + def getValue(self) -> Optional[bytes]: + return self._value + + def setValue(self, value: bytes): + self._value = value + + def getDescriptors(self) -> List[BluetoothGattDescriptor]: + return list(self._descriptors) + + def getDescriptor(self, uuid_str: str) -> Optional[BluetoothGattDescriptor]: + for d in self._descriptors: + if d.getUuid() == uuid_str: + return d + return None + + def addDescriptor(self, descriptor: BluetoothGattDescriptor): + self._descriptors.append(descriptor) + descriptor._characteristic = self + + def getIntValue(self, format_type: int, offset: int) -> Optional[int]: + if self._value is None: + return None + import struct + try: + fmt_map = { + self.FORMAT_UINT8: ("B", 1), self.FORMAT_SINT8: ("b", 1), + self.FORMAT_UINT16: ("H", 2), self.FORMAT_SINT16: ("h", 2), + self.FORMAT_UINT32: ("I", 4), self.FORMAT_SINT32: ("i", 4), + self.FORMAT_FLOAT: ("f", 4), self.FORMAT_SFLOAT: ("e", 2), + } + fmt, size = fmt_map.get(format_type, ("B", 1)) + if offset + size > len(self._value): + return None + val = struct.unpack(fmt, self._value[offset:offset + size])[0] + return int(val) + except Exception: + return None + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothGattService +# ═══════════════════════════════════════════════════════════════ + +class BluetoothGattService(Stub): + """BluetoothGattService — GATT 服务""" + + SERVICE_TYPE_PRIMARY = 0 + SERVICE_TYPE_SECONDARY = 1 + + def __init__(self, uuid_str: str, service_type: int = SERVICE_TYPE_PRIMARY): + super().__init__() + self._uuid = uuid_str + self._type = service_type + self._characteristics: List[BluetoothGattCharacteristic] = [] + self._included_services: List["BluetoothGattService"] = [] + self._device: Optional[BluetoothDevice] = None + + def getUuid(self) -> str: + return self._uuid + + def getType(self) -> int: + return self._type + + def getCharacteristics(self) -> List[BluetoothGattCharacteristic]: + return list(self._characteristics) + + def getCharacteristic(self, uuid_str: str) -> Optional[BluetoothGattCharacteristic]: + for c in self._characteristics: + if c.getUuid() == uuid_str: + return c + return None + + def addCharacteristic(self, characteristic: BluetoothGattCharacteristic): + self._characteristics.append(characteristic) + characteristic._service = self + + def getIncludedServices(self) -> List["BluetoothGattService"]: + return list(self._included_services) + + def addIncludedService(self, service: "BluetoothGattService"): + self._included_services.append(service) + + def getDevice(self) -> Optional[BluetoothDevice]: + return self._device + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothGattCallback +# ═══════════════════════════════════════════════════════════════ + +class BluetoothGattCallback(Stub): + """BluetoothGattCallback — GATT 回调""" + + def onConnectionStateChange(self, gatt: "BluetoothGatt", status: int, new_state: int): + pass + + def onServicesDiscovered(self, gatt: "BluetoothGatt", status: int): + pass + + def onCharacteristicRead(self, gatt: "BluetoothGatt", + characteristic: BluetoothGattCharacteristic, value: bytes, status: int): + pass + + def onCharacteristicWrite(self, gatt: "BluetoothGatt", + characteristic: BluetoothGattCharacteristic, status: int): + pass + + def onDescriptorRead(self, gatt: "BluetoothGatt", + descriptor: BluetoothGattDescriptor, status: int): + pass + + def onDescriptorWrite(self, gatt: "BluetoothGatt", + descriptor: BluetoothGattDescriptor, status: int): + pass + + def onCharacteristicChanged(self, gatt: "BluetoothGatt", + characteristic: BluetoothGattCharacteristic, value: bytes): + pass + + def onReadRemoteRssy(self, gatt: "BluetoothGatt", rssi: int, status: int): + pass + + def onMtuChanged(self, gatt: "BluetoothGatt", mtu: int, status: int): + pass + + def onPhyUpdate(self, gatt: "BluetoothGatt", tx_phy: int, rx_phy: int, status: int): + pass + + def onPhyRead(self, gatt: "BluetoothGatt", tx_phy: int, rx_phy: int, status: int): + pass + + def onServiceChanged(self, gatt: "BluetoothGatt"): + pass + + def onNotificationSent(self, gatt: "BluetoothGatt", status: int): + pass + + def onConnectionUpdated(self, gatt: "BluetoothGatt", interval: int, + latency: int, timeout: int, status: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothGatt +# ═══════════════════════════════════════════════════════════════ + +class BluetoothGatt(Stub): + """BluetoothGatt — GATT 客户端""" + + CONNECTION_STATE_DISCONNECTED = 0 + CONNECTION_STATE_CONNECTING = 1 + CONNECTION_STATE_CONNECTED = 2 + CONNECTION_STATE_DISCONNECTING = 3 + + GATT_SUCCESS = 0 + GATT_FAILURE = 1 + GATT_CONNECTION_CONGESTED = 143 + GATT_INSUFFICIENT_AUTHENTICATION = 5 + GATT_INSUFFICIENT_ENCRYPTION = 15 + GATT_INSUFFICIENT_KEY_SIZE = 12 + GATT_INVALID_ATTRIBUTE_LENGTH = 13 + GATT_REQUEST_NOT_SUPPORTED = 6 + GATT_WRITE_NOT_PERMITTED = 3 + GATT_READ_NOT_PERMITTED = 2 + GATT_WRITE_NO_RESPONSE = 1 + GATT_WRITE_DEFAULT = 0 + + def __init__(self, device: BluetoothDevice): + super().__init__() + self._device = device + self._services: List[BluetoothGattService] = [] + self._connected = False + self._mtu = 23 + + def connect(self) -> bool: + self._connected = True + return True + + def disconnect(self): + self._connected = False + + def close(self): + self._connected = False + self._services.clear() + + def discoverServices(self) -> bool: + return True + + def getServices(self) -> List[BluetoothGattService]: + return list(self._services) + + def getService(self, uuid_str: str) -> Optional[BluetoothGattService]: + for s in self._services: + if s.getUuid() == uuid_str: + return s + return None + + def getConnectedDevices(self) -> List[BluetoothDevice]: + return [self._device] if self._connected else [] + + def getDevicesMatchingConnectionState(self, state: int) -> List[BluetoothDevice]: + if state == self.CONNECTION_STATE_CONNECTED and self._connected: + return [self._device] + return [] + + def readCharacteristic(self, characteristic: BluetoothGattCharacteristic) -> bool: + return True + + def writeCharacteristic(self, characteristic: BluetoothGattCharacteristic) -> bool: + return True + + def readDescriptor(self, descriptor: BluetoothGattDescriptor) -> bool: + return True + + def writeDescriptor(self, descriptor: BluetoothGattDescriptor) -> bool: + return True + + def setCharacteristicNotification(self, characteristic: BluetoothGattCharacteristic, + enable: bool) -> bool: + return True + + def readRemoteRssy(self) -> bool: + return True + + def requestMtu(self, mtu: int) -> bool: + self._mtu = mtu + return True + + def getMtu(self) -> int: + return self._mtu + + def requestConnectionPriority(self, connection_priority: int) -> bool: + return True + + def getDevice(self) -> BluetoothDevice: + return self._device + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothManager +# ═══════════════════════════════════════════════════════════════ + +class BluetoothManager(Stub): + """BluetoothManager — 蓝牙管理器""" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + + def getAdapter(self) -> Optional["BluetoothAdapter"]: + return BluetoothAdapter.getDefaultAdapter() + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothAdapter +# ═══════════════════════════════════════════════════════════════ + +class BluetoothAdapter(Stub): + """BluetoothAdapter — 蓝牙适配器""" + + STATE_OFF = 10 + STATE_TURNING_ON = 11 + STATE_ON = 12 + STATE_TURNING_OFF = 13 + + ACTION_REQUEST_ENABLE = "android.bluetooth.adapter.action.REQUEST_ENABLE" + ACTION_REQUEST_DISCOVERABLE = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE" + ACTION_STATE_CHANGED = "android.bluetooth.adapter.action.STATE_CHANGED" + ACTION_DISCOVERY_STARTED = "android.bluetooth.adapter.action.DISCOVERY_STARTED" + ACTION_DISCOVERY_FINISHED = "android.bluetooth.adapter.action.DISCOVERY_FINISHED" + + SCAN_MODE_NONE = 20 + SCAN_MODE_CONNECTABLE = 21 + SCAN_MODE_CONNECTABLE_DISCOVERABLE = 23 + + _default_adapter: Optional["BluetoothAdapter"] = None + + def __init__(self): + super().__init__() + self._enabled = True + self._state = self.STATE_ON + self._address = "00:1A:2B:3C:4D:5E" + self._name = "MockBluetooth" + self._discovering = False + self._bonded_devices: List[BluetoothDevice] = [] + + @staticmethod + def getDefaultAdapter() -> Optional["BluetoothAdapter"]: + if BluetoothAdapter._default_adapter is None: + BluetoothAdapter._default_adapter = BluetoothAdapter() + return BluetoothAdapter._default_adapter + + def isEnabled(self) -> bool: + return self._enabled + + def enable(self) -> bool: + self._enabled = True + self._state = self.STATE_ON + return True + + def disable(self) -> bool: + self._enabled = False + self._state = self.STATE_OFF + return True + + def getAddress(self) -> str: + return self._address + + def getName(self) -> str: + return self._name + + def setName(self, name: str) -> bool: + self._name = name + return True + + def getBondedDevices(self) -> Set[BluetoothDevice]: + return set(self._bonded_devices) + + def startDiscovery(self) -> bool: + self._discovering = True + return True + + def cancelDiscovery(self) -> bool: + self._discovering = False + return True + + def isDiscovering(self) -> bool: + return self._discovering + + @staticmethod + def checkBluetoothAddress(address: str) -> bool: + import re + return bool(re.match(r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$", address)) + + def getProfileProxy(self, context: Any, listener: BluetoothProfileServiceListener, + profile_id: int) -> bool: + return True + + def closeProfileProxy(self, profile_id: int, proxy: Any): + pass + + def getState(self) -> int: + return self._state + + def listenUsingRfcommWithServiceRecord(self, name: str, uuid_str: str) -> BluetoothServerSocket: + return BluetoothServerSocket() + + def listenUsingInsecureRfcommWithServiceRecord(self, name: str, + uuid_str: str) -> BluetoothServerSocket: + return BluetoothServerSocket() + + def getRemoteDevice(self, address: str) -> Optional[BluetoothDevice]: + if not BluetoothAdapter.checkBluetoothAddress(address): + return None + return BluetoothDevice(address) + + def getLeScanner(self) -> Any: + from .bluetooth_le import BluetoothLeScanner + return BluetoothLeScanner() + + def toString(self) -> str: + return f"BluetoothAdapter(name={self._name})" + + +# ═══════════════════════════════════════════════════════════════ +# Additional profile stubs +# ═══════════════════════════════════════════════════════════════ + +class BluetoothHeadset(Stub): + pass + + +class BluetoothA2dp(Stub): + pass + + +class BluetoothHealth(Stub): + pass + + +class BluetoothHealthCallback(Stub): + pass + + +class BluetoothHealthAppConfiguration(Stub): + pass + + +class BluetoothCodecConfig(Stub): + pass + + +class BluetoothLeAudio(Stub): + pass + + +class BluetoothLeBroadcast(Stub): + pass + + +class BluetoothLeBroadcastMetadata(Stub): + pass + + +class BluetoothLeBroadcastSubgroup(Stub): + pass + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "BluetoothAdapter", + "BluetoothDevice", + "BluetoothServerSocket", + "BluetoothSocket", + "BluetoothClass", + "BluetoothGatt", + "BluetoothGattCallback", + "BluetoothGattCharacteristic", + "BluetoothGattDescriptor", + "BluetoothGattService", + "BluetoothHeadset", + "BluetoothA2dp", + "BluetoothHealth", + "BluetoothHealthCallback", + "BluetoothHealthAppConfiguration", + "BluetoothProfile", + "BluetoothProfileServiceListener", + "BluetoothManager", + "BluetoothCodecConfig", + "BluetoothLeAudio", + "BluetoothLeBroadcast", + "BluetoothLeBroadcastMetadata", + "BluetoothLeBroadcastSubgroup", +] \ No newline at end of file diff --git a/android_pyapi/android/bluetooth_le.py b/android_pyapi/android/bluetooth_le.py new file mode 100644 index 0000000..0a8d311 --- /dev/null +++ b/android_pyapi/android/bluetooth_le.py @@ -0,0 +1,512 @@ +""" +Android BLE 组件 — BluetoothLeScanner, ScanCallback, ScanFilter 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# ScanRecord +# ═══════════════════════════════════════════════════════════════ + +class ScanRecord(Stub): + """ScanRecord — 扫描记录""" + + def __init__(self): + super().__init__() + self._service_uuids: List[str] = [] + self._service_solicitation_uuids: List[str] = [] + self._service_data: Dict[str, bytes] = {} + self._manufacturer_specific_data: Dict[int, bytes] = {} + self._tx_power_level: int = -127 + self._device_name: str = "" + self._advertise_flags: int = 0 + self._bytes_data: bytes = b"" + + def getServiceUuids(self) -> Optional[List[str]]: + return list(self._service_uuids) if self._service_uuids else None + + def getServiceSolicitationUuids(self) -> Optional[List[str]]: + return list(self._service_solicitation_uuids) if self._service_solicitation_uuids else None + + def getServiceData(self) -> Optional[Dict[str, bytes]]: + return dict(self._service_data) if self._service_data else None + + def getServiceDataForService(self, uuid_str: str) -> Optional[bytes]: + return self._service_data.get(uuid_str) + + def getManufacturerSpecificData(self) -> Optional[Dict[int, bytes]]: + return dict(self._manufacturer_specific_data) if self._manufacturer_specific_data else None + + def getTxPowerLevel(self) -> int: + return self._tx_power_level + + def getDeviceName(self) -> Optional[str]: + return self._device_name or None + + def getAdvertiseFlags(self) -> int: + return self._advertise_flags + + def getBytes(self) -> bytes: + return self._bytes_data + + +# ═══════════════════════════════════════════════════════════════ +# ScanFilter +# ═══════════════════════════════════════════════════════════════ + +class ScanFilter(Stub): + """ScanFilter — 扫描过滤器""" + + class Builder(Stub): + def __init__(self): + super().__init__() + self._device_name: Optional[str] = None + self._device_address: Optional[str] = None + self._service_uuid: Optional[str] = None + self._service_data: Optional[bytes] = None + self._manufacturer_id: int = -1 + self._manufacturer_data: Optional[bytes] = None + + def setDeviceName(self, name: str) -> "ScanFilter.Builder": + self._device_name = name + return self + + def setDeviceAddress(self, address: str) -> "ScanFilter.Builder": + self._device_address = address + return self + + def setServiceUuid(self, uuid_str: str) -> "ScanFilter.Builder": + self._service_uuid = uuid_str + return self + + def setServiceData(self, data: bytes) -> "ScanFilter.Builder": + self._service_data = data + return self + + def setManufacturerData(self, manufacturer_id: int, data: bytes) -> "ScanFilter.Builder": + self._manufacturer_id = manufacturer_id + self._manufacturer_data = data + return self + + def build(self) -> "ScanFilter": + return ScanFilter( + device_name=self._device_name, + device_address=self._device_address, + service_uuid=self._service_uuid, + service_data=self._service_data, + manufacturer_id=self._manufacturer_id, + manufacturer_data=self._manufacturer_data, + ) + + def __init__(self, device_name: Optional[str] = None, + device_address: Optional[str] = None, + service_uuid: Optional[str] = None, + service_data: Optional[bytes] = None, + manufacturer_id: int = -1, + manufacturer_data: Optional[bytes] = None): + super().__init__() + self._device_name = device_name + self._device_address = device_address + self._service_uuid = service_uuid + self._service_data = service_data + self._manufacturer_id = manufacturer_id + self._manufacturer_data = manufacturer_data + + +class ScanFilterBuilder(ScanFilter.Builder): + """ScanFilter.Builder 别名""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# ScanSettings +# ═══════════════════════════════════════════════════════════════ + +class ScanSettings(Stub): + """ScanSettings — 扫描设置""" + + SCAN_MODE_OPPORTUNISTIC = -1 + SCAN_MODE_LOW_POWER = 0 + SCAN_MODE_BALANCED = 1 + SCAN_MODE_LOW_LATENCY = 2 + + CALLBACK_TYPE_ALL_MATCHES = 1 + CALLBACK_TYPE_FIRST_MATCH = 2 + CALLBACK_TYPE_MATCH_LOST = 4 + + MATCH_NUM_FEW_CFG = 2 + MATCH_NUM_MAX_CFG = 1 + MATCH_MODE_AGGRESSIVE = 0 + MATCH_MODE_STICKY = 1 + + class Builder(Stub): + def __init__(self): + super().__init__() + self._scan_mode = ScanSettings.SCAN_MODE_LOW_POWER + self._callback_type = ScanSettings.CALLBACK_TYPE_ALL_MATCHES + self._legacy = True + self._phy = 1 + + def setScanMode(self, scan_mode: int) -> "ScanSettings.Builder": + self._scan_mode = scan_mode + return self + + def setCallbackType(self, callback_type: int) -> "ScanSettings.Builder": + self._callback_type = callback_type + return self + + def setLegacy(self, legacy: bool) -> "ScanSettings.Builder": + self._legacy = legacy + return self + + def setPhy(self, phy: int) -> "ScanSettings.Builder": + self._phy = phy + return self + + def build(self) -> "ScanSettings": + return ScanSettings( + scan_mode=self._scan_mode, + callback_type=self._callback_type, + legacy=self._legacy, + phy=self._phy, + ) + + def __init__(self, scan_mode: int = SCAN_MODE_LOW_POWER, + callback_type: int = CALLBACK_TYPE_ALL_MATCHES, + legacy: bool = True, phy: int = 1): + super().__init__() + self._scan_mode = scan_mode + self._callback_type = callback_type + self._legacy = legacy + self._phy = phy + + def getScanMode(self) -> int: + return self._scan_mode + + def getCallbackType(self) -> int: + return self._callback_type + + def getLegacy(self) -> bool: + return self._legacy + + def getPhy(self) -> int: + return self._phy + + +# ═══════════════════════════════════════════════════════════════ +# ScanResult +# ═══════════════════════════════════════════════════════════════ + +class ScanResult(Stub): + """ScanResult — BLE 扫描结果""" + + DATA_COMPLETE = 0 + DATA_TRUNCATED = 2 + PHY_LE_1M = 1 + PHY_LE_2M = 2 + PHY_LE_CODED = 3 + PERIODIC_INTERVAL_NOT_PRESENT = 0 + + def __init__(self, device: Any = None, scan_record: Optional[ScanRecord] = None, + rssi: int = -127, timestamp_nanos: int = 0): + super().__init__() + self._device = device + self._scan_record = scan_record or ScanRecord() + self._rssi = rssi + self._timestamp_nanos = timestamp_nanos + self._primary_phy = self.PHY_LE_1M + self._secondary_phy = 0 + self._advertising_sid = -1 + self._tx_power = -127 + self._periodic_interval = self.PERIODIC_INTERVAL_NOT_PRESENT + self._data_status = self.DATA_COMPLETE + + def getDevice(self) -> Any: + return self._device + + def getScanRecord(self) -> Optional[ScanRecord]: + return self._scan_record + + def getRssi(self) -> int: + return self._rssi + + def getTimestampNanos(self) -> int: + return self._timestamp_nanos + + def getPrimaryPhy(self) -> int: + return self._primary_phy + + def getSecondaryPhy(self) -> int: + return self._secondary_phy + + def getAdvertisingSid(self) -> int: + return self._advertising_sid + + def getTxPower(self) -> int: + return self._tx_power + + def getPeriodicAdvertisingInterval(self) -> int: + return self._periodic_interval + + def getDataStatus(self) -> int: + return self._data_status + + +# ═══════════════════════════════════════════════════════════════ +# ScanCallback +# ═══════════════════════════════════════════════════════════════ + +class ScanCallback(Stub): + """ScanCallback — 扫描回调""" + + SCAN_FAILED_ALREADY_STARTED = 1 + SCAN_FAILED_APPLICATION_REGISTRATION_FAILED = 2 + SCAN_FAILED_INTERNAL_ERROR = 3 + SCAN_FAILED_FEATURE_UNSUPPORTED = 4 + SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES = 5 + SCAN_FAILED_SCANNING_TOO_FREQUENTLY = 6 + + def onScanResult(self, callback_type: int, result: ScanResult): + pass + + def onBatchScanResults(self, results: List[ScanResult]): + pass + + def onScanFailed(self, error_code: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# BluetoothLeScanner +# ═══════════════════════════════════════════════════════════════ + +class BluetoothLeScanner(Stub): + """BluetoothLeScanner — BLE 扫描器""" + + def __init__(self): + super().__init__() + + def startScan(self, filters: Optional[List[ScanFilter]] = None, + settings: Optional[ScanSettings] = None, + callback: Optional[ScanCallback] = None): + pass + + def startScanWithFilters(self, filters: List[ScanFilter], + callback: ScanCallback): + self.startScan(filters, None, callback) + + def stopScan(self, callback: Optional[ScanCallback] = None): + pass + + def flushPendingScanResults(self, callback: ScanCallback): + pass + + +# ═══════════════════════════════════════════════════════════════ +# AdvertiseData / AdvertiseSettings / AdvertiseCallback +# ═══════════════════════════════════════════════════════════════ + +class AdvertiseData(Stub): + """AdvertiseData — 广播数据""" + + class Builder(Stub): + def __init__(self): + super().__init__() + self._include_device_name = False + self._include_tx_power_level = False + self._service_uuids: List[str] = [] + self._service_data: Dict[str, bytes] = {} + self._manufacturer_data: Dict[int, bytes] = {} + + def setIncludeDeviceName(self, include: bool) -> "AdvertiseData.Builder": + self._include_device_name = include + return self + + def setIncludeTxPowerLevel(self, include: bool) -> "AdvertiseData.Builder": + self._include_tx_power_level = include + return self + + def addServiceUuid(self, uuid_str: str) -> "AdvertiseData.Builder": + self._service_uuids.append(uuid_str) + return self + + def addServiceData(self, uuid_str: str, data: bytes) -> "AdvertiseData.Builder": + self._service_data[uuid_str] = data + return self + + def addManufacturerData(self, manufacturer_id: int, data: bytes) -> "AdvertiseData.Builder": + self._manufacturer_data[manufacturer_id] = data + return self + + def build(self) -> "AdvertiseData": + return AdvertiseData( + include_device_name=self._include_device_name, + include_tx_power_level=self._include_tx_power_level, + service_uuids=self._service_uuids, + service_data=self._service_data, + manufacturer_data=self._manufacturer_data, + ) + + def __init__(self, include_device_name: bool = False, + include_tx_power_level: bool = False, + service_uuids: Optional[List[str]] = None, + service_data: Optional[Dict[str, bytes]] = None, + manufacturer_data: Optional[Dict[int, bytes]] = None): + super().__init__() + self._include_device_name = include_device_name + self._include_tx_power_level = include_tx_power_level + self._service_uuids = service_uuids or [] + self._service_data = service_data or {} + self._manufacturer_data = manufacturer_data or {} + + +class AdvertiseDataBuilder(AdvertiseData.Builder): + pass + + +class AdvertiseSettings(Stub): + """AdvertiseSettings — 广播设置""" + + ADVERTISE_MODE_LOW_POWER = 0 + ADVERTISE_MODE_BALANCED = 1 + ADVERTISE_MODE_LOW_LATENCY = 2 + + ADVERTISE_TX_POWER_ULTRA_LOW = 0 + ADVERTISE_TX_POWER_LOW = 1 + ADVERTISE_TX_POWER_MEDIUM = 2 + ADVERTISE_TX_POWER_HIGH = 3 + + class Builder(Stub): + def __init__(self): + super().__init__() + self._advertise_mode = AdvertiseSettings.ADVERTISE_MODE_LOW_POWER + self._tx_power_level = AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM + self._connectable = True + self._timeout = 0 + + def setAdvertiseMode(self, mode: int) -> "AdvertiseSettings.Builder": + self._advertise_mode = mode + return self + + def setTxPowerLevel(self, power: int) -> "AdvertiseSettings.Builder": + self._tx_power_level = power + return self + + def setConnectable(self, connectable: bool) -> "AdvertiseSettings.Builder": + self._connectable = connectable + return self + + def setTimeout(self, timeout: int) -> "AdvertiseSettings.Builder": + self._timeout = timeout + return self + + def build(self) -> "AdvertiseSettings": + return AdvertiseSettings( + advertise_mode=self._advertise_mode, + tx_power_level=self._tx_power_level, + connectable=self._connectable, + timeout=self._timeout, + ) + + def __init__(self, advertise_mode: int = ADVERTISE_MODE_LOW_POWER, + tx_power_level: int = ADVERTISE_TX_POWER_MEDIUM, + connectable: bool = True, timeout: int = 0): + super().__init__() + self._advertise_mode = advertise_mode + self._tx_power_level = tx_power_level + self._connectable = connectable + self._timeout = timeout + + def getAdvertiseMode(self) -> int: + return self._advertise_mode + + def getTxPowerLevel(self) -> int: + return self._tx_power_level + + def isConnectable(self) -> bool: + return self._connectable + + def getTimeout(self) -> int: + return self._timeout + + +class AdvertiseCallback(Stub): + """AdvertiseCallback — 广播回调""" + + ADVERTISE_FAILED_DATA_TOO_LARGE = 1 + ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 2 + ADVERTISE_FAILED_ALREADY_STARTED = 3 + ADVERTISE_FAILED_INTERNAL_ERROR = 4 + ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 5 + + def onStartSuccess(self, settings_in_effect: AdvertiseSettings): + pass + + def onStartFailure(self, error_code: int): + pass + + +class BluetoothLeAdvertiser(Stub): + """BluetoothLeAdvertiser — BLE 广播器""" + + def startAdvertising(self, settings: AdvertiseSettings, + advertise_data: AdvertiseData, + scan_response: Optional[AdvertiseData], + callback: AdvertiseCallback): + try: + callback.onStartSuccess(settings) + except Exception: + pass + + def stopAdvertising(self, callback: AdvertiseCallback): + pass + + +class PeriodicAdvertisingManager(Stub): + pass + + +class PeriodicAdvertisingCallback(Stub): + pass + + +class DistanceMeasurementManager(Stub): + pass + + +class DistanceMeasurementResult(Stub): + pass + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "BluetoothLeScanner", + "ScanCallback", + "ScanResult", + "ScanFilter", + "ScanFilterBuilder", + "ScanSettings", + "ScanRecord", + "BluetoothLeAdvertiser", + "AdvertiseCallback", + "AdvertiseData", + "AdvertiseDataBuilder", + "AdvertiseSettings", + "PeriodicAdvertisingManager", + "PeriodicAdvertisingCallback", + "DistanceMeasurementManager", + "DistanceMeasurementResult", +] \ No newline at end of file diff --git a/android_pyapi/android/content.py b/android_pyapi/android/content.py new file mode 100644 index 0000000..7ec2547 --- /dev/null +++ b/android_pyapi/android/content.py @@ -0,0 +1,1671 @@ +""" +Android Content 组件 — Context, ContentResolver, Uri, SharedPreferences 等 +方法签名完全对齐 Android SDK +""" + +import json +import os +import sys +import threading +from typing import Any, Dict, List, Optional, Set, Tuple, Union +from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, quote, unquote + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# Uri +# ═══════════════════════════════════════════════════════════════ + +class Uri(Stub): + """Android Uri — 统一资源标识符""" + + def __init__(self, scheme: str = "", authority: str = "", path: str = "", + query: str = "", fragment: str = "", host: str = "", port: int = -1, + ssp: str = "", user_info: str = ""): + super().__init__() + self._scheme = scheme + self._authority = authority + self._path = path + self._query = query + self._fragment = fragment + self._host = host + self._port = port + self._ssp = ssp + self._user_info = user_info + self._opaque = bool(ssp) + self._cached_string: Optional[str] = None + + @staticmethod + def parse(uri_string: str) -> "Uri": + parsed = urlparse(uri_string) + host = parsed.hostname or "" + port = parsed.port or -1 + return Uri( + scheme=parsed.scheme, + authority=parsed.netloc, + path=parsed.path, + query=parsed.query, + fragment=parsed.fragment, + host=host, + port=port, + user_info=parsed.username or "", + ) + + @staticmethod + def fromParts(scheme: str, ssp: str, fragment: str) -> "Uri": + uri = Uri(scheme=scheme, ssp=ssp, fragment=fragment) + uri._opaque = True + return uri + + @staticmethod + def withAppendedPath(base_uri: "Uri", path_segment: str) -> "Uri": + new_path = base_uri._path.rstrip("/") + "/" + path_segment + return Uri( + scheme=base_uri._scheme, + authority=base_uri._authority, + path=new_path, + query=base_uri._query, + fragment=base_uri._fragment, + host=base_uri._host, + port=base_uri._port, + ) + + @staticmethod + def encode(s: str) -> str: + return quote(s, safe=":/?#[]@!$&'()*+,;=-._~") + + @staticmethod + def decode(s: str) -> str: + return unquote(s) + + def buildUpon(self) -> "Uri.Builder": + b = Uri.Builder() + b._scheme = self._scheme + b._authority = self._authority + b._path = self._path + b._query = self._query + b._fragment = self._fragment + return b + + def getScheme(self) -> Optional[str]: + return self._scheme or None + + def getAuthority(self) -> Optional[str]: + return self._authority or None + + def getPath(self) -> Optional[str]: + return self._path or None + + def getQuery(self) -> Optional[str]: + return self._query or None + + def getFragment(self) -> Optional[str]: + return self._fragment or None + + def getHost(self) -> Optional[str]: + return self._host or None + + def getPort(self) -> int: + return self._port + + def getPathSegments(self) -> List[str]: + return [s for s in self._path.split("/") if s] if self._path else [] + + def getLastPathSegment(self) -> Optional[str]: + segments = self.getPathSegments() + return segments[-1] if segments else None + + def getUserInfo(self) -> Optional[str]: + return self._user_info or None + + def isAbsolute(self) -> bool: + return bool(self._scheme) + + def isOpaque(self) -> bool: + return self._opaque + + def isHierarchical(self) -> bool: + return not self._opaque + + def getSchemeSpecificPart(self) -> Optional[str]: + return self._ssp or None + + def normalizeScheme(self) -> "Uri": + if self._scheme: + self._scheme = self._scheme.lower() + return self + + def toString(self) -> str: + if self._cached_string is not None: + return self._cached_string + if self._opaque and self._ssp: + result = f"{self._scheme}:{self._ssp}" + if self._fragment: + result += f"#{self._fragment}" + else: + parts = [] + if self._scheme: + parts.append(f"{self._scheme}:") + if self._authority: + parts.append(f"//{self._authority}") + if self._path: + parts.append(self._path) + if self._query: + parts.append(f"?{self._query}") + if self._fragment: + parts.append(f"#{self._fragment}") + result = "".join(parts) + self._cached_string = result + return result + + def __repr__(self): + return self.toString() + + def __str__(self): + return self.toString() + + def __eq__(self, other): + if isinstance(other, Uri): + return self.toString() == other.toString() + return NotImplemented + + def __hash__(self): + return hash(self.toString()) + + class Builder(Stub): + """Uri builder pattern""" + + def __init__(self): + super().__init__() + self._scheme = "" + self._authority = "" + self._path = "" + self._query = "" + self._fragment = "" + self._host = "" + self._port = -1 + self._segments: List[str] = [] + + def scheme(self, scheme: str) -> "Uri.Builder": + self._scheme = scheme + return self + + def authority(self, authority: str) -> "Uri.Builder": + self._authority = authority + return self + + def path(self, path: str) -> "Uri.Builder": + self._path = path + return self + + def appendPath(self, new_segment: str) -> "Uri.Builder": + self._segments.append(new_segment) + return self + + def query(self, query: str) -> "Uri.Builder": + self._query = query + return self + + def appendQueryParameter(self, key: str, value: str) -> "Uri.Builder": + params = dict(parse_qs(self._query, keep_blank_values=True)) if self._query else {} + if key in params: + params[key].append(value) + else: + params[key] = [value] + self._query = urlencode(params, doseq=True) + return self + + def fragment(self, fragment: str) -> "Uri.Builder": + self._fragment = fragment + return self + + def opaquePart(self, opaque_part: str) -> "Uri.Builder": + return self + + def build(self) -> "Uri": + path = self._path + if self._segments: + path = (path.rstrip("/") if path else "") + "/" + "/".join(self._segments) + return Uri(self._scheme, self._authority, path, self._query, self._fragment) + + +# ═══════════════════════════════════════════════════════════════ +# ContentValues +# ═══════════════════════════════════════════════════════════════ + +class ContentValues(Stub): + """ContentValues — 类似 dict 的内容值容器""" + + def __init__(self, from_dict: Optional[Dict[str, Any]] = None): + super().__init__() + self._values: Dict[str, Any] = dict(from_dict) if from_dict else {} + + def put(self, key: str, value: Any): + self._values[key] = value + + def putNull(self, key: str): + self._values[key] = None + + def get(self, key: str) -> Optional[Any]: + return self._values.get(key) + + def getAsString(self, key: str) -> Optional[str]: + v = self._values.get(key) + return str(v) if v is not None else None + + def getAsInteger(self, key: str) -> Optional[int]: + v = self._values.get(key) + if v is None: + return None + try: + return int(v) + except (ValueError, TypeError): + return None + + def getAsLong(self, key: str) -> Optional[int]: + return self.getAsInteger(key) + + def getAsFloat(self, key: str) -> Optional[float]: + v = self._values.get(key) + if v is None: + return None + try: + return float(v) + except (ValueError, TypeError): + return None + + def getAsDouble(self, key: str) -> Optional[float]: + return self.getAsFloat(key) + + def getAsBoolean(self, key: str) -> Optional[bool]: + v = self._values.get(key) + if v is None: + return None + if isinstance(v, bool): + return v + if isinstance(v, str): + return v.lower() in ("true", "1", "yes", "y") + return bool(v) + + def getAsByteArray(self, key: str) -> Optional[bytes]: + v = self._values.get(key) + if isinstance(v, bytes): + return v + if isinstance(v, str): + return v.encode("utf-8") + return None + + def containsKey(self, key: str) -> bool: + return key in self._values + + def valueSet(self) -> Set[str]: + return set(self._values.keys()) + + def keySet(self) -> Set[str]: + return set(self._values.keys()) + + def size(self) -> int: + return len(self._values) + + def isEmpty(self) -> bool: + return len(self._values) == 0 + + def clear(self): + self._values.clear() + + def remove(self, key: str): + self._values.pop(key, None) + + def putAll(self, other: "ContentValues"): + if other: + self._values.update(other._values) + + def equals(self, other) -> bool: + if isinstance(other, ContentValues): + return self._values == other._values + return False + + def hashCode(self) -> int: + return hash(frozenset(self._values.items())) & 0x7FFFFFFF + + def toString(self) -> str: + return f"ContentValues({self._values})" + + def __getitem__(self, key: str): + return self._values[key] + + def __setitem__(self, key: str, value: Any): + self._values[key] = value + + def __contains__(self, key: str) -> bool: + return key in self._values + + def __len__(self) -> int: + return len(self._values) + + def __repr__(self): + return self.toString() + + +# ═══════════════════════════════════════════════════════════════ +# ContentResolver +# ═══════════════════════════════════════════════════════════════ + +class ContentResolver(Stub): + """ContentResolver — 内容解析器""" + + NOTIFY_SYNC_TO_NETWORK = 1 + NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2 + + def __init__(self): + super().__init__() + self._observers: Dict[str, List[Any]] = {} + + def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str], + selection_args: Optional[List[str]], sort_order: Optional[str], + cancellation_signal: Any = None) -> Any: + """Query content provider""" + sys.stderr.write(f"[ContentResolver] query: uri={uri}\n") + return None + + def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]: + sys.stderr.write(f"[ContentResolver] insert: uri={uri}\n") + return uri + + def update(self, uri: Uri, values: ContentValues, selection: Optional[str], + selection_args: Optional[List[str]]) -> int: + sys.stderr.write(f"[ContentResolver] update: uri={uri}\n") + return 0 + + def delete(self, uri: Uri, selection: Optional[str], + selection_args: Optional[List[str]]) -> int: + sys.stderr.write(f"[ContentResolver] delete: uri={uri}\n") + return 0 + + def applyBatch(self, authority: str, operations: List[Any]) -> List[Any]: + sys.stderr.write(f"[ContentResolver] applyBatch: authority={authority}\n") + return [] + + def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int: + sys.stderr.write(f"[ContentResolver] bulkInsert: uri={uri}\n") + return len(values) if values else 0 + + def call(self, uri: Uri, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]: + return None + + def notifyChange(self, uri: Uri, observer: Any, sync_to_network: bool = False): + uri_str = str(uri) + if uri_str in self._observers: + for obs in list(self._observers[uri_str]): + try: + obs.onChange(True) + except Exception: + pass + + def registerContentObserver(self, uri: Uri, notify_for_descendants: bool, observer: Any): + uri_str = str(uri) + if uri_str not in self._observers: + self._observers[uri_str] = [] + self._observers[uri_str].append(observer) + + def unregisterContentObserver(self, observer: Any): + for uri_str in list(self._observers.keys()): + if observer in self._observers[uri_str]: + self._observers[uri_str].remove(observer) + if not self._observers[uri_str]: + del self._observers[uri_str] + + def getType(self, uri: Uri) -> Optional[str]: + return None + + def acquireContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]: + return ContentProviderClient(uri) + + def acquireUnstableContentProviderClient(self, uri: Uri) -> Optional["ContentProviderClient"]: + return ContentProviderClient(uri) + + def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: + return None + + def openInputStream(self, uri: Uri) -> Any: + return None + + def openOutputStream(self, uri: Uri) -> Any: + return None + + +# ═══════════════════════════════════════════════════════════════ +# Context +# ═══════════════════════════════════════════════════════════════ + +class Context(Stub): + """Android Context — 应用上下文""" + + # Service 常量 + POWER_SERVICE = "power" + WINDOW_SERVICE = "window" + LAYOUT_INFLATER_SERVICE = "layout_inflater" + ACTIVITY_SERVICE = "activity" + ALARM_SERVICE = "alarm" + NOTIFICATION_SERVICE = "notification" + ACCESSIBILITY_SERVICE = "accessibility" + KEYGUARD_SERVICE = "keyguard" + LOCATION_SERVICE = "location" + SENSOR_SERVICE = "sensor" + STORAGE_SERVICE = "storage" + VIBRATOR_SERVICE = "vibrator" + CONNECTIVITY_SERVICE = "connectivity" + WIFI_SERVICE = "wifi" + AUDIO_SERVICE = "audio" + TELEPHONY_SERVICE = "telephony" + INPUT_METHOD_SERVICE = "input_method" + CAMERA_SERVICE = "camera" + BATTERY_SERVICE = "battery" + + # Mode 常量 + MODE_PRIVATE = 0x0000 + MODE_WORLD_READABLE = 0x0001 + MODE_WORLD_WRITEABLE = 0x0002 + MODE_APPEND = 0x8000 + MODE_MULTI_PROCESS = 0x0004 + + # Flag 常量 + BIND_AUTO_CREATE = 0x0001 + BIND_DEBUG_UNBIND = 0x0002 + BIND_NOT_FOREGROUND = 0x0004 + BIND_ABOVE_CLIENT = 0x0008 + BIND_ALLOW_OOM_MANAGEMENT = 0x0010 + BIND_WAIVE_PRIORITY = 0x0020 + BIND_IMPORTANT = 0x0040 + BIND_ADJUST_WITH_ACTIVITY = 0x0080 + + def __init__(self): + super().__init__() + self._package_name = _package_name + self._services: Dict[str, Any] = {} + self._shared_prefs: Dict[str, "SharedPreferences"] = {} + self._registered_receivers: List[Dict] = [] + self._content_resolver = ContentResolver() + + def getPackageName(self) -> str: + return self._package_name + + def getFilesDir(self): + return os.path.join(_app_data_dir, "files") + + def getCacheDir(self): + return os.path.join(_app_data_dir, "cache") + + def getCodeCacheDir(self): + return os.path.join(_app_data_dir, "code_cache") + + def getNoBackupFilesDir(self): + return os.path.join(_app_data_dir, "no_backup") + + def getExternalFilesDir(self, type_: Optional[str] = None) -> str: + base = os.path.join(_sdcard_root, "Android", "data", self._package_name, "files") + if type_: + base = os.path.join(base, type_) + os.makedirs(base, exist_ok=True) + return base + + def getExternalCacheDir(self): + base = os.path.join(_sdcard_root, "Android", "data", self._package_name, "cache") + os.makedirs(base, exist_ok=True) + return base + + def getObbDir(self): + base = os.path.join(_sdcard_root, "Android", "obb", self._package_name) + os.makedirs(base, exist_ok=True) + return base + + def getDataDir(self): + return _app_data_dir + + def getPackageCodePath(self) -> str: + return os.path.join(_app_data_dir, "base.apk") + + def getPackageResourcePath(self) -> str: + return os.path.join(_app_data_dir, "base.apk") + + def startActivity(self, intent: Any): + sys.stderr.write(f"[Context] startActivity: {intent}\n") + + def startActivities(self, intents: List[Any], options: Optional[Any] = None): + for intent in intents: + self.startActivity(intent) + + def sendBroadcast(self, intent: Any): + sys.stderr.write(f"[Context] sendBroadcast: {intent}\n") + for receiver in self._registered_receivers: + if self._matches_intent_filter(intent, receiver.get("filter")): + try: + receiver["receiver"].onReceive(self, intent) + except Exception as e: + sys.stderr.write(f"[Context] broadcast error: {e}\n") + + def sendOrderedBroadcast(self, intent: Any, receiver_permission: Optional[str] = None): + self.sendBroadcast(intent) + + def sendStickyBroadcast(self, intent: Any): + self.sendBroadcast(intent) + + def registerReceiver(self, receiver: Optional[Any], filter: Any) -> Optional[Any]: + if receiver: + self._registered_receivers.append({"receiver": receiver, "filter": filter}) + return receiver + + def unregisterReceiver(self, receiver: Any): + self._registered_receivers = [ + r for r in self._registered_receivers + if r["receiver"] is not receiver + ] + + def _matches_intent_filter(self, intent: Any, intent_filter: Any) -> bool: + if intent_filter is None: + return True + action = getattr(intent, "getAction", lambda: None)() + if intent_filter._action and action != intent_filter._action: + return False + return True + + def getContentResolver(self) -> ContentResolver: + return self._content_resolver + + def getSharedPreferences(self, name: str, mode: int = MODE_PRIVATE) -> "SharedPreferences": + if name not in self._shared_prefs: + self._shared_prefs[name] = SharedPreferences(name) + return self._shared_prefs[name] + + def getPackageManager(self): + from .content_pm import PackageManager + return PackageManager() + + def getResources(self): + return None + + def getAssets(self): + return None + + def getMainLooper(self): + from .os import Looper + return Looper.getMainLooper() + + def getApplicationContext(self): + return self + + def getSystemService(self, name: str) -> Any: + if name == Context.ACTIVITY_SERVICE: + from .app import ActivityManager + return ActivityManager() + if name == Context.ALARM_SERVICE: + from .app import AlarmManager + return AlarmManager() + if name == Context.NOTIFICATION_SERVICE: + from .app import NotificationManager + return NotificationManager() + if name == Context.VIBRATOR_SERVICE: + from .os import Vibrator + return Vibrator() + if name == Context.POWER_SERVICE: + from .os import PowerManager + return PowerManager() + if name == Context.LOCATION_SERVICE: + from .location import LocationManager + return LocationManager() + if name == Context.SENSOR_SERVICE: + from .hardware import SensorManager + return SensorManager() + if name == Context.CONNECTIVITY_SERVICE: + from .net import ConnectivityManager + return ConnectivityManager() + if name == Context.WIFI_SERVICE: + from .net_wifi import WifiManager + return WifiManager() + if name == Context.AUDIO_SERVICE: + from .media import AudioManager + return AudioManager() + if name == Context.TELEPHONY_SERVICE: + from .telephony import TelephonyManager + return TelephonyManager() + if name == Context.INPUT_METHOD_SERVICE: + from .view_inputmethod import InputMethodManager + return InputMethodManager() + if name == Context.CAMERA_SERVICE: + from .hardware_camera2 import CameraManager + return CameraManager() + if name == Context.BATTERY_SERVICE: + from .os import BatteryManager + return BatteryManager() + return self._services.get(name) + + def startService(self, intent: Any) -> Optional[Any]: + sys.stderr.write(f"[Context] startService: {intent}\n") + return None + + def stopService(self, intent: Any) -> bool: + return True + + def bindService(self, intent: Any, conn: Any, flags: int) -> bool: + sys.stderr.write(f"[Context] bindService: {intent}\n") + return True + + def unbindService(self, conn: Any): + pass + + def getString(self, res_id: int, *args) -> str: + return str(res_id) + + def getColor(self, color_id: int) -> int: + return 0 + + def getDrawable(self, drawable_id: int): + return None + + def getWallpaper(self): + return None + + def getWallpaperDesiredMinimumWidth(self) -> int: + return 1080 + + def getWallpaperDesiredMinimumHeight(self) -> int: + return 1920 + + def checkCallingPermission(self, permission: str) -> int: + from .content_pm import PackageManager + return PackageManager.PERMISSION_GRANTED + + def checkSelfPermission(self, permission: str) -> int: + from .content_pm import PackageManager + return PackageManager.PERMISSION_GRANTED + + def shouldShowRequestPermissionRationale(self, permission: str) -> bool: + return False + + def enforcePermission(self, permission: str, pid: int, uid: int, message: Optional[str]): + pass + + def createPackageContext(self, package_name: str, flags: int) -> "Context": + ctx = Context() + ctx._package_name = package_name + return ctx + + def isDeviceProtectedStorage(self) -> bool: + return False + + def isCredentialProtectedStorage(self) -> bool: + return False + + def deleteFile(self, name: str) -> bool: + path = os.path.join(self.getFilesDir(), name) + try: + os.remove(path) + return True + except OSError: + return False + + def fileList(self) -> List[str]: + try: + return os.listdir(self.getFilesDir()) + except OSError: + return [] + + def getDatabasePath(self, name: str): + path = os.path.join(_app_data_dir, "databases", name) + os.makedirs(os.path.dirname(path), exist_ok=True) + return path + + def databaseList(self) -> List[str]: + db_dir = os.path.join(_app_data_dir, "databases") + try: + return [f for f in os.listdir(db_dir) if f.endswith(".db")] + except OSError: + return [] + + def deleteDatabase(self, name: str) -> bool: + db_path = os.path.join(_app_data_dir, "databases", name) + try: + os.remove(db_path) + return True + except OSError: + return False + + def getDir(self, name: str, mode: int) -> str: + path = os.path.join(_app_data_dir, name) + os.makedirs(path, exist_ok=True) + return path + + def toString(self) -> str: + return f"Context{{{self._package_name}}}" + + +class ContextWrapper(Context): + """ContextWrapper — Context 的包装器""" + + def __init__(self, base: Context): + super().__init__() + self._base = base + + def getBaseContext(self) -> Context: + return self._base + + def getPackageName(self) -> str: + return self._base.getPackageName() + + def getFilesDir(self): + return self._base.getFilesDir() + + def getCacheDir(self): + return self._base.getCacheDir() + + def startActivity(self, intent: Any): + self._base.startActivity(intent) + + def sendBroadcast(self, intent: Any): + self._base.sendBroadcast(intent) + + def getContentResolver(self) -> ContentResolver: + return self._base.getContentResolver() + + def getSharedPreferences(self, name: str, mode: int) -> "SharedPreferences": + return self._base.getSharedPreferences(name, mode) + + def getSystemService(self, name: str) -> Any: + return self._base.getSystemService(name) + + def startService(self, intent: Any) -> Optional[Any]: + return self._base.startService(intent) + + def bindService(self, intent: Any, conn: Any, flags: int) -> bool: + return self._base.bindService(intent, conn, flags) + + def registerReceiver(self, receiver: Optional[Any], filter: Any) -> Optional[Any]: + return self._base.registerReceiver(receiver, filter) + + def unregisterReceiver(self, receiver: Any): + self._base.unregisterReceiver(receiver) + + +# ═══════════════════════════════════════════════════════════════ +# IntentFilter +# ═══════════════════════════════════════════════════════════════ + +class IntentFilter(Stub): + """IntentFilter — 描述 Intent 匹配规则""" + + MATCH_CATEGORY_EMPTY = 0x100000 + MATCH_CATEGORY_HOST = 0x200000 + MATCH_CATEGORY_PORT = 0x400000 + MATCH_CATEGORY_PATH = 0x800000 + MATCH_CATEGORY_SCHEME = 0x1000000 + MATCH_CATEGORY_SCHEME_SPECIFIC_PART = 0x2000000 + MATCH_CATEGORY_TYPE = 0x4000000 + NO_MATCH_ACTION = -3 + NO_MATCH_CATEGORY = -4 + NO_MATCH_DATA = -5 + NO_MATCH_TYPE = -6 + + def __init__(self, action: Optional[str] = None): + super().__init__() + self._action = action + self._actions: List[str] = [] + self._categories: List[str] = [] + self._data_schemes: List[str] = [] + self._data_authorities: List[str] = [] + self._data_paths: List[str] = [] + self._data_types: List[str] = [] + self._priority = 0 + if action: + self.addAction(action) + + def addAction(self, action: str): + if action not in self._actions: + self._actions.append(action) + + def addCategory(self, category: str): + if category not in self._categories: + self._categories.append(category) + + def addDataScheme(self, scheme: str): + if scheme not in self._data_schemes: + self._data_schemes.append(scheme) + + def addDataAuthority(self, host: str, port: int = -1): + auth = f"{host}:{port}" if port >= 0 else host + if auth not in self._data_authorities: + self._data_authorities.append(auth) + + def addDataPath(self, path: str, pattern_type: int = 0): + if path not in self._data_paths: + self._data_paths.append(path) + + def addDataType(self, type_: str): + if type_ not in self._data_types: + self._data_types.append(type_) + + def hasAction(self, action: str) -> bool: + return action in self._actions + + def hasCategory(self, category: str) -> bool: + return category in self._categories + + def hasDataScheme(self, scheme: str) -> bool: + return scheme in self._data_schemes + + def countActions(self) -> int: + return len(self._actions) + + def getAction(self, index: int) -> str: + return self._actions[index] + + def setPriority(self, priority: int): + self._priority = priority + + def getPriority(self) -> int: + return self._priority + + def match(self, action: str, type_: Optional[str], scheme: Optional[str], + data: Optional[Any], categories: Optional[Set[str]], tag: str) -> int: + if action and action not in self._actions: + return self.NO_MATCH_ACTION + return self.MATCH_CATEGORY_EMPTY + + def matchAction(self, action: str) -> bool: + return action in self._actions + + def matchCategories(self, categories: Set[str]) -> Optional[str]: + return None + + def matchData(self, type_: str, scheme: str, data: Any) -> int: + return self.MATCH_CATEGORY_EMPTY + + def dump(self, du: Any, prefix: str): + pass + + def describeContents(self) -> int: + return 0 + + def toString(self) -> str: + return f"IntentFilter(actions={self._actions})" + + +# ═══════════════════════════════════════════════════════════════ +# BroadcastReceiver +# ═══════════════════════════════════════════════════════════════ + +class BroadcastReceiver(Stub): + """BroadcastReceiver — 广播接收器""" + + RESULT_OK = -1 + RESULT_CANCELED = 0 + RESULT_REDIRECT = 1 + + def __init__(self): + super().__init__() + self._result_code = self.RESULT_OK + self._result_data = "" + self._result_extras: Optional[ContentValues] = None + self._ordered = False + self._initial_sticky = False + + def onReceive(self, context: Context, intent: Any): + """Override this method to handle broadcasts""" + pass + + def setResultCode(self, code: int): + self._result_code = code + + def getResultCode(self) -> int: + return self._result_code + + def setResultData(self, data: str): + self._result_data = data + + def getResultData(self) -> str: + return self._result_data + + def setResult(self, code: int, data: str, extras: Optional[ContentValues]): + self._result_code = code + self._result_data = data + self._result_extras = extras + + def setResultExtras(self, extras: ContentValues): + self._result_extras = extras + + def getResultExtras(self, make_map: bool) -> Optional[ContentValues]: + return self._result_extras + + def abortBroadcast(self): + self._result_code = self.RESULT_CANCELED + + def isOrderedBroadcast(self) -> bool: + return self._ordered + + def isInitialStickyBroadcast(self) -> bool: + return self._initial_sticky + + def getDebugUnregister(self) -> bool: + return False + + def toString(self) -> str: + return f"BroadcastReceiver@{hex(id(self))}" + + +# ═══════════════════════════════════════════════════════════════ +# SharedPreferences / SharedPreferencesEditor +# ═══════════════════════════════════════════════════════════════ + +_SHARED_PREFS_DIR = os.path.join(_app_data_dir, "shared_prefs") +os.makedirs(_SHARED_PREFS_DIR, exist_ok=True) + +class SharedPreferences(Stub): + """SharedPreferences — JSON 文件实现的持久化键值存储""" + + def __init__(self, name: str): + super().__init__() + self._name = name + self._lock = threading.Lock() + self._file_path = os.path.join(_SHARED_PREFS_DIR, f"{name}.json") + self._data: Dict[str, Any] = {} + self._load() + + def _load(self): + try: + if os.path.exists(self._file_path): + with open(self._file_path, "r", encoding="utf-8") as f: + self._data = json.load(f) + else: + self._data = {} + except (json.JSONDecodeError, OSError): + self._data = {} + + def _save(self): + with self._lock: + os.makedirs(os.path.dirname(self._file_path), exist_ok=True) + with open(self._file_path, "w", encoding="utf-8") as f: + json.dump(self._data, f, indent=2, ensure_ascii=False) + + def getString(self, key: str, def_value: Optional[str] = None) -> Optional[str]: + v = self._data.get(key) + return str(v) if v is not None else def_value + + def getInt(self, key: str, def_value: int = 0) -> int: + v = self._data.get(key) + if isinstance(v, int) or isinstance(v, float): + return int(v) + return def_value + + def getLong(self, key: str, def_value: int = 0) -> int: + return self.getInt(key, def_value) + + def getFloat(self, key: str, def_value: float = 0.0) -> float: + v = self._data.get(key) + if isinstance(v, (int, float)): + return float(v) + return def_value + + def getBoolean(self, key: str, def_value: bool = False) -> bool: + v = self._data.get(key) + if isinstance(v, bool): + return v + if isinstance(v, str): + return v.lower() in ("true", "1", "yes") + return def_value + + def getAll(self) -> Dict[str, Any]: + return dict(self._data) + + def contains(self, key: str) -> bool: + return key in self._data + + def edit(self) -> "SharedPreferencesEditor": + return SharedPreferencesEditor(self) + + def registerOnSharedPreferenceChangeListener(self, listener): + pass + + def unregisterOnSharedPreferenceChangeListener(self, listener): + pass + + def getSharedPrefsFile(self): + return self._file_path + + def toString(self) -> str: + return f"SharedPreferences({self._name})" + + +class SharedPreferencesEditor(Stub): + """SharedPreferences Editor""" + + def __init__(self, prefs: SharedPreferences): + super().__init__() + self._prefs = prefs + self._modified: Dict[str, Any] = {} + self._removed: Set[str] = set() + self._clear_all = False + + def putString(self, key: str, value: str) -> "SharedPreferencesEditor": + self._modified[key] = value + self._removed.discard(key) + return self + + def putInt(self, key: str, value: int) -> "SharedPreferencesEditor": + self._modified[key] = value + self._removed.discard(key) + return self + + def putLong(self, key: str, value: int) -> "SharedPreferencesEditor": + self._modified[key] = value + self._removed.discard(key) + return self + + def putFloat(self, key: str, value: float) -> "SharedPreferencesEditor": + self._modified[key] = value + self._removed.discard(key) + return self + + def putBoolean(self, key: str, value: bool) -> "SharedPreferencesEditor": + self._modified[key] = value + self._removed.discard(key) + return self + + def remove(self, key: str) -> "SharedPreferencesEditor": + self._removed.add(key) + self._modified.pop(key, None) + return self + + def clear(self) -> "SharedPreferencesEditor": + self._clear_all = True + self._modified.clear() + self._removed.clear() + return self + + def apply(self): + """异步保存""" + self.commit() + + def commit(self) -> bool: + try: + if self._clear_all: + self._prefs._data.clear() + for key in self._removed: + self._prefs._data.pop(key, None) + self._prefs._data.update(self._modified) + self._prefs._save() + self._modified.clear() + self._removed.clear() + self._clear_all = False + return True + except Exception: + return False + + +# ═══════════════════════════════════════════════════════════════ +# ComponentName +# ═══════════════════════════════════════════════════════════════ + +class ComponentName(Stub): + """ComponentName — 标识包中的特定组件""" + + def __init__(self, pkg: str, cls: str): + super().__init__() + self._package = pkg + self._classname = cls + + def getPackageName(self) -> str: + return self._package + + def getClassName(self) -> str: + return self._classname + + def getShortClassName(self) -> str: + return self._classname.split(".")[-1] if "." in self._classname else self._classname + + def flattenToString(self) -> str: + return f"{self._package}/{self._classname}" + + def flattenToShortString(self) -> str: + return f"{self._package}/{self.getShortClassName()}" + + @staticmethod + def unflattenFromString(str_: str) -> Optional["ComponentName"]: + if "/" in str_: + parts = str_.split("/", 1) + return ComponentName(parts[0], parts[1]) + return None + + def toString(self) -> str: + return f"ComponentInfo{{{self._package}/{self._classname}}}" + + def __repr__(self): + return self.toString() + + +# ═══════════════════════════════════════════════════════════════ +# ClipData / ClipDescription +# ═══════════════════════════════════════════════════════════════ + +class ClipDescription(Stub): + """ClipDescription — 描述剪贴板数据""" + + MIMETYPE_TEXT_PLAIN = "text/plain" + MIMETYPE_TEXT_HTML = "text/html" + MIMETYPE_TEXT_URILIST = "text/uri-list" + MIMETYPE_TEXT_INTENT = "text/vnd.android.intent" + + def __init__(self, label: str, mime_types: List[str]): + super().__init__() + self._label = label + self._mime_types = list(mime_types) + self._timestamp = 0 + + def getLabel(self) -> str: + return self._label + + def getMimeType(self, index: int) -> str: + if 0 <= index < len(self._mime_types): + return self._mime_types[index] + return "" + + def getMimeTypeCount(self) -> int: + return len(self._mime_types) + + def filterMimeTypes(self, mime_type: str) -> bool: + return mime_type in self._mime_types + + def compareMimeTypes(self, mime_type1: str, mime_type2: str) -> bool: + return mime_type1 == mime_type2 + + def getTimestamp(self) -> int: + return self._timestamp + + def toString(self) -> str: + return f"ClipDescription({self._label}, {self._mime_types})" + + +class ClipData(Stub): + """ClipData — 剪贴板数据""" + + def __init__(self, description: ClipDescription, item: "ClipData.Item"): + super().__init__() + self._description = description + self._items: List["ClipData.Item"] = [item] if item else [] + + class Item(Stub): + """单个剪贴板条目""" + def __init__(self, text: Optional[str] = None, intent: Optional[Any] = None, uri: Optional[Uri] = None): + super().__init__() + self._text = text + self._intent = intent + self._uri = uri + self._html_text = None + + def getText(self) -> Optional[str]: + return self._text + + def getIntent(self): + return self._intent + + def getUri(self) -> Optional[Uri]: + return self._uri + + def getHtmlText(self) -> Optional[str]: + return self._html_text + + def setHtmlText(self, html_text: str): + self._html_text = html_text + + def coerceToText(self, context: Context) -> str: + if self._text: + return self._text + if self._uri: + return str(self._uri) + if self._intent: + return str(self._intent) + return "" + + def coerceToHtmlText(self, context: Context) -> Optional[str]: + return self._html_text + + def toString(self) -> str: + return f"ClipData.Item({self._text})" + + @staticmethod + def newPlainText(label: str, text: str) -> "ClipData": + desc = ClipDescription(label, [ClipDescription.MIMETYPE_TEXT_PLAIN]) + return ClipData(desc, ClipData.Item(text=text)) + + @staticmethod + def newUri(cr: ContentResolver, label: str, uri: Uri) -> "ClipData": + desc = ClipDescription(label, [ClipDescription.MIMETYPE_TEXT_URILIST]) + return ClipData(desc, ClipData.Item(uri=uri)) + + @staticmethod + def newIntent(label: str, intent: Any) -> "ClipData": + desc = ClipDescription(label, [ClipDescription.MIMETYPE_TEXT_INTENT]) + return ClipData(desc, ClipData.Item(intent=intent)) + + def getDescription(self) -> ClipDescription: + return self._description + + def addItem(self, item: "ClipData.Item"): + self._items.append(item) + + def getItemAt(self, index: int) -> Optional["ClipData.Item"]: + if 0 <= index < len(self._items): + return self._items[index] + return None + + def getItemCount(self) -> int: + return len(self._items) + + def toString(self) -> str: + return f"ClipData({self._description})" + + +# ═══════════════════════════════════════════════════════════════ +# ContentProvider / ContentProviderClient +# ═══════════════════════════════════════════════════════════════ + +class ContentProvider(Stub): + """ContentProvider — 内容提供者基类""" + + def __init__(self): + super().__init__() + self._context: Optional[Context] = None + self._calling_package = "" + + def onCreate(self) -> bool: + return True + + def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str], + selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]: + return None + + def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]: + return None + + def update(self, uri: Uri, values: ContentValues, selection: Optional[str], + selection_args: Optional[List[str]]) -> int: + return 0 + + def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int: + return 0 + + def getType(self, uri: Uri) -> Optional[str]: + return None + + def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: + return None + + def openFile(self, uri: Uri, mode: str) -> Any: + return None + + def openAssetFile(self, uri: Uri, mode: str) -> Any: + return None + + def attachInfo(self, context: Context, info: Any): + self._context = context + + def getContext(self) -> Optional[Context]: + return self._context + + def getCallingPackage(self) -> str: + return self._calling_package + + def applyBatch(self, operations: List[Any]) -> List[Any]: + return [] + + def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int: + return len(values) if values else 0 + + def isTemporary(self) -> bool: + return False + + def shutdown(self): + pass + + def toString(self) -> str: + return f"ContentProvider@{hex(id(self))}" + + +class ContentProviderClient(Stub): + """ContentProviderClient — 内容提供者客户端""" + + def __init__(self, uri: Optional[Uri] = None): + super().__init__() + self._uri = uri + + def query(self, uri: Uri, projection: Optional[List[str]], selection: Optional[str], + selection_args: Optional[List[str]], sort_order: Optional[str]) -> Optional[Any]: + return None + + def insert(self, uri: Uri, values: ContentValues) -> Optional[Uri]: + return uri + + def update(self, uri: Uri, values: ContentValues, selection: Optional[str], + selection_args: Optional[List[str]]) -> int: + return 0 + + def delete(self, uri: Uri, selection: Optional[str], selection_args: Optional[List[str]]) -> int: + return 0 + + def applyBatch(self, operations: List[Any]) -> List[Any]: + return [] + + def bulkInsert(self, uri: Uri, values: List[ContentValues]) -> int: + return len(values) if values else 0 + + def getType(self, uri: Uri) -> Optional[str]: + return None + + def getStreamTypes(self, uri: Uri, mime_type_filter: str) -> Optional[List[str]]: + return None + + def call(self, method: str, arg: Optional[str], extras: Optional[ContentValues]) -> Optional[ContentValues]: + return None + + def release(self): + pass + + def close(self): + pass + + def openFile(self, uri: Uri, mode: str) -> Any: + return None + + def openAssetFile(self, uri: Uri, mode: str) -> Any: + return None + + def openTypedAssetFile(self, uri: Uri, mime_type: str, opts: Optional[ContentValues]) -> Any: + return None + + def getLocalContentProvider(self) -> Optional[ContentProvider]: + return None + + +class ContentProviderOperation(Stub): + """ContentProviderOperation — 批量操作""" + + TYPE_INSERT = 1 + TYPE_UPDATE = 2 + TYPE_DELETE = 3 + TYPE_ASSERT = 4 + + class Builder(Stub): + def __init__(self, type_: int, uri: Uri): + super().__init__() + self._type = type_ + self._uri = uri + self._values: Optional[ContentValues] = None + self._selection: Optional[str] = None + self._selection_args: Optional[List[str]] = None + + def withValue(self, key: str, value: Any) -> "ContentProviderOperation.Builder": + if self._values is None: + self._values = ContentValues() + self._values.put(key, value) + return self + + def withValues(self, values: ContentValues) -> "ContentProviderOperation.Builder": + self._values = values + return self + + def withSelection(self, selection: str, selection_args: List[str]) -> "ContentProviderOperation.Builder": + self._selection = selection + self._selection_args = selection_args + return self + + def build(self) -> "ContentProviderOperation": + op = ContentProviderOperation() + op._type = self._type + op._uri = self._uri + op._values = self._values + op._selection = self._selection + op._selection_args = self._selection_args + return op + + @staticmethod + def newInsert(uri: Uri) -> "ContentProviderOperation.Builder": + return ContentProviderOperation.Builder(ContentProviderOperation.TYPE_INSERT, uri) + + @staticmethod + def newUpdate(uri: Uri) -> "ContentProviderOperation.Builder": + return ContentProviderOperation.Builder(ContentProviderOperation.TYPE_UPDATE, uri) + + @staticmethod + def newDelete(uri: Uri) -> "ContentProviderOperation.Builder": + return ContentProviderOperation.Builder(ContentProviderOperation.TYPE_DELETE, uri) + + @staticmethod + def newAssert(uri: Uri) -> "ContentProviderOperation.Builder": + return ContentProviderOperation.Builder(ContentProviderOperation.TYPE_ASSERT, uri) + + +class ContentProviderResult(Stub): + """ContentProviderResult — 操作结果""" + + def __init__(self, uri: Optional[Uri] = None, count: Optional[int] = None): + super().__init__() + self.uri = uri + self.count = count + + +# ═══════════════════════════════════════════════════════════════ +# Loader / CursorLoader / DialogInterface +# ═══════════════════════════════════════════════════════════════ + +class Loader(Stub): + """Loader — 异步数据加载器基类""" + + def __init__(self, context: Context): + super().__init__() + self._context = context + self._started = False + self._abandoned = False + self._listeners: List = [] + + def deliverResult(self, data: Any): + for listener in self._listeners: + try: + listener.onLoadFinished(self, data) + except Exception: + pass + + def startLoading(self): + self._started = True + + def forceLoad(self): + self.deliverResult(None) + + def stopLoading(self): + self._started = False + + def abandon(self): + self._abandoned = True + + def isStarted(self) -> bool: + return self._started + + def isAbandoned(self) -> bool: + return self._abandoned + + def reset(self): + self._started = False + self._abandoned = False + + def registerListener(self, listener): + if listener not in self._listeners: + self._listeners.append(listener) + + def unregisterListener(self, listener): + if listener in self._listeners: + self._listeners.remove(listener) + + def dataToString(self, data: Any) -> str: + return str(data) + + def toString(self) -> str: + return f"Loader@{hex(id(self))}" + + +class CursorLoader(Loader): + """CursorLoader — 游标加载器""" + + def __init__(self, context: Context, uri: Uri, projection: Optional[List[str]], + selection: Optional[str], selection_args: Optional[List[str]], sort_order: Optional[str]): + super().__init__(context) + self._uri = uri + self._projection = projection + self._selection = selection + self._selection_args = selection_args + self._sort_order = sort_order + + def getUri(self) -> Uri: + return self._uri + + def setUri(self, uri: Uri): + self._uri = uri + + def getProjection(self) -> Optional[List[str]]: + return self._projection + + def setProjection(self, projection: Optional[List[str]]): + self._projection = projection + + def getSelection(self) -> Optional[str]: + return self._selection + + def setSelection(self, selection: Optional[str]): + self._selection = selection + + def getSelectionArgs(self) -> Optional[List[str]]: + return self._selection_args + + def setSelectionArgs(self, args: Optional[List[str]]): + self._selection_args = args + + def getSortOrder(self) -> Optional[str]: + return self._sort_order + + def setSortOrder(self, sort_order: Optional[str]): + self._sort_order = sort_order + + def loadInBackground(self): + return None + + +class DialogInterface(Stub): + """DialogInterface — 对话框接口常量""" + + BUTTON_POSITIVE = -1 + BUTTON_NEGATIVE = -2 + BUTTON_NEUTRAL = -3 + + def cancel(self): + pass + + def dismiss(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# SyncAdapter / SyncResult +# ═══════════════════════════════════════════════════════════════ + +class SyncAdapter(Stub): + """SyncAdapter — 同步适配器""" + + def __init__(self, context: Context, auto_initialize: bool, allow_parallel_syncs: bool): + super().__init__() + self._context = context + + def onPerformSync(self, account: Any, extras: ContentValues, authority: str, + provider: ContentProviderClient, sync_result: "SyncResult"): + pass + + def onSecurityException(self, account: Any, extras: ContentValues, + authority: str, sync_result: "SyncResult"): + pass + + +class SyncResult(Stub): + """SyncResult — 同步结果""" + + def __init__(self): + super().__init__() + self.stats = self._Stats() + + class _Stats: + def __init__(self): + self.numEntries = 0 + self.numUpdates = 0 + self.numDeletes = 0 + self.numInserts = 0 + self.numSkippedEntries = 0 + + def clear(self): + self.stats = self._Stats() + + def hasError(self) -> bool: + return False + + def hasSoftError(self) -> bool: + return False + + def hasHardError(self) -> bool: + return False + + def madeSomeProgress(self) -> bool: + return self.stats.numEntries > 0 + + def toString(self) -> str: + return f"SyncResult({self.stats.numEntries} entries)" + + +# ═══════════════════════════════════════════════════════════════ +# 替换 _core.py 中的 _GlobalContext +# ═══════════════════════════════════════════════════════════════ + +_real_context = Context() +# 将 _core 中的 android_context 替换为真实 Context +import importlib +try: + _core_mod = importlib.import_module(".._core", __package__) + _core_mod.android_context.__class__ = Context +except Exception: + pass + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Uri", + "ContentValues", + "ContentResolver", + "Context", + "ContextWrapper", + "IntentFilter", + "BroadcastReceiver", + "SharedPreferences", + "SharedPreferencesEditor", + "ComponentName", + "ClipData", + "ClipDescription", + "ContentProvider", + "ContentProviderClient", + "ContentProviderOperation", + "ContentProviderResult", + "CursorLoader", + "Loader", + "DialogInterface", + "SyncAdapter", + "SyncResult", +] \ No newline at end of file diff --git a/android_pyapi/android/content_pm.py b/android_pyapi/android/content_pm.py new file mode 100644 index 0000000..1f2283e --- /dev/null +++ b/android_pyapi/android/content_pm.py @@ -0,0 +1,801 @@ +""" +Android PackageManager 组件 — PackageManager, PackageInfo, ApplicationInfo 等 +方法签名完全对齐 Android SDK +""" + +import json +import os +import sys +from typing import Any, Dict, List, Optional, Set + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# 数据存储路径 +# ═══════════════════════════════════════════════════════════════ + +_PACKAGE_DATA_DIR = os.path.join(_app_data_dir, "package_data") +os.makedirs(_PACKAGE_DATA_DIR, exist_ok=True) + +_DEFAULT_PACKAGE_INFO_PATH = os.path.join(_PACKAGE_DATA_DIR, f"{_package_name}.json") + + +def _get_default_package_info() -> Dict[str, Any]: + """获取默认包信息""" + return { + "packageName": _package_name, + "versionName": "1.0.0", + "versionCode": 1, + "applicationInfo": { + "name": _package_name, + "packageName": _package_name, + "className": f"{_package_name}.MainApplication", + "icon": 0, + "targetSdkVersion": 34, + "sourceDir": os.path.join(_app_data_dir, "base.apk"), + "dataDir": _app_data_dir, + "enabled": True, + "flags": 0, + "theme": 0, + "labelRes": 0, + "descriptionRes": 0, + "logo": 0, + "banner": 0, + "processName": _package_name, + "uid": 10001, + "manageSpaceActivityName": "", + "taskAffinity": "", + "permission": "", + }, + "activities": [], + "services": [], + "receivers": [], + "providers": [], + "permissions": [], + "requestedPermissions": [ + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + ], + "features": { + "android.hardware.camera": True, + "android.hardware.wifi": True, + "android.hardware.bluetooth": True, + "android.hardware.location": True, + "android.hardware.sensor.accelerometer": True, + "android.hardware.sensor.gyroscope": True, + "android.hardware.touchscreen": True, + "android.hardware.microphone": True, + "android.hardware.nfc": False, + "android.hardware.telephony": True, + "android.software.picture_in_picture": True, + "android.hardware.usb.host": False, + } + } + + +# ═══════════════════════════════════════════════════════════════ +# PackageItemInfo (基类) +# ═══════════════════════════════════════════════════════════════ + +class PackageItemInfo(Stub): + """PackageItemInfo — 包条目信息基类""" + + def __init__(self, data: Optional[Dict[str, Any]] = None): + super().__init__() + data = data or {} + self.name = data.get("name", "") + self.packageName = data.get("packageName", "") + self.labelRes = data.get("labelRes", 0) + self.nonLocalizedLabel = data.get("nonLocalizedLabel", None) + self.icon = data.get("icon", 0) + self.banner = data.get("banner", 0) + self.logo = data.get("logo", 0) + + def loadLabel(self, pm: "PackageManager") -> str: + return self.nonLocalizedLabel or self.name.split(".")[-1] if self.name else "" + + def loadIcon(self, pm: "PackageManager") -> Any: + return None + + def loadLogo(self, pm: "PackageManager") -> Any: + return None + + def loadBanner(self, pm: "PackageManager") -> Any: + return None + + def toString(self) -> str: + return f"{self.__class__.__name__}({self.packageName}:{self.name})" + + +class ComponentInfo(PackageItemInfo): + """ComponentInfo — 组件信息基类""" + + def __init__(self, data: Optional[Dict[str, Any]] = None): + super().__init__(data) + data = data or {} + self.processName = data.get("processName", self.packageName) + self.splitName = data.get("splitName", None) + self.enabled = data.get("enabled", True) + self.exported = data.get("exported", False) + self.directBootAware = data.get("directBootAware", False) + + def getComponentName(self) -> Any: + from .content import ComponentName + return ComponentName(self.packageName, self.name) + + def isEnabled(self) -> bool: + return self.enabled + + +# ═══════════════════════════════════════════════════════════════ +# ApplicationInfo +# ═══════════════════════════════════════════════════════════════ + +class ApplicationInfo(ComponentInfo): + """ApplicationInfo — 应用信息""" + + FLAG_SYSTEM = 1 + FLAG_DEBUGGABLE = 2 + FLAG_HAS_CODE = 4 + FLAG_ALLOW_CLEAR_USER_DATA = 0x00400000 + FLAG_ALLOW_BACKUP = 0x00008000 + FLAG_EXTERNAL_STORAGE = 0x00100000 + FLAG_LARGE_HEAP = 0x00100000 + FLAG_MULTIPROCESS = 0x00010000 + FLAG_TEST_ONLY = 0x00000100 + FLAG_STOPPED = 0x00200000 + FLAG_RESTART_ON_UPGRADE = 0x00000020 + + def __init__(self, data: Optional[Dict[str, Any]] = None): + super().__init__(data) + data = data or {} + self.taskAffinity = data.get("taskAffinity", "") + self.permission = data.get("permission", "") + self.processName = data.get("processName", self.packageName) + self.className = data.get("className", "") + self.descriptionRes = data.get("descriptionRes", 0) + self.theme = data.get("theme", 0) + self.flags = data.get("flags", 0) + self.sourceDir = data.get("sourceDir", "") + self.publicSourceDir = data.get("publicSourceDir", self.sourceDir) + self.dataDir = data.get("dataDir", "") + self.nativeLibraryDir = data.get("nativeLibraryDir", "") + self.metaData = data.get("metaData", None) + self.manageSpaceActivityName = data.get("manageSpaceActivityName", "") + self.targetSdkVersion = data.get("targetSdkVersion", 34) + self.uid = data.get("uid", 10001) + self.backupAgentName = data.get("backupAgentName", "") + self.enabled = data.get("enabled", True) + + def loadDescription(self, pm: "PackageManager") -> Optional[str]: + return None + + def toString(self) -> str: + return f"ApplicationInfo({self.packageName}, targetSdk={self.targetSdkVersion})" + + +# ═══════════════════════════════════════════════════════════════ +# ActivityInfo +# ═══════════════════════════════════════════════════════════════ + +class ActivityInfo(ComponentInfo): + """ActivityInfo — Activity 组件信息""" + + CONFIG_MCC = 0x0001 + CONFIG_MNC = 0x0002 + CONFIG_LOCALE = 0x0004 + CONFIG_TOUCHSCREEN = 0x0008 + CONFIG_KEYBOARD = 0x0010 + CONFIG_KEYBOARD_HIDDEN = 0x0020 + CONFIG_NAVIGATION = 0x0040 + CONFIG_ORIENTATION = 0x0080 + CONFIG_SCREEN_LAYOUT = 0x0100 + CONFIG_UI_MODE = 0x0200 + CONFIG_SCREEN_SIZE = 0x0400 + CONFIG_SMALLEST_SCREEN_SIZE = 0x0800 + CONFIG_DENSITY = 0x1000 + CONFIG_LAYOUT_DIRECTION = 0x2000 + CONFIG_COLOR_MODE = 0x4000 + + LAUNCH_MULTIPLE = 0 + LAUNCH_SINGLE_TOP = 1 + LAUNCH_SINGLE_TASK = 2 + LAUNCH_SINGLE_INSTANCE = 3 + LAUNCH_SINGLE_INSTANCE_PER_TASK = 4 + + SCREEN_ORIENTATION_UNSPECIFIED = -1 + SCREEN_ORIENTATION_LANDSCAPE = 0 + SCREEN_ORIENTATION_PORTRAIT = 1 + SCREEN_ORIENTATION_USER = 2 + SCREEN_ORIENTATION_BEHIND = 3 + SCREEN_ORIENTATION_SENSOR = 4 + SCREEN_ORIENTATION_NOSENSOR = 5 + SCREEN_ORIENTATION_SENSOR_LANDSCAPE = 6 + SCREEN_ORIENTATION_SENSOR_PORTRAIT = 7 + SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8 + SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9 + SCREEN_ORIENTATION_FULL_SENSOR = 10 + SCREEN_ORIENTATION_USER_LANDSCAPE = 11 + SCREEN_ORIENTATION_USER_PORTRAIT = 12 + SCREEN_ORIENTATION_FULL_USER = 13 + SCREEN_ORIENTATION_LOCKED = 14 + + def __init__(self, data: Optional[Dict[str, Any]] = None): + super().__init__(data) + data = data or {} + self.launchMode = data.get("launchMode", self.LAUNCH_MULTIPLE) + self.screenOrientation = data.get("screenOrientation", self.SCREEN_ORIENTATION_UNSPECIFIED) + self.configChanges = data.get("configChanges", 0) + self.softInputMode = data.get("softInputMode", 0) + self.theme = data.get("theme", 0) + self.parentActivityName = data.get("parentActivityName", "") + self.permission = data.get("permission", "") + self.taskAffinity = data.get("taskAffinity", "") + self.targetActivity = data.get("targetActivity", "") + + def toString(self) -> str: + return f"ActivityInfo({self.name}, launchMode={self.launchMode})" + + +# ═══════════════════════════════════════════════════════════════ +# ServiceInfo +# ═══════════════════════════════════════════════════════════════ + +class ServiceInfo(ComponentInfo): + """ServiceInfo — Service 组件信息""" + + FLAG_STOP_WITH_TASK = 0x01 + FLAG_ISOLATED_PROCESS = 0x02 + FLAG_EXTERNAL_SERVICE = 0x04 + FLAG_SINGLE_USER = 0x40000000 + FLAG_USE_APP_OP = 0x08 + + def __init__(self, data: Optional[Dict[str, Any]] = None): + super().__init__(data) + data = data or {} + self.flags = data.get("flags", 0) + self.permission = data.get("permission", "") + self.foregroundServiceType = data.get("foregroundServiceType", 0) + + def toString(self) -> str: + return f"ServiceInfo({self.name})" + + +# ═══════════════════════════════════════════════════════════════ +# ProviderInfo +# ═══════════════════════════════════════════════════════════════ + +class ProviderInfo(ComponentInfo): + """ProviderInfo — ContentProvider 组件信息""" + + def __init__(self, data: Optional[Dict[str, Any]] = None): + super().__init__(data) + data = data or {} + self.authority = data.get("authority", "") + self.readPermission = data.get("readPermission", "") + self.writePermission = data.get("writePermission", "") + self.grantUriPermissions = data.get("grantUriPermissions", False) + self.multiprocess = data.get("multiprocess", False) + self.initOrder = data.get("initOrder", 0) + self.flags = data.get("flags", 0) + + def toString(self) -> str: + return f"ProviderInfo({self.name}, authority={self.authority})" + + +# ═══════════════════════════════════════════════════════════════ +# PermissionInfo +# ═══════════════════════════════════════════════════════════════ + +class PermissionInfo(PackageItemInfo): + """PermissionInfo — 权限信息""" + + PROTECTION_NORMAL = 0 + PROTECTION_DANGEROUS = 1 + PROTECTION_SIGNATURE = 2 + PROTECTION_SIGNATURE_OR_SYSTEM = 3 + PROTECTION_INTERNAL = 4 + + FLAG_COSTS_MONEY = 0x01 + FLAG_HARDWARE_RESTRICTED = 0x04 + FLAG_IMMUTABLY_RESTRICTED = 0x08 + FLAG_INSTALLED = 0x10 + + def __init__(self, data: Optional[Dict[str, Any]] = None): + super().__init__(data) + data = data or {} + self.group = data.get("group", "") + self.descriptionRes = data.get("descriptionRes", 0) + self.protectionLevel = data.get("protectionLevel", self.PROTECTION_NORMAL) + self.flags = data.get("flags", 0) + + def loadDescription(self, pm: "PackageManager") -> Optional[str]: + return None + + def toString(self) -> str: + return f"PermissionInfo({self.name}, protectionLevel={self.protectionLevel})" + + +# ═══════════════════════════════════════════════════════════════ +# ResolveInfo +# ═══════════════════════════════════════════════════════════════ + +class ResolveInfo(Stub): + """ResolveInfo — 解析意图的结果""" + + def __init__(self): + super().__init__() + self.activityInfo: Optional[ActivityInfo] = None + self.serviceInfo: Optional[ServiceInfo] = None + self.providerInfo: Optional[ProviderInfo] = None + self.filter: Optional[Any] = None + self.priority = 0 + self.match = 0 + self.preferredOrder = 0 + self.isDefault = False + self.labelRes = 0 + self.nonLocalizedLabel = None + self.icon = 0 + self.resolvePackageName = "" + self.specificIndex = -1 + self.system = False + + def loadLabel(self, pm: "PackageManager") -> str: + if self.activityInfo: + return self.activityInfo.name.split(".")[-1] + if self.serviceInfo: + return self.serviceInfo.name.split(".")[-1] + return "" + + def loadIcon(self, pm: "PackageManager"): + return None + + def getIconResource(self) -> int: + return self.icon + + def toString(self) -> str: + return f"ResolveInfo(priority={self.priority})" + + +# ═══════════════════════════════════════════════════════════════ +# PackageInfo +# ═══════════════════════════════════════════════════════════════ + +class PackageInfo(Stub): + """PackageInfo — 包信息""" + + INSTALL_LOCATION_AUTO = 0 + INSTALL_LOCATION_INTERNAL_ONLY = 1 + INSTALL_LOCATION_PREFER_EXTERNAL = 2 + REQUESTED_PERMISSION_REQUIRED = 0x00100000 + REQUESTED_PERMISSION_GRANTED = 0x01000000 + REQUESTED_PERMISSION_DENIED = 0x02000000 + + def __init__(self): + super().__init__() + self.packageName = "" + self.versionName = "" + self.versionCode = 0 + self.applicationInfo: Optional[ApplicationInfo] = None + self.activities: List[ActivityInfo] = [] + self.services: List[ServiceInfo] = [] + self.receivers: List[ActivityInfo] = [] + self.providers: List[ProviderInfo] = [] + self.permissions: List[PermissionInfo] = [] + self.requestedPermissions: List[str] = [] + self.requestedPermissionsFlags: List[int] = [] + self.sharedUserId = "" + self.sharedUserLabel = 0 + self.firstInstallTime = 0 + self.lastUpdateTime = 0 + self.gids: List[int] = [] + self.installLocation = self.INSTALL_LOCATION_AUTO + self.configPreferences: List = [] + self.featureGroups: List = [] + self.packageName_ = "" + + def toString(self) -> str: + return (f"PackageInfo({self.packageName} v{self.versionName} " + f"code={self.versionCode})") + + +# ═══════════════════════════════════════════════════════════════ +# Signature +# ═══════════════════════════════════════════════════════════════ + +class Signature(Stub): + """Signature — APK 签名""" + + def __init__(self, signature: str = ""): + super().__init__() + self._signature = signature + + def toCharsString(self) -> str: + return self._signature + + def toByteArray(self) -> bytes: + return self._signature.encode("utf-8") if self._signature else b"" + + def equals(self, other) -> bool: + if isinstance(other, Signature): + return self._signature == other._signature + return False + + def hashCode(self) -> int: + return hash(self._signature) & 0x7FFFFFFF + + def toString(self) -> str: + return self._signature[:64] if len(self._signature) > 64 else self._signature + + +# ═══════════════════════════════════════════════════════════════ +# PackageStats +# ═══════════════════════════════════════════════════════════════ + +class PackageStats(Stub): + """PackageStats — 包存储统计""" + + def __init__(self, package_name: str = ""): + super().__init__() + self.packageName = package_name + self.codeSize = 0 + self.dataSize = 0 + self.cacheSize = 0 + self.externalCodeSize = 0 + self.externalDataSize = 0 + self.externalCacheSize = 0 + self.externalMediaSize = 0 + self.externalObbSize = 0 + + def toString(self) -> str: + return (f"PackageStats({self.packageName}, " + f"code={self.codeSize}, data={self.dataSize})") + + +# ═══════════════════════════════════════════════════════════════ +# PackageManager +# ═══════════════════════════════════════════════════════════════ + +class PackageManager(Stub): + """PackageManager — 包管理器""" + + # Flag 常量 + GET_ACTIVITIES = 1 + GET_CONFIGURATIONS = 2 + GET_GIDS = 4 + GET_INSTRUMENTATION = 8 + GET_INTENT_FILTERS = 16 + GET_META_DATA = 128 + GET_PERMISSIONS = 4096 + GET_PROVIDERS = 8 + GET_RECEIVERS = 4 + GET_SERVICES = 2 + GET_SHARED_LIBRARY_FILES = 32 + GET_SIGNATURES = 64 + GET_UNINSTALLED_PACKAGES = 512 + GET_URI_PERMISSION_PATTERNS = 256 + MATCH_ALL = 0x10000 + MATCH_DISABLED_COMPONENTS = 0x200 + MATCH_DISABLED_UNTIL_USED_COMPONENTS = 0x8000 + MATCH_DEFAULT_ONLY = 0x10000 + MATCH_SYSTEM_ONLY = 0x800000 + MATCH_UNINSTALLED_PACKAGES = 0x2000000 + PERMISSION_DENIED = -1 + PERMISSION_GRANTED = 0 + + # 特性常量 + FEATURE_CAMERA = "android.hardware.camera" + FEATURE_CAMERA_AUTOFOCUS = "android.hardware.camera.autofocus" + FEATURE_CAMERA_FLASH = "android.hardware.camera.flash" + FEATURE_CAMERA_FRONT = "android.hardware.camera.front" + FEATURE_LOCATION = "android.hardware.location" + FEATURE_LOCATION_GPS = "android.hardware.location.gps" + FEATURE_LOCATION_NETWORK = "android.hardware.location.network" + FEATURE_MICROPHONE = "android.hardware.microphone" + FEATURE_NFC = "android.hardware.nfc" + FEATURE_SENSOR_ACCELEROMETER = "android.hardware.sensor.accelerometer" + FEATURE_SENSOR_BAROMETER = "android.hardware.sensor.barometer" + FEATURE_SENSOR_COMPASS = "android.hardware.sensor.compass" + FEATURE_SENSOR_GYROSCOPE = "android.hardware.sensor.gyroscope" + FEATURE_SENSOR_LIGHT = "android.hardware.sensor.light" + FEATURE_SENSOR_PROXIMITY = "android.hardware.sensor.proximity" + FEATURE_TELEPHONY = "android.hardware.telephony" + FEATURE_TOUCHSCREEN = "android.hardware.touchscreen" + FEATURE_USB_HOST = "android.hardware.usb.host" + FEATURE_WIFI = "android.hardware.wifi" + FEATURE_BLUETOOTH = "android.hardware.bluetooth" + + def __init__(self): + super().__init__() + self._packages: Dict[str, Dict[str, Any]] = {} + self._load_packages() + + def _load_packages(self): + """从 JSON 文件加载包信息""" + try: + if os.path.exists(_DEFAULT_PACKAGE_INFO_PATH): + with open(_DEFAULT_PACKAGE_INFO_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + if data.get("packageName"): + self._packages[data["packageName"]] = data + else: + # 创建默认信息 + default_info = _get_default_package_info() + self._packages[_package_name] = default_info + os.makedirs(os.path.dirname(_DEFAULT_PACKAGE_INFO_PATH), exist_ok=True) + with open(_DEFAULT_PACKAGE_INFO_PATH, "w", encoding="utf-8") as f: + json.dump(default_info, f, indent=2, ensure_ascii=False) + except (json.JSONDecodeError, OSError): + self._packages[_package_name] = _get_default_package_info() + + def _make_app_info(self, data: Dict) -> ApplicationInfo: + ai_data = data.get("applicationInfo", data) + return ApplicationInfo(ai_data) + + def _make_activity_info(self, data: Dict) -> ActivityInfo: + return ActivityInfo(data) + + def _make_service_info(self, data: Dict) -> ServiceInfo: + return ServiceInfo(data) + + def _make_provider_info(self, data: Dict) -> ProviderInfo: + return ProviderInfo(data) + + def getPackageInfo(self, package_name: str, flags: int) -> Optional[PackageInfo]: + if package_name not in self._packages: + return None + raw = self._packages[package_name] + pi = PackageInfo() + pi.packageName = raw.get("packageName", package_name) + pi.versionName = raw.get("versionName", "1.0.0") + pi.versionCode = raw.get("versionCode", 1) + pi.applicationInfo = self._make_app_info(raw) + + if flags & self.GET_ACTIVITIES: + pi.activities = [self._make_activity_info(a) for a in raw.get("activities", [])] + if flags & self.GET_SERVICES: + pi.services = [self._make_service_info(s) for s in raw.get("services", [])] + if flags & self.GET_RECEIVERS: + pi.receivers = [self._make_activity_info(r) for r in raw.get("receivers", [])] + if flags & self.GET_PROVIDERS: + pi.providers = [self._make_provider_info(p) for p in raw.get("providers", [])] + if flags & self.GET_PERMISSIONS: + pi.permissions = [PermissionInfo(p) for p in raw.get("permissions", [])] + + pi.requestedPermissions = raw.get("requestedPermissions", []) + return pi + + def getApplicationInfo(self, package_name: str, flags: int) -> Optional[ApplicationInfo]: + if package_name not in self._packages: + return None + return self._make_app_info(self._packages[package_name]) + + def getActivityInfo(self, component: Any, flags: int) -> Optional[ActivityInfo]: + cn = component + pkg = getattr(cn, "getPackageName", lambda: "")() + cls = getattr(cn, "getClassName", lambda: "")() + if pkg in self._packages: + for a in self._packages[pkg].get("activities", []): + if a.get("name") == cls: + return ActivityInfo(a) + return ActivityInfo({"name": cls, "packageName": pkg}) + + def getServiceInfo(self, component: Any, flags: int) -> Optional[ServiceInfo]: + cn = component + pkg = getattr(cn, "getPackageName", lambda: "")() + cls = getattr(cn, "getClassName", lambda: "")() + if pkg in self._packages: + for s in self._packages[pkg].get("services", []): + if s.get("name") == cls: + return ServiceInfo(s) + return ServiceInfo({"name": cls, "packageName": pkg}) + + def getInstalledPackages(self, flags: int) -> List[PackageInfo]: + result = [] + for pkg_name in self._packages: + pi = self.getPackageInfo(pkg_name, flags) + if pi: + result.append(pi) + return result + + def getInstalledApplications(self, flags: int) -> List[ApplicationInfo]: + result = [] + for pkg_name in self._packages: + ai = self.getApplicationInfo(pkg_name, flags) + if ai: + result.append(ai) + return result + + def queryIntentActivities(self, intent: Any, flags: int) -> List[ResolveInfo]: + """查询匹配 Intent 的 Activity 列表""" + results = [] + action = getattr(intent, "getAction", lambda: None)() + for pkg_name, raw in self._packages.items(): + for a in raw.get("activities", []): + ri = ResolveInfo() + ri.activityInfo = ActivityInfo(a) + ri.priority = 0 + ri.match = 0 + results.append(ri) + return results + + def queryBroadcastReceivers(self, intent: Any, flags: int) -> List[ResolveInfo]: + results = [] + for pkg_name, raw in self._packages.items(): + for r in raw.get("receivers", []): + ri = ResolveInfo() + ri.activityInfo = ActivityInfo(r) + results.append(ri) + return results + + def queryIntentServices(self, intent: Any, flags: int) -> List[ResolveInfo]: + results = [] + for pkg_name, raw in self._packages.items(): + for s in raw.get("services", []): + ri = ResolveInfo() + ri.serviceInfo = ServiceInfo(s) + results.append(ri) + return results + + def queryIntentContentProviders(self, intent: Any, flags: int) -> List[ResolveInfo]: + results = [] + for pkg_name, raw in self._packages.items(): + for p in raw.get("providers", []): + ri = ResolveInfo() + ri.providerInfo = ProviderInfo(p) + results.append(ri) + return results + + def resolveActivity(self, intent: Any, flags: int) -> Optional[ResolveInfo]: + activities = self.queryIntentActivities(intent, flags) + return activities[0] if activities else None + + def resolveService(self, intent: Any, flags: int) -> Optional[ResolveInfo]: + services = self.queryIntentServices(intent, flags) + return services[0] if services else None + + def checkPermission(self, permission: str, package_name: str) -> int: + if package_name in self._packages: + perms = self._packages[package_name].get("requestedPermissions", []) + if permission in perms: + return self.PERMISSION_GRANTED + return self.PERMISSION_DENIED if permission else self.PERMISSION_GRANTED + + def getPermissionInfo(self, name: str, flags: int) -> Optional[PermissionInfo]: + return PermissionInfo({"name": name, "packageName": "android"}) + + def getDefaultActivityIcon(self) -> Any: + return None + + def getApplicationLabel(self, info: ApplicationInfo) -> str: + return info.name.split(".")[-1] if info.name else "" + + def getApplicationIcon(self, package_name: str) -> Any: + return None + + def hasSystemFeature(self, feature: str) -> bool: + for pkg_name, raw in self._packages.items(): + features = raw.get("features", {}) + if feature in features: + return features[feature] + return False + + def getLaunchIntentForPackage(self, package_name: str) -> Any: + from .app import Intent + if package_name in self._packages: + intent = Intent(Intent.ACTION_MAIN) + intent.setPackage(package_name) + return intent + return None + + def getLeanbackLaunchIntentForPackage(self, package_name: str) -> Any: + return self.getLaunchIntentForPackage(package_name) + + def getPackageSizeInfo(self, package_name: str, observer: Any): + pass + + def getPackageGids(self, package_name: str) -> Optional[List[int]]: + return [] + + def getPackageUid(self, package_name: str, flags: int) -> int: + return 10001 + + def getNameForUid(self, uid: int) -> Optional[str]: + return _package_name + + def getPackagesForUid(self, uid: int) -> Optional[List[str]]: + return [_package_name] + + def getResourcesForActivity(self, component: Any) -> Any: + return None + + def getResourcesForApplication(self, app_info: ApplicationInfo) -> Any: + return None + + def isSafeMode(self) -> bool: + return False + + def addPackageToPreferred(self, package_name: str): + pass + + def removePackageFromPreferred(self, package_name: str): + pass + + def getPreferredPackages(self, flags: int) -> List[PackageInfo]: + return [] + + def setApplicationEnabledSetting(self, package_name: str, new_state: int, flags: int): + pass + + def getApplicationEnabledSetting(self, package_name: str) -> int: + return 0 + + def getComponentEnabledSetting(self, component_name: Any) -> int: + return 1 + + def setComponentEnabledSetting(self, component_name: Any, new_state: int, flags: int): + pass + + def clearInstantAppCookie(self): + pass + + def getInstantAppCookie(self) -> bytes: + return b"" + + def getInstantAppCookieMaxSize(self) -> int: + return 4096 + + def getMimeGroup(self, mime_group: str) -> List[str]: + return [] + + def isInstantApp(self) -> bool: + return False + + def isInstantAppPackageName(self, package_name: str) -> bool: + return False + + def getSyntheticAppDetailsActivityEnabled(self, package_name: str) -> int: + return 1 + + def setSyntheticAppDetailsActivityEnabled(self, package_name: str, enabled: int): + pass + + def canRequestPackageInstalls(self) -> bool: + return True + + def currentToCanonicalPackageNames(self, names: List[str]) -> List[str]: + return list(names) + + def canonicalToCurrentPackageNames(self, names: List[str]) -> List[str]: + return list(names) + + def getInstallerPackageName(self, package_name: str) -> Optional[str]: + return None + + def toString(self) -> str: + return f"PackageManager(packages={len(self._packages)})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "PackageManager", + "PackageInfo", + "ApplicationInfo", + "ActivityInfo", + "ServiceInfo", + "ProviderInfo", + "PermissionInfo", + "ResolveInfo", + "PackageItemInfo", + "ComponentInfo", + "Signature", + "PackageStats", +] \ No newline at end of file diff --git a/android_pyapi/android/database.py b/android_pyapi/android/database.py new file mode 100644 index 0000000..cea4c18 --- /dev/null +++ b/android_pyapi/android/database.py @@ -0,0 +1,1076 @@ +""" +Android Database 组件 — Cursor, MatrixCursor, ContentObserver 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# SQLException hierarchy +# ═══════════════════════════════════════════════════════════════ + +class SQLException(RuntimeError): + """SQLException — 数据库异常基类""" + + def __init__(self, message: str = ""): + super().__init__(message) + self._error_message = message + + def getMessage(self) -> str: + return self._error_message + + def toString(self) -> str: + return f"SQLException: {self._error_message}" + + +class CursorIndexOutOfBoundsException(SQLException): + """CursorIndexOutOfBoundsException — 游标越界""" + + def __init__(self, message: str = ""): + super().__init__(message) + + +# ═══════════════════════════════════════════════════════════════ +# ContentObserver +# ═══════════════════════════════════════════════════════════════ + +class ContentObserver(Stub): + """ContentObserver — 内容观察者""" + + def __init__(self, handler: Any = None): + super().__init__() + self._handler = handler + self._notify_for_descendants = False + + def onChange(self, self_change: bool = True): + pass + + def onChange(self, self_change: bool, uri: Any = None): # type: ignore + pass + + def deliverSelfNotifications(self) -> bool: + return True + + def getSelfChangeNotify(self) -> bool: + return True + + def dispatchChange(self, self_change: bool, uri: Any = None): + self.onChange(self_change, uri) + + +# ═══════════════════════════════════════════════════════════════ +# DataSetObserver +# ═══════════════════════════════════════════════════════════════ + +class DataSetObserver(Stub): + """DataSetObserver — 数据集观察者""" + + def onChanged(self): + pass + + def onInvalidated(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# ContentObservable +# ═══════════════════════════════════════════════════════════════ + +class ContentObservable(Stub): + """ContentObservable — 内容可观察对象""" + + def __init__(self): + super().__init__() + self._observers: List[ContentObserver] = [] + + def registerObserver(self, observer: ContentObserver): + if observer not in self._observers: + self._observers.append(observer) + + def unregisterObserver(self, observer: ContentObserver): + self._observers = [o for o in self._observers if o is not observer] + + def dispatchChange(self, self_change: bool, uri: Any = None): + for observer in list(self._observers): + observer.dispatchChange(self_change, uri) + + def notifyChange(self, self_change: bool): + for observer in list(self._observers): + observer.onChange(self_change) + + +# ═══════════════════════════════════════════════════════════════ +# DataSetObservable +# ═══════════════════════════════════════════════════════════════ + +class DataSetObservable(Stub): + """DataSetObservable — 数据集可观察对象""" + + def __init__(self): + super().__init__() + self._observers: List[DataSetObserver] = [] + + def registerObserver(self, observer: DataSetObserver): + if observer not in self._observers: + self._observers.append(observer) + + def unregisterObserver(self, observer: DataSetObserver): + self._observers = [o for o in self._observers if o is not observer] + + def notifyChanged(self): + for observer in list(self._observers): + observer.onChanged() + + def notifyInvalidated(self): + for observer in list(self._observers): + observer.onInvalidated() + + +# ═══════════════════════════════════════════════════════════════ +# CharArrayBuffer +# ═══════════════════════════════════════════════════════════════ + +class CharArrayBuffer(Stub): + """CharArrayBuffer — 字符数组缓冲区""" + + def __init__(self, size: int = 0): + super().__init__() + self.data: List[str] = [""] * size + + +# ═══════════════════════════════════════════════════════════════ +# DatabaseErrorHandler +# ═══════════════════════════════════════════════════════════════ + +class DatabaseErrorHandler(Stub): + """DatabaseErrorHandler — 数据库错误处理器""" + + def onCorruption(self, db_obj: Any): + pass + + +class DefaultDatabaseErrorHandler(DatabaseErrorHandler): + """DefaultDatabaseErrorHandler — 默认数据库错误处理器""" + + def onCorruption(self, db_obj: Any): + import sys + sys.stderr.write(f"[DatabaseError] Corruption detected for {db_obj}\n") + + +# ═══════════════════════════════════════════════════════════════ +# CursorWindow +# ═══════════════════════════════════════════════════════════════ + +class CursorWindow(Stub): + """CursorWindow — 游标窗口""" + + def __init__(self, name: str = "unnamed"): + super().__init__() + self._name = name + self._start_position: int = 0 + self._num_rows: int = 0 + self._num_columns: int = 0 + self._data: List[List[Optional[Any]]] = [] + + def setStartPosition(self, pos: int): + self._start_position = pos + + def getStartPosition(self) -> int: + return self._start_position + + def allocRow(self) -> bool: + self._data.append([None] * self._num_columns) + self._num_rows = len(self._data) + return True + + def freeRow(self, row: int): + if 0 <= row < len(self._data): + self._data[row] = [None] * self._num_columns + + def freeLastRow(self): + if self._data: + self._data.pop() + self._num_rows = len(self._data) + + def getNumRows(self) -> int: + return self._num_rows + + def setNumColumns(self, num_columns: int): + self._num_columns = num_columns + + def getNumColumns(self) -> int: + return self._num_columns + + def writeString(self, value: str, row: int, col: int): + while len(self._data) <= row: + self._data.append([None] * self._num_columns) + while len(self._data[row]) <= col: + self._data[row].append(None) + self._data[row][col] = value + + def writeLong(self, value: int, row: int, col: int): + while len(self._data) <= row: + self._data.append([None] * self._num_columns) + while len(self._data[row]) <= col: + self._data[row].append(None) + self._data[row][col] = value + + def writeDouble(self, value: float, row: int, col: int): + while len(self._data) <= row: + self._data.append([None] * self._num_columns) + while len(self._data[row]) <= col: + self._data[row].append(None) + self._data[row][col] = value + + def writeBlob(self, value: bytes, row: int, col: int): + while len(self._data) <= row: + self._data.append([None] * self._num_columns) + while len(self._data[row]) <= col: + self._data[row].append(None) + self._data[row][col] = value + + def writeNull(self, row: int, col: int): + while len(self._data) <= row: + self._data.append([None] * self._num_columns) + while len(self._data[row]) <= col: + self._data[row].append(None) + self._data[row][col] = None + + def getString(self, row: int, col: int) -> Optional[str]: + try: + v = self._data[row][col] + return str(v) if v is not None else None + except (IndexError, TypeError): + return None + + def getLong(self, row: int, col: int) -> int: + try: + v = self._data[row][col] + return int(v) if v is not None else 0 + except (IndexError, ValueError, TypeError): + return 0 + + def getDouble(self, row: int, col: int) -> float: + try: + v = self._data[row][col] + return float(v) if v is not None else 0.0 + except (IndexError, ValueError, TypeError): + return 0.0 + + def getBlob(self, row: int, col: int) -> Optional[bytes]: + try: + v = self._data[row][col] + if isinstance(v, bytes): + return v + if isinstance(v, str): + return v.encode("utf-8") + return None + except IndexError: + return None + + def isNull(self, row: int, col: int) -> bool: + try: + return self._data[row][col] is None + except IndexError: + return True + + def clear(self): + self._data.clear() + self._num_rows = 0 + + def toString(self) -> str: + return f"CursorWindow(name={self._name}, rows={self._num_rows}, cols={self._num_columns})" + + +# ═══════════════════════════════════════════════════════════════ +# Cursor interface +# ═══════════════════════════════════════════════════════════════ + +class Cursor(Stub): + """Cursor — 游标接口""" + + FIELD_TYPE_NULL = 0 + FIELD_TYPE_INTEGER = 1 + FIELD_TYPE_FLOAT = 2 + FIELD_TYPE_STRING = 3 + FIELD_TYPE_BLOB = 4 + + def __init__(self): + super().__init__() + self._pos: int = -1 + self._closed: bool = False + + def getCount(self) -> int: + return 0 + + def getPosition(self) -> int: + return self._pos + + def moveToFirst(self) -> bool: + return self.moveToPosition(0) + + def moveToLast(self) -> bool: + return self.moveToPosition(self.getCount() - 1) + + def moveToNext(self) -> bool: + return self.moveToPosition(self._pos + 1) + + def moveToPrevious(self) -> bool: + return self.moveToPosition(self._pos - 1) + + def move(self, offset: int) -> bool: + return self.moveToPosition(self._pos + offset) + + def moveToPosition(self, position: int) -> bool: + if position < -1 or position >= self.getCount(): + return False + self._pos = position + return True + + def isFirst(self) -> bool: + return self._pos == 0 + + def isLast(self) -> bool: + return self._pos == self.getCount() - 1 + + def isBeforeFirst(self) -> bool: + return self._pos < 0 + + def isAfterLast(self) -> bool: + return self._pos >= self.getCount() + + def getColumnIndex(self, column_name: str) -> int: + names = self.getColumnNames() + try: + return names.index(column_name) + except ValueError: + return -1 + + def getColumnIndexOrThrow(self, column_name: str) -> int: + idx = self.getColumnIndex(column_name) + if idx < 0: + raise IllegalArgumentException(f"Column '{column_name}' does not exist") + return idx + + def getColumnName(self, column_index: int) -> str: + names = self.getColumnNames() + if 0 <= column_index < len(names): + return names[column_index] + return "" + + def getColumnNames(self) -> List[str]: + return [] + + def getType(self, column_index: int) -> int: + return self.FIELD_TYPE_NULL + + def getString(self, column_index: int) -> Optional[str]: + return None + + def getInt(self, column_index: int) -> int: + return 0 + + def getLong(self, column_index: int) -> int: + return 0 + + def getFloat(self, column_index: int) -> float: + return 0.0 + + def getDouble(self, column_index: int) -> float: + return 0.0 + + def getShort(self, column_index: int) -> int: + return 0 + + def getBlob(self, column_index: int) -> Optional[bytes]: + return None + + def getBytes(self, column_index: int) -> Optional[bytes]: + return self.getBlob(column_index) + + def close(self): + self._closed = True + + def isClosed(self) -> bool: + return self._closed + + def requery(self) -> bool: + return False + + def deactivate(self): + pass + + def registerContentObserver(self, observer: ContentObserver): + pass + + def unregisterContentObserver(self, observer: ContentObserver): + pass + + def registerDataSetObserver(self, observer: DataSetObserver): + pass + + def unregisterDataSetObserver(self, observer: DataSetObserver): + pass + + def getWantsAllOnMoveCalls(self) -> bool: + return False + + def getExtras(self) -> Any: + return None + + def respond(self, extras: Any) -> Any: + return None + + def setNotificationUri(self, cr: Any, uri: Any): + pass + + def getNotificationUri(self) -> Any: + return None + + def fillWindow(self, pos: int, window: CursorWindow): + pass + + def copyStringToBuffer(self, column_index: int, buffer: CharArrayBuffer): + val = self.getString(column_index) + if val is not None: + buffer.data = list(val) + + def isNull(self, column_index: int) -> bool: + return self.getType(column_index) == self.FIELD_TYPE_NULL + + +# ═══════════════════════════════════════════════════════════════ +# AbstractCursor +# ═══════════════════════════════════════════════════════════════ + +class AbstractCursor(Cursor): + """AbstractCursor — 游标抽象基类""" + + def __init__(self): + super().__init__() + self._content_observable = ContentObservable() + self._data_set_observable = DataSetObservable() + self._notification_uri: Any = None + self._self_observer: Optional[ContentObserver] = None + + def getCount(self) -> int: + return 0 + + def getColumnNames(self) -> List[str]: + return [] + + def registerContentObserver(self, observer: ContentObserver): + self._content_observable.registerObserver(observer) + + def unregisterContentObserver(self, observer: ContentObserver): + self._content_observable.unregisterObserver(observer) + + def registerDataSetObserver(self, observer: DataSetObserver): + self._data_set_observable.registerObserver(observer) + + def unregisterDataSetObserver(self, observer: DataSetObserver): + self._data_set_observable.unregisterObserver(observer) + + def setNotificationUri(self, cr: Any, uri: Any): + self._notification_uri = uri + + def getNotificationUri(self) -> Any: + return self._notification_uri + + def onChange(self, self_change: bool): + self._content_observable.dispatchChange(self_change) + + def getWantsAllOnMoveCalls(self) -> bool: + return False + + def getExtras(self) -> Any: + return None + + def respond(self, extras: Any) -> Any: + return None + + def fillWindow(self, pos: int, window: CursorWindow): + pass + + +# ═══════════════════════════════════════════════════════════════ +# AbstractWindowedCursor +# ═══════════════════════════════════════════════════════════════ + +class AbstractWindowedCursor(AbstractCursor): + """AbstractWindowedCursor — 带窗口的游标""" + + def __init__(self): + super().__init__() + self._window: Optional[CursorWindow] = None + + def getWindow(self) -> Optional[CursorWindow]: + return self._window + + def setWindow(self, window: CursorWindow): + self._window = window + + def hasWindow(self) -> bool: + return self._window is not None + + def clearBlobCache(self): + pass + + def fillWindow(self, pos: int, window: CursorWindow): + if self._window: + self._window.clear() + + def getString(self, column_index: int) -> Optional[str]: + if self._window and self._pos >= 0: + return self._window.getString(self._pos, column_index) + return None + + def getInt(self, column_index: int) -> int: + if self._window and self._pos >= 0: + return self._window.getLong(self._pos, column_index) + return 0 + + def getLong(self, column_index: int) -> int: + if self._window and self._pos >= 0: + return self._window.getLong(self._pos, column_index) + return 0 + + def getFloat(self, column_index: int) -> float: + if self._window and self._pos >= 0: + return self._window.getDouble(self._pos, column_index) + return 0.0 + + def getDouble(self, column_index: int) -> float: + if self._window and self._pos >= 0: + return self._window.getDouble(self._pos, column_index) + return 0.0 + + def getBlob(self, column_index: int) -> Optional[bytes]: + if self._window and self._pos >= 0: + return self._window.getBlob(self._pos, column_index) + return None + + def isNull(self, column_index: int) -> bool: + if self._window and self._pos >= 0: + return self._window.isNull(self._pos, column_index) + return True + + +# ═══════════════════════════════════════════════════════════════ +# CursorWrapper +# ═══════════════════════════════════════════════════════════════ + +class CursorWrapper(Cursor): + """CursorWrapper — 游标包装器""" + + def __init__(self, cursor: Cursor): + super().__init__() + self._wrapped_cursor = cursor + + def getWrappedCursor(self) -> Cursor: + return self._wrapped_cursor + + def getCount(self) -> int: + return self._wrapped_cursor.getCount() + + def getPosition(self) -> int: + return self._wrapped_cursor.getPosition() + + def moveToFirst(self) -> bool: + return self._wrapped_cursor.moveToFirst() + + def moveToLast(self) -> bool: + return self._wrapped_cursor.moveToLast() + + def moveToNext(self) -> bool: + return self._wrapped_cursor.moveToNext() + + def moveToPrevious(self) -> bool: + return self._wrapped_cursor.moveToPrevious() + + def move(self, offset: int) -> bool: + return self._wrapped_cursor.move(offset) + + def moveToPosition(self, position: int) -> bool: + return self._wrapped_cursor.moveToPosition(position) + + def getColumnNames(self) -> List[str]: + return self._wrapped_cursor.getColumnNames() + + def getColumnIndex(self, column_name: str) -> int: + return self._wrapped_cursor.getColumnIndex(column_name) + + def getColumnIndexOrThrow(self, column_name: str) -> int: + return self._wrapped_cursor.getColumnIndexOrThrow(column_name) + + def getString(self, column_index: int) -> Optional[str]: + return self._wrapped_cursor.getString(column_index) + + def getInt(self, column_index: int) -> int: + return self._wrapped_cursor.getInt(column_index) + + def getLong(self, column_index: int) -> int: + return self._wrapped_cursor.getLong(column_index) + + def getFloat(self, column_index: int) -> float: + return self._wrapped_cursor.getFloat(column_index) + + def getDouble(self, column_index: int) -> float: + return self._wrapped_cursor.getDouble(column_index) + + def getShort(self, column_index: int) -> int: + return self._wrapped_cursor.getShort(column_index) + + def getBlob(self, column_index: int) -> Optional[bytes]: + return self._wrapped_cursor.getBlob(column_index) + + def close(self): + self._wrapped_cursor.close() + + def isClosed(self) -> bool: + return self._wrapped_cursor.isClosed() + + def registerContentObserver(self, observer: ContentObserver): + self._wrapped_cursor.registerContentObserver(observer) + + def unregisterContentObserver(self, observer: ContentObserver): + self._wrapped_cursor.unregisterContentObserver(observer) + + def registerDataSetObserver(self, observer: DataSetObserver): + self._wrapped_cursor.registerDataSetObserver(observer) + + def unregisterDataSetObserver(self, observer: DataSetObserver): + self._wrapped_cursor.unregisterDataSetObserver(observer) + + def getWantsAllOnMoveCalls(self) -> bool: + return self._wrapped_cursor.getWantsAllOnMoveCalls() + + +# ═══════════════════════════════════════════════════════════════ +# CrossProcessCursor / CrossProcessCursorWrapper +# ═══════════════════════════════════════════════════════════════ + +class CrossProcessCursor(Cursor): + """CrossProcessCursor — 跨进程游标""" + + def fillWindow(self, pos: int, window: CursorWindow): + pass + + def getWindow(self) -> Optional[CursorWindow]: + return None + + def onMove(self, old_position: int, new_position: int) -> bool: + return True + + +class CrossProcessCursorWrapper(CursorWrapper): + """CrossProcessCursorWrapper — 跨进程游标包装器""" + + def __init__(self, cursor: Cursor): + super().__init__(cursor) + self._cursor = cursor + + def fillWindow(self, pos: int, window: CursorWindow): + if isinstance(self._cursor, CrossProcessCursor): + self._cursor.fillWindow(pos, window) + + def getWindow(self) -> Optional[CursorWindow]: + if isinstance(self._cursor, CrossProcessCursor): + return self._cursor.getWindow() + return None + + def onMove(self, old_position: int, new_position: int) -> bool: + if isinstance(self._cursor, CrossProcessCursor): + return self._cursor.onMove(old_position, new_position) + return True + + +# ═══════════════════════════════════════════════════════════════ +# MatrixCursor +# ═══════════════════════════════════════════════════════════════ + +class MatrixCursor(AbstractCursor): + """MatrixCursor — 内存矩阵游标""" + + class RowBuilder(Stub): + """RowBuilder — 行构建器""" + + def __init__(self, cursor: "MatrixCursor", row_index: int): + super().__init__() + self._cursor = cursor + self._row_index = row_index + + def add(self, column_value: Any) -> "MatrixCursor.RowBuilder": + self._cursor._add_row_value(self._row_index, column_value) + return self + + def __init__(self, column_names: List[str], initial_capacity: int = 0): + super().__init__() + self._column_names = list(column_names) + self._rows: List[List[Optional[Any]]] = [] + self._num_columns = len(column_names) + + def _add_row_value(self, row_index: int, value: Any): + while len(self._rows) <= row_index: + self._rows.append([]) + self._rows[row_index].append(value) + + def addRow(self, values: Union[List[Any], Tuple[Any, ...]]) -> None: + row = list(values) + [None] * (self._num_columns - len(values)) + self._rows.append(row[:self._num_columns]) + + def newRow(self) -> RowBuilder: + row_index = len(self._rows) + self._rows.append([]) + return MatrixCursor.RowBuilder(self, row_index) + + def getCount(self) -> int: + return len(self._rows) + + def getColumnNames(self) -> List[str]: + return list(self._column_names) + + def getString(self, column_index: int) -> Optional[str]: + if self._pos < 0 or self._pos >= len(self._rows): + return None + if column_index < 0 or column_index >= len(self._rows[self._pos]): + return None + v = self._rows[self._pos][column_index] + return str(v) if v is not None else None + + def getInt(self, column_index: int) -> int: + v = self.getString(column_index) + if v is None: + return 0 + try: + return int(float(v)) + except (ValueError, TypeError): + return 0 + + def getLong(self, column_index: int) -> int: + return self.getInt(column_index) + + def getFloat(self, column_index: int) -> float: + v = self.getString(column_index) + if v is None: + return 0.0 + try: + return float(v) + except (ValueError, TypeError): + return 0.0 + + def getDouble(self, column_index: int) -> float: + return self.getFloat(column_index) + + def getBlob(self, column_index: int) -> Optional[bytes]: + if self._pos < 0 or self._pos >= len(self._rows): + return None + if column_index < 0 or column_index >= len(self._rows[self._pos]): + return None + v = self._rows[self._pos][column_index] + if isinstance(v, bytes): + return v + if isinstance(v, str): + return v.encode("utf-8") + return None + + def getType(self, column_index: int) -> int: + v = None + if 0 <= self._pos < len(self._rows): + row = self._rows[self._pos] + if 0 <= column_index < len(row): + v = row[column_index] + if v is None: + return Cursor.FIELD_TYPE_NULL + if isinstance(v, int): + return Cursor.FIELD_TYPE_INTEGER + if isinstance(v, float): + return Cursor.FIELD_TYPE_FLOAT + if isinstance(v, bytes): + return Cursor.FIELD_TYPE_BLOB + return Cursor.FIELD_TYPE_STRING + + def isNull(self, column_index: int) -> bool: + return self.getType(column_index) == Cursor.FIELD_TYPE_NULL + + def toString(self) -> str: + return f"MatrixCursor(cols={self._column_names}, rows={len(self._rows)})" + + +# ═══════════════════════════════════════════════════════════════ +# MergeCursor +# ═══════════════════════════════════════════════════════════════ + +class MergeCursor(AbstractCursor): + """MergeCursor — 合并多个游标""" + + def __init__(self, cursors: List[Cursor]): + super().__init__() + self._cursors = cursors + self._cursor_index = 0 + self._current_cursor: Optional[Cursor] = None + + def getCount(self) -> int: + return sum(c.getCount() for c in self._cursors) + + def getColumnNames(self) -> List[str]: + if self._cursors: + return self._cursors[0].getColumnNames() + return [] + + def moveToPosition(self, position: int) -> bool: + if position < -1: + return False + if position >= self.getCount(): + return False + offset = 0 + for i, c in enumerate(self._cursors): + count = c.getCount() + if position < offset + count: + self._cursor_index = i + self._current_cursor = c + return c.moveToPosition(position - offset) + offset += count + return False + + def getString(self, column_index: int) -> Optional[str]: + if self._current_cursor and self._pos >= 0: + return self._current_cursor.getString(column_index) + return None + + def getInt(self, column_index: int) -> int: + if self._current_cursor and self._pos >= 0: + return self._current_cursor.getInt(column_index) + return 0 + + def close(self): + for c in self._cursors: + c.close() + super().close() + + +# ═══════════════════════════════════════════════════════════════ +# CursorJoiner +# ═══════════════════════════════════════════════════════════════ + +class CursorJoiner(Stub): + """CursorJoiner — 游标连接器""" + + class Result(Stub): + BOTH = 0 + LEFT = 1 + RIGHT = 2 + + def __init__(self, cursor_a: Cursor, column_names_a: List[str], + cursor_b: Cursor, column_names_b: List[str]): + super().__init__() + self._cursor_a = cursor_a + self._cursor_b = cursor_b + self._col_names_a = column_names_a + self._col_names_b = column_names_b + self._has_next = False + + def hasNext(self) -> bool: + return self._has_next + + def next(self) -> int: + return self.Result.BOTH + + +# ═══════════════════════════════════════════════════════════════ +# DatabaseUtils +# ═══════════════════════════════════════════════════════════════ + +class DatabaseUtils(Stub): + """DatabaseUtils — 数据库工具""" + + STATEMENT_ABORT = "ABORT" + STATEMENT_FAIL = "FAIL" + STATEMENT_IGNORE = "IGNORE" + STATEMENT_REPLACE = "REPLACE" + STATEMENT_ROLLBACK = "ROLLBACK" + + @staticmethod + def cursorFillWindow(cursor: Cursor, pos: int, window: CursorWindow): + cursor.fillWindow(pos, window) + + @staticmethod + def cursorStringToContentValues(cursor: Cursor, field: str, values: Any): + pass + + @staticmethod + def cursorIntToContentValues(cursor: Cursor, field: str, values: Any): + pass + + @staticmethod + def cursorLongToContentValues(cursor: Cursor, field: str, values: Any): + pass + + @staticmethod + def cursorDoubleToContentValues(cursor: Cursor, field: str, values: Any): + pass + + @staticmethod + def cursorRowToContentValues(cursor: Cursor, values: Any): + for col_name in cursor.getColumnNames(): + idx = cursor.getColumnIndex(col_name) + val = cursor.getString(idx) + if hasattr(values, "put"): + values.put(col_name, val) + + @staticmethod + def dumpCursor(cursor: Cursor): + col_names = cursor.getColumnNames() + sys.stderr.write(f"Rows: {cursor.getCount()}\n") + sys.stderr.write(f"Columns: {col_names}\n") + + @staticmethod + def dumpCursorToString(cursor: Cursor) -> str: + return f"Cursor(count={cursor.getCount()})" + + @staticmethod + def bindObjectToProgram(program: Any, index: int, value: Any): + pass + + @staticmethod + def cursorToContentValues(cursor: Cursor, values: Any): + DatabaseUtils.cursorRowToContentValues(cursor, values) + + @staticmethod + def longForQuery(db: Any, query: str, args: Optional[List[str]] = None) -> int: + return 0 + + @staticmethod + def stringForQuery(db: Any, query: str, args: Optional[List[str]] = None) -> Optional[str]: + return None + + @staticmethod + def blobForQuery(db: Any, query: str, args: Optional[List[str]] = None) -> Optional[bytes]: + return None + + @staticmethod + def sqlEscapeString(value: str) -> str: + return value.replace("'", "''") + + @staticmethod + def appendEscapedSQLString(sb: Any, value: str): + escaped = DatabaseUtils.sqlEscapeString(value) + sb.append(f"'{escaped}'") + + @staticmethod + def appendValueToSql(sb: Any, value: Any): + if value is None: + sb.append("NULL") + elif isinstance(value, bool): + sb.append("1" if value else "0") + elif isinstance(value, (int, float)): + sb.append(str(value)) + else: + DatabaseUtils.appendEscapedSQLString(sb, str(value)) + + @staticmethod + def appendSelectionArgs(original_values: List[str], new_values: List[str]) -> List[str]: + return list(original_values) + list(new_values) + + @staticmethod + def getCollationKey(name: str) -> str: + return name.lower() + + @staticmethod + def getHexCollationKey(name: str) -> str: + return name.lower().encode("utf-8").hex() + + @staticmethod + def createDbFromSqlStatements(context: Any, db_name: str, db_version: int, sql_statements: str): + from .database_sqlite import SQLiteDatabase + db_path = os.path.join(_app_data_dir, "databases", db_name) + os.makedirs(os.path.dirname(db_path), exist_ok=True) + db = SQLiteDatabase.openOrCreateDatabase(db_path, None) + for stmt in sql_statements.split(";"): + stmt = stmt.strip() + if stmt: + try: + db.execSQL(stmt) + except Exception: + pass + return db + + +# ═══════════════════════════════════════════════════════════════ +# DatabaseUtilsInsertHelper +# ═══════════════════════════════════════════════════════════════ + +class DatabaseUtilsInsertHelper(Stub): + """DatabaseUtils.InsertHelper — 插入辅助类""" + + TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1 + + def __init__(self, db: Any, table_name: str): + super().__init__() + self._db = db + self._table_name = table_name + + def getTableName(self) -> str: + return self._table_name + + def prepareForInsert(self) -> int: + return 0 + + def prepareForReplace(self) -> int: + return 0 + + def bind(self, index: int, value: Any): + pass + + def execute(self) -> int: + return 0 + + def close(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# Helper +# ═══════════════════════════════════════════════════════════════ + +class IllegalArgumentException(RuntimeError): + """IllegalArgumentException — 非法参数异常""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Cursor", + "MatrixCursor", + "AbstractCursor", + "AbstractWindowedCursor", + "ContentObservable", + "ContentObserver", + "DataSetObservable", + "DatabaseErrorHandler", + "DatabaseUtils", + "DefaultDatabaseErrorHandler", + "CharArrayBuffer", + "CursorJoiner", + "CursorWindow", + "CursorWrapper", + "CrossProcessCursor", + "CrossProcessCursorWrapper", + "DataSetObserver", + "MergeCursor", + "SQLException", + "CursorIndexOutOfBoundsException", + "DatabaseUtilsInsertHelper", +] \ No newline at end of file diff --git a/android_pyapi/android/database_sqlite.py b/android_pyapi/android/database_sqlite.py new file mode 100644 index 0000000..9b2fa5a --- /dev/null +++ b/android_pyapi/android/database_sqlite.py @@ -0,0 +1,965 @@ +""" +Android SQLite 组件 — SQLiteDatabase, SQLiteOpenHelper, SQLiteStatement 等 +使用 Python sqlite3 模块作为后端 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import threading +import sqlite3 as _sqlite3 +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) +from .database import ( + Cursor, AbstractCursor, ContentObserver, DataSetObserver, SQLException as BaseSQLException, +) + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteException hierarchy +# ═══════════════════════════════════════════════════════════════ + +class SQLiteException(BaseSQLException): + """SQLiteException — SQLite 异常基类""" + + def __init__(self, message: str = ""): + super().__init__(message) + self._error_message = message + + +class SQLiteAbortException(SQLiteException): + pass + + +class SQLiteAccessPermException(SQLiteException): + pass + + +class SQLiteBindOrColumnIndexOutOfRangeException(SQLiteException): + pass + + +class SQLiteBlobTooBigException(SQLiteException): + pass + + +class SQLiteCantOpenDatabaseException(SQLiteException): + pass + + +class SQLiteConstraintException(SQLiteException): + pass + + +class SQLiteDatabaseCorruptException(SQLiteException): + pass + + +class SQLiteDatabaseLockedException(SQLiteException): + pass + + +class SQLiteDatatypeMismatchException(SQLiteException): + pass + + +class SQLiteDiskIOException(SQLiteException): + pass + + +class SQLiteDoneException(SQLiteException): + pass + + +class SQLiteException_General(SQLiteException): + pass + + +class SQLiteFullException(SQLiteException): + pass + + +class SQLiteMisuseException(SQLiteException): + pass + + +class SQLiteOutOfMemoryException(SQLiteException): + pass + + +class SQLiteReadOnlyDatabaseException(SQLiteException): + pass + + +class SQLiteTableLockedException(SQLiteException): + pass + + +class SQLiteUnsupportedOperationException(SQLiteException): + pass + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteClosable +# ═══════════════════════════════════════════════════════════════ + +class SQLiteClosable(Stub): + """SQLiteClosable — 可关闭资源基类""" + + def __init__(self): + super().__init__() + self._closed = False + self._reference_count = 1 + + def acquireReference(self): + if self._closed: + raise IllegalStateException("already closed") + self._reference_count += 1 + + def releaseReference(self): + self._reference_count -= 1 + if self._reference_count <= 0: + self.onAllReferencesReleased() + + def releaseReferenceFromContainer(self): + self.releaseReference() + + def onAllReferencesReleased(self): + self.close() + + def close(self): + self._closed = True + + def isClosed(self) -> bool: + return self._closed + + +class IllegalStateException(RuntimeError): + pass + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteProgram +# ═══════════════════════════════════════════════════════════════ + +class SQLiteProgram(SQLiteClosable): + """SQLiteProgram — SQL 程序基类""" + + def __init__(self, db: "SQLiteDatabase", sql: str): + super().__init__() + self._db = db + self._sql = sql + self._bindings: Dict[int, Any] = {} + self._next_bind_index = 1 + + def getDatabase(self) -> "SQLiteDatabase": + return self._db + + def getSql(self) -> str: + return self._sql + + def bindNull(self, index: int): + self._bindings[index] = None + + def bindLong(self, index: int, value: int): + self._bindings[index] = value + + def bindDouble(self, index: int, value: float): + self._bindings[index] = value + + def bindString(self, index: int, value: str): + self._bindings[index] = value + + def bindBlob(self, index: int, value: bytes): + self._bindings[index] = value + + def clearBindings(self): + self._bindings.clear() + + def _bind_parameters(self, cursor: _sqlite3.Cursor): + for idx, val in sorted(self._bindings.items()): + cursor.execute(f"PRAGMA dummy={idx}") + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteStatement +# ═══════════════════════════════════════════════════════════════ + +class SQLiteStatement(SQLiteProgram): + """SQLiteStatement — 预编译语句""" + + def __init__(self, db: "SQLiteDatabase", sql: str): + super().__init__(db, sql) + + def execute(self): + try: + conn = self._db._acquire_connection() + cursor = conn.execute(self._sql, list(self._bindings.values())) + conn.commit() + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), self._sql) + + def executeUpdateDelete(self) -> int: + try: + conn = self._db._acquire_connection() + cursor = conn.execute(self._sql, list(self._bindings.values())) + conn.commit() + return cursor.rowcount + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), self._sql) + + def executeInsert(self) -> int: + try: + conn = self._db._acquire_connection() + cursor = conn.execute(self._sql, list(self._bindings.values())) + conn.commit() + return cursor.lastrowid + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), self._sql) + + def simpleQueryForLong(self) -> int: + try: + conn = self._db._acquire_connection() + cursor = conn.execute(self._sql, list(self._bindings.values())) + row = cursor.fetchone() + conn.commit() + if row: + return int(row[0]) if row[0] is not None else 0 + raise SQLiteDoneException("no results") + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), self._sql) + + def simpleQueryForString(self) -> Optional[str]: + try: + conn = self._db._acquire_connection() + cursor = conn.execute(self._sql, list(self._bindings.values())) + row = cursor.fetchone() + conn.commit() + if row: + return str(row[0]) if row[0] is not None else None + raise SQLiteDoneException("no results") + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), self._sql) + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteQuery +# ═══════════════════════════════════════════════════════════════ + +class SQLiteQuery(SQLiteProgram): + """SQLiteQuery — 查询对象""" + + def __init__(self, db: "SQLiteDatabase", sql: str): + super().__init__(db, sql) + + def toString(self) -> str: + return f"SQLiteQuery: {self._sql}" + + def sql(self) -> str: + return self._sql + + def bind(self, index: int, value: Any): + self._bindings[index] = value + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteCursor +# ═══════════════════════════════════════════════════════════════ + +class SQLiteCursor(AbstractCursor): + """SQLiteCursor — SQLite 游标实现""" + + def __init__(self, db: "SQLiteDatabase", query: SQLiteQuery): + super().__init__() + self._db = db + self._query = query + self._column_names: List[str] = [] + self._rows: List[List[Any]] = [] + self._current_row: Optional[List[Any]] = None + self._loaded = False + self._closed = False + + def _load(self): + if self._loaded or self._closed: + return + self._loaded = True + try: + conn = self._db._acquire_connection() + cursor = conn.execute(self._query.getSql(), list(self._query._bindings.values())) + self._column_names = [desc[0] for desc in cursor.description] if cursor.description else [] + self._rows = cursor.fetchall() + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), self._query.getSql()) + + def getCount(self) -> int: + self._load() + return len(self._rows) + + def getColumnNames(self) -> List[str]: + self._load() + return list(self._column_names) + + def getColumnIndex(self, column_name: str) -> int: + self._load() + try: + return self._column_names.index(column_name) + except ValueError: + return -1 + + def moveToPosition(self, position: int) -> bool: + self._load() + if position < -1 or position >= len(self._rows): + return False + self._pos = position + if 0 <= position < len(self._rows): + self._current_row = self._rows[position] + else: + self._current_row = None + return True + + def getString(self, column_index: int) -> Optional[str]: + if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): + return None + v = self._current_row[column_index] + return str(v) if v is not None else None + + def getInt(self, column_index: int) -> int: + if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): + return 0 + v = self._current_row[column_index] + if v is None: + return 0 + try: + return int(v) + except (ValueError, TypeError): + return 0 + + def getLong(self, column_index: int) -> int: + return self.getInt(column_index) + + def getFloat(self, column_index: int) -> float: + if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): + return 0.0 + v = self._current_row[column_index] + if v is None: + return 0.0 + try: + return float(v) + except (ValueError, TypeError): + return 0.0 + + def getDouble(self, column_index: int) -> float: + return self.getFloat(column_index) + + def getShort(self, column_index: int) -> int: + return self.getInt(column_index) + + def getBlob(self, column_index: int) -> Optional[bytes]: + if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): + return None + v = self._current_row[column_index] + if isinstance(v, bytes): + return v + if v is not None: + return bytes(str(v), "utf-8") + return None + + def getType(self, column_index: int) -> int: + self._load() + if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): + return Cursor.FIELD_TYPE_NULL + v = self._current_row[column_index] + if v is None: + return Cursor.FIELD_TYPE_NULL + if isinstance(v, int): + return Cursor.FIELD_TYPE_INTEGER + if isinstance(v, float): + return Cursor.FIELD_TYPE_FLOAT + if isinstance(v, bytes): + return Cursor.FIELD_TYPE_BLOB + return Cursor.FIELD_TYPE_STRING + + def isNull(self, column_index: int) -> bool: + return self.getType(column_index) == Cursor.FIELD_TYPE_NULL + + def close(self): + self._closed = True + self._rows.clear() + self._current_row = None + + def requery(self) -> bool: + self._loaded = False + self._rows.clear() + self._current_row = None + self._pos = -1 + return True + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteQueryBuilder +# ═══════════════════════════════════════════════════════════════ + +class SQLiteQueryBuilder(Stub): + """SQLiteQueryBuilder — 查询构建器""" + + def __init__(self): + super().__init__() + self._tables: str = "" + self._distinct: bool = False + self._where: Optional[str] = None + + def setTables(self, tables: str): + self._tables = tables + + def getTables(self) -> str: + return self._tables + + def appendWhere(self, where: str): + if self._where: + self._where = f"({self._where}) AND ({where})" + else: + self._where = where + + def appendWhereStandalone(self, where: str): + self._where = where + + def setDistinct(self, distinct: bool): + self._distinct = distinct + + def buildQueryString(self, distinct: bool, tables: str, + columns: Optional[List[str]], where: Optional[str], + group_by: Optional[str], having: Optional[str], + order_by: Optional[str], limit: Optional[str]) -> str: + sql = "SELECT " + if distinct: + sql += "DISTINCT " + if columns and len(columns) > 0: + sql += ", ".join(columns) + else: + sql += "*" + sql += f" FROM {tables}" + if where: + sql += f" WHERE ({where})" + if group_by: + sql += f" GROUP BY {group_by}" + if having: + sql += f" HAVING {having}" + if order_by: + sql += f" ORDER BY {order_by}" + if limit: + sql += f" LIMIT {limit}" + return sql + + def buildQuery(self, columns: Optional[List[str]], where: Optional[str], + group_by: Optional[str], having: Optional[str], + order_by: Optional[str], limit: Optional[str]) -> str: + return self.buildQueryString(self._distinct, self._tables, + columns, where or self._where, + group_by, having, order_by, limit) + + def query(self, db: "SQLiteDatabase", columns: Optional[List[str]], + where: Optional[str], group_by: Optional[str], + having: Optional[str], order_by: Optional[str], + limit: Optional[str]) -> SQLiteCursor: + sql = self.buildQuery(columns, where, group_by, having, order_by, limit) + return db.rawQuery(sql, None) + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteDatabase +# ═══════════════════════════════════════════════════════════════ + +def _map_sqlite_error(msg: str, sql: str = "") -> SQLiteException: + msg_lower = msg.lower() + if "constraint" in msg_lower: + return SQLiteConstraintException(msg) + if "locked" in msg_lower: + return SQLiteDatabaseLockedException(msg) + if "corrupt" in msg_lower: + return SQLiteDatabaseCorruptException(msg) + if "readonly" in msg_lower or "read only" in msg_lower: + return SQLiteReadOnlyDatabaseException(msg) + if "disk" in msg_lower or "full" in msg_lower: + return SQLiteDiskIOException(msg) if "disk" in msg_lower else SQLiteFullException(msg) + if "abort" in msg_lower: + return SQLiteAbortException(msg) + if "permission" in msg_lower: + return SQLiteAccessPermException(msg) + if "out of memory" in msg_lower: + return SQLiteOutOfMemoryException(msg) + if "not a database" in msg_lower or "cantopen" in msg_lower: + return SQLiteCantOpenDatabaseException(msg) + if "misuse" in msg_lower: + return SQLiteMisuseException(msg) + return SQLiteException(msg) + + +class SQLiteDatabase(SQLiteClosable): + """SQLiteDatabase — 数据库操作类""" + + CONFLICT_ROLLBACK = 1 + CONFLICT_ABORT = 2 + CONFLICT_FAIL = 3 + CONFLICT_IGNORE = 4 + CONFLICT_REPLACE = 5 + + OPEN_READWRITE = 0 + OPEN_READONLY = 1 + CREATE_IF_NECESSARY = 268435456 + NO_LOCALIZED_COLLATORS = 16 + + _db_registry: Dict[str, "SQLiteDatabase"] = {} + _lock = threading.Lock() + + def __init__(self, path: str, flags: int = 0): + super().__init__() + self._path = path + self._flags = flags + self._conn: Optional[_sqlite3.Connection] = None + self._in_transaction = False + self._transaction_successful = False + self._version: int = 0 + self._max_size: int = 0 + self._page_size: int = 4096 + self._enable_wal = False + self._attached_dbs: List[Dict[str, Any]] = [] + + @staticmethod + def openDatabase(path: str, cursor_factory: Any = None, + flags: int = 0, error_handler: Any = None) -> "SQLiteDatabase": + db = SQLiteDatabase(path, flags) + try: + db._conn = _sqlite3.connect(path) + db._conn.execute("PRAGMA journal_mode=WAL") + conn_cursor = db._conn.execute("PRAGMA schema_version") + row = conn_cursor.fetchone() + if row: + db._version = row[0] + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e)) + SQLiteDatabase._db_registry[path] = db + return db + + @staticmethod + def openOrCreateDatabase(path: str, cursor_factory: Any = None, + error_handler: Any = None) -> "SQLiteDatabase": + return SQLiteDatabase.openDatabase(path, cursor_factory, + SQLiteDatabase.CREATE_IF_NECESSARY, error_handler) + + @staticmethod + def create(cursor_factory: Any = None, error_handler: Any = None) -> "SQLiteDatabase": + import tempfile + tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) + tmp.close() + return SQLiteDatabase.openOrCreateDatabase(tmp.name, cursor_factory, error_handler) + + @staticmethod + def deleteDatabase(path: str) -> bool: + try: + if path in SQLiteDatabase._db_registry: + db = SQLiteDatabase._db_registry[path] + db.close() + del SQLiteDatabase._db_registry[path] + os.remove(path) + wal_path = path + "-wal" + if os.path.exists(wal_path): + os.remove(wal_path) + shm_path = path + "-shm" + if os.path.exists(shm_path): + os.remove(shm_path) + return True + except (OSError, Exception): + return False + + @staticmethod + def databaseList() -> List[str]: + return list(SQLiteDatabase._db_registry.keys()) + + def _acquire_connection(self) -> _sqlite3.Connection: + if self._conn is None: + raise SQLiteCantOpenDatabaseException("Database not open") + return self._conn + + def execSQL(self, sql: str, bind_args: Optional[List[Any]] = None): + self._ensure_open() + try: + conn = self._acquire_connection() + args = bind_args or [] + conn.execute(sql, args) + if not self._in_transaction: + conn.commit() + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), sql) + + def rawQuery(self, sql: str, selection_args: Optional[List[Any]] = None) -> SQLiteCursor: + self._ensure_open() + query = SQLiteQuery(self, sql) + if selection_args: + for i, arg in enumerate(selection_args): + query.bind(i + 1, arg) + cursor = SQLiteCursor(self, query) + return cursor + + def insert(self, table: str, null_column_hack: Optional[str], + values: Any) -> int: + self._ensure_open() + if values is None: + return -1 + cols = [] + vals = [] + if hasattr(values, "valueSet"): + items = list(values.valueSet()) + elif hasattr(values, "key_set"): + items = [(k, getattr(values, f"get{k[0].upper()}{k[1:]}", lambda: None)()) + for k in values.key_set()] + elif isinstance(values, dict): + items = list(values.items()) + else: + return -1 + for k, v in items: + cols.append(str(k)) + vals.append(v) + if len(cols) == 0: + cols = ["_id"] + vals = [None] + col_str = ", ".join(cols) + placeholders = ", ".join(["?"] * len(vals)) + sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})" + try: + conn = self._acquire_connection() + cursor = conn.execute(sql, vals) + if not self._in_transaction: + conn.commit() + return cursor.lastrowid + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), sql) + + def update(self, table: str, values: Any, + where_clause: Optional[str] = None, + where_args: Optional[List[Any]] = None) -> int: + self._ensure_open() + if values is None: + return 0 + set_pairs = [] + vals = [] + if isinstance(values, dict): + items = list(values.items()) + elif hasattr(values, "valueSet"): + items = list(values.valueSet()) + else: + return 0 + for k, v in items: + set_pairs.append(f"{k} = ?") + vals.append(v) + set_str = ", ".join(set_pairs) + sql = f"UPDATE {table} SET {set_str}" + if where_clause: + sql += f" WHERE {where_clause}" + if where_args: + vals.extend(where_args) + try: + conn = self._acquire_connection() + cursor = conn.execute(sql, vals) + if not self._in_transaction: + conn.commit() + return cursor.rowcount + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), sql) + + def delete(self, table: str, where_clause: Optional[str] = None, + where_args: Optional[List[Any]] = None) -> int: + self._ensure_open() + sql = f"DELETE FROM {table}" + vals: List[Any] = [] + if where_clause: + sql += f" WHERE {where_clause}" + if where_args: + vals = list(where_args) + try: + conn = self._acquire_connection() + cursor = conn.execute(sql, vals) + if not self._in_transaction: + conn.commit() + return cursor.rowcount + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e), sql) + + def query(self, table: str, columns: Optional[List[str]] = None, + selection: Optional[str] = None, + selection_args: Optional[List[Any]] = None, + group_by: Optional[str] = None, having: Optional[str] = None, + order_by: Optional[str] = None, + limit: Optional[str] = None) -> SQLiteCursor: + col_str = ", ".join(columns) if columns else "*" + sql = f"SELECT {col_str} FROM {table}" + if selection: + sql += f" WHERE {selection}" + if group_by: + sql += f" GROUP BY {group_by}" + if having: + sql += f" HAVING {having}" + if order_by: + sql += f" ORDER BY {order_by}" + if limit: + sql += f" LIMIT {limit}" + return self.rawQuery(sql, selection_args) + + def compileStatement(self, sql: str) -> SQLiteStatement: + return SQLiteStatement(self, sql) + + def beginTransaction(self): + self._ensure_open() + try: + conn = self._acquire_connection() + conn.execute("BEGIN TRANSACTION") + self._in_transaction = True + self._transaction_successful = False + except _sqlite3.DatabaseError as e: + raise _map_sqlite_error(str(e)) + + def endTransaction(self): + if self._in_transaction and self._conn: + try: + if not self._transaction_successful: + self._conn.rollback() + else: + self._conn.commit() + except _sqlite3.DatabaseError: + pass + self._in_transaction = False + + def setTransactionSuccessful(self): + if not self._in_transaction: + raise IllegalStateException("Not in transaction") + self._transaction_successful = True + + def inTransaction(self) -> bool: + return self._in_transaction + + def isOpen(self) -> bool: + return self._conn is not None + + def getPath(self) -> str: + return self._path + + def getVersion(self) -> int: + return self._version + + def setVersion(self, version: int): + self._version = version + if self._conn: + try: + self._conn.execute(f"PRAGMA schema_version = {version}") + except _sqlite3.DatabaseError: + pass + + def onCreate(self, db: "SQLiteDatabase"): + pass + + def onConfigure(self, db: "SQLiteDatabase"): + pass + + def enableWriteAheadLogging(self) -> bool: + self._ensure_open() + try: + self._conn.execute("PRAGMA journal_mode=WAL") + self._enable_wal = True + return True + except _sqlite3.DatabaseError: + return False + + def disableWriteAheadLogging(self): + try: + self._conn.execute("PRAGMA journal_mode=DELETE") + self._enable_wal = False + except _sqlite3.DatabaseError: + pass + + def isWriteAheadLoggingEnabled(self) -> bool: + return self._enable_wal + + def getMaximumSize(self) -> int: + return self._max_size + + def getPageSize(self) -> int: + return self._page_size + + def setMaximumSize(self, size: int) -> int: + self._max_size = size + return size + + def setPageSize(self, size: int): + self._page_size = size + if self._conn: + try: + self._conn.execute(f"PRAGMA page_size = {size}") + except _sqlite3.DatabaseError: + pass + + def getPageCount(self) -> int: + if self._conn: + try: + cursor = self._conn.execute("PRAGMA page_count") + row = cursor.fetchone() + if row: + return row[0] + except _sqlite3.DatabaseError: + pass + return 0 + + def yieldIfContendedSafely(self, sleep_time: int = 0) -> bool: + return True + + def needUpgrade(self, new_version: int) -> bool: + return new_version > self._version + + def getAttachedDbs(self) -> Optional[List[Dict[str, Any]]]: + return self._attached_dbs + + def isReadOnly(self) -> bool: + return bool(self._flags & self.OPEN_READONLY) + + def close(self): + if self._conn: + try: + self._conn.close() + except Exception: + pass + self._conn = None + super().close() + + def _ensure_open(self): + if self._conn is None: + raise SQLiteCantOpenDatabaseException("Database not open") + + def toString(self) -> str: + return f"SQLiteDatabase(path={self._path})" + + +# ═══════════════════════════════════════════════════════════════ +# SQLiteOpenHelper +# ═══════════════════════════════════════════════════════════════ + +class SQLiteOpenHelper(Stub): + """SQLiteOpenHelper — 数据库打开辅助类""" + + def __init__(self, context: Any, name: Optional[str], + factory: Any, version: int, + error_handler: Any = None): + super().__init__() + self._context = context + self._name = name + self._factory = factory + self._version = version + self._error_handler = error_handler + self._readable_db: Optional[SQLiteDatabase] = None + self._writable_db: Optional[SQLiteDatabase] = None + self._database_path: Optional[str] = None + self._is_initialized = False + + def _get_database_path(self) -> str: + if self._database_path: + return self._database_path + db_dir = os.path.join(_app_data_dir, "databases") + os.makedirs(db_dir, exist_ok=True) + if self._name: + self._database_path = os.path.join(db_dir, self._name) + else: + import tempfile + tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) + tmp.close() + self._database_path = tmp.name + return self._database_path + + def getWritableDatabase(self) -> SQLiteDatabase: + if self._writable_db and self._writable_db.isOpen(): + return self._writable_db + path = self._get_database_path() + db = SQLiteDatabase.openOrCreateDatabase(path, self._factory, self._error_handler) + if not self._is_initialized: + self._is_initialized = True + current_version = db.getVersion() + if current_version == 0: + self.onCreate(db) + db.setVersion(self._version) + elif current_version < self._version: + self.onUpgrade(db, current_version, self._version) + elif current_version > self._version: + self.onDowngrade(db, current_version, self._version) + self.onOpen(db) + self._writable_db = db + self._readable_db = db + return db + + def getReadableDatabase(self) -> SQLiteDatabase: + return self.getWritableDatabase() + + def onCreate(self, db: SQLiteDatabase): + pass + + def onUpgrade(self, db: SQLiteDatabase, old_version: int, new_version: int): + pass + + def onDowngrade(self, db: SQLiteDatabase, old_version: int, new_version: int): + pass + + def onOpen(self, db: SQLiteDatabase): + pass + + def close(self): + if self._writable_db: + self._writable_db.close() + self._writable_db = None + if self._readable_db: + self._readable_db = None + + def getDatabaseName(self) -> Optional[str]: + return self._name + + def getReadableDB(self) -> Optional[SQLiteDatabase]: + return self._readable_db + + def getWriteableDB(self) -> Optional[SQLiteDatabase]: + return self._writable_db + + def toString(self) -> str: + return f"SQLiteOpenHelper(name={self._name}, version={self._version})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "SQLiteDatabase", + "SQLiteOpenHelper", + "SQLiteStatement", + "SQLiteCursor", + "SQLiteQuery", + "SQLiteQueryBuilder", + "SQLiteClosable", + "SQLiteProgram", + "SQLiteAbortException", + "SQLiteAccessPermException", + "SQLiteBindOrColumnIndexOutOfRangeException", + "SQLiteBlobTooBigException", + "SQLiteCantOpenDatabaseException", + "SQLiteConstraintException", + "SQLiteDatabaseCorruptException", + "SQLiteDatabaseLockedException", + "SQLiteDatatypeMismatchException", + "SQLiteDiskIOException", + "SQLiteDoneException", + "SQLiteException", + "SQLiteFullException", + "SQLiteMisuseException", + "SQLiteOutOfMemoryException", + "SQLiteReadOnlyDatabaseException", + "SQLiteTableLockedException", + "SQLiteUnsupportedOperationException", +] \ No newline at end of file diff --git a/android_pyapi/android/graphics.py b/android_pyapi/android/graphics.py new file mode 100644 index 0000000..26b866a --- /dev/null +++ b/android_pyapi/android/graphics.py @@ -0,0 +1,2792 @@ +""" +Android Graphics 组件 — Bitmap, Canvas, Paint, Color, Path, Rect 等 +方法签名完全对齐 Android SDK +""" + +import array +import math +import struct +import io +import os +from enum import IntEnum +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union + +from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, resolve_android_path + +# Re-export Typeface from text module +from .text import Typeface + + +# ═══════════════════════════════════════════════════════════════ +# Color +# ═══════════════════════════════════════════════════════════════ + +class Color(Stub): + """Color — 颜色工具类""" + + BLACK = 0xFF000000 + DKGRAY = 0xFF444444 + GRAY = 0xFF888888 + LTGRAY = 0xFFCCCCCC + WHITE = 0xFFFFFFFF + RED = 0xFFFF0000 + GREEN = 0xFF00FF00 + BLUE = 0xFF0000FF + YELLOW = 0xFFFFFF00 + CYAN = 0xFF00FFFF + MAGENTA = 0xFFFF00FF + TRANSPARENT = 0 + + @staticmethod + def alpha(color: int) -> int: + return (color >> 24) & 0xFF + + @staticmethod + def red(color: int) -> int: + return (color >> 16) & 0xFF + + @staticmethod + def green(color: int) -> int: + return (color >> 8) & 0xFF + + @staticmethod + def blue(color: int) -> int: + return color & 0xFF + + @staticmethod + def rgb(r: int, g: int, b: int) -> int: + return 0xFF000000 | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF) + + @staticmethod + def argb(a: int, r: int, g: int, b: int) -> int: + return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF) + + @staticmethod + def parseColor(colorString: str) -> int: + s = colorString.strip() + if s.startswith("#"): + s = s[1:] + if len(s) == 3: + s = "".join(c * 2 for c in s) + if len(s) == 6: + s = "FF" + s + return int(s, 16) & 0xFFFFFFFF + if s.startswith("0x"): + return int(s, 16) & 0xFFFFFFFF + name_map = { + "black": Color.BLACK, "white": Color.WHITE, "red": Color.RED, + "green": Color.GREEN, "blue": Color.BLUE, "yellow": Color.YELLOW, + "cyan": Color.CYAN, "magenta": Color.MAGENTA, "transparent": Color.TRANSPARENT, + "dkgray": Color.DKGRAY, "gray": Color.GRAY, "ltgray": Color.LTGRAY, + "darkgray": Color.DKGRAY, "lightgray": Color.LTGRAY, + } + return name_map.get(s.lower(), Color.BLACK) + + @staticmethod + def RGBToHSV(red: int, green: int, blue: int, hsv: List[float]): + r, g, b = red / 255.0, green / 255.0, blue / 255.0 + mx = max(r, g, b) + mn = min(r, g, b) + df = mx - mn + if df == 0: + h = 0.0 + elif mx == r: + h = ((g - b) / df) % 6.0 + elif mx == g: + h = (b - r) / df + 2.0 + else: + h = (r - g) / df + 4.0 + h = h * 60.0 + if h < 0: + h += 360.0 + s = 0.0 if mx == 0 else (df / mx) + v = mx + hsv[0] = h + hsv[1] = s + hsv[2] = v + + @staticmethod + def HSVToColor(hsv: List[float]) -> int: + return Color.HSVToColor(255, hsv) + + @staticmethod + def HSVToColor(alpha: int, hsv: List[float]) -> int: + h, s, v = hsv[0], hsv[1], hsv[2] + hi = int(h / 60.0) % 6 + f = (h / 60.0) - hi + p = v * (1.0 - s) + q = v * (1.0 - f * s) + t = v * (1.0 - (1.0 - f) * s) + if hi == 0: + r, g, b = v, t, p + elif hi == 1: + r, g, b = q, v, p + elif hi == 2: + r, g, b = p, v, t + elif hi == 3: + r, g, b = p, q, v + elif hi == 4: + r, g, b = t, p, v + else: + r, g, b = v, p, q + return Color.argb(alpha, int(r * 255 + 0.5), int(g * 255 + 0.5), int(b * 255 + 0.5)) + + @staticmethod + def ColorToHSV(color: int, hsv: List[float]): + Color.RGBToHSV(Color.red(color), Color.green(color), Color.blue(color), hsv) + + @staticmethod + def getHue(color: int) -> float: + hsv = [0.0, 0.0, 0.0] + Color.ColorToHSV(color, hsv) + return hsv[0] + + @staticmethod + def getSaturation(color: int) -> float: + hsv = [0.0, 0.0, 0.0] + Color.ColorToHSV(color, hsv) + return hsv[1] + + @staticmethod + def getBrightness(color: int) -> float: + hsv = [0.0, 0.0, 0.0] + Color.ColorToHSV(color, hsv) + return hsv[2] + + @staticmethod + def HSLToColor(hsl: List[float], alpha: float = 1.0) -> int: + h, s, l = hsl[0] / 360.0, hsl[1], hsl[2] + if s == 0: + r = g = b = l + else: + def hue2rgb(p, q, t): + if t < 0: + t += 1 + if t > 1: + t -= 1 + if t < 1.0 / 6.0: + return p + (q - p) * 6.0 * t + if t < 1.0 / 2.0: + return q + if t < 2.0 / 3.0: + return p + (q - p) * (2.0 / 3.0 - t) * 6.0 + return p + q = l * (1 + s) if l < 0.5 else l + s - l * s + p = 2 * l - q + r = hue2rgb(p, q, h + 1.0 / 3.0) + g = hue2rgb(p, q, h) + b = hue2rgb(p, q, h - 1.0 / 3.0) + a = max(0, min(255, int(alpha * 255))) + return Color.argb(a, int(r * 255), int(g * 255), int(b * 255)) + + @staticmethod + def ColorToHSL(color: int, hsl: List[float]): + r, g, b = Color.red(color) / 255.0, Color.green(color) / 255.0, Color.blue(color) / 255.0 + mx = max(r, g, b) + mn = min(r, g, b) + l = (mx + mn) / 2.0 + if mx == mn: + h = 0.0 + s = 0.0 + else: + d = mx - mn + s = d / (1 - abs(2 * l - 1)) + if mx == r: + h = ((g - b) / d) % 6.0 + elif mx == g: + h = (b - r) / d + 2.0 + else: + h = (r - g) / d + 4.0 + h = h * 60.0 + if h < 0: + h += 360.0 + hsl[0] = h + hsl[1] = s + hsl[2] = l + + +# ═══════════════════════════════════════════════════════════════ +# Rect +# ═══════════════════════════════════════════════════════════════ + +class Rect(Stub): + """Rect — 矩形 (int 坐标)""" + + def __init__(self, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0): + super().__init__() + self.left = left + self.top = top + self.right = right + self.bottom = bottom + + def width(self) -> int: + return self.right - self.left + + def height(self) -> int: + return self.bottom - self.top + + def centerX(self) -> int: + return (self.left + self.right) // 2 + + def centerY(self) -> int: + return (self.top + self.bottom) // 2 + + def exactCenterX(self) -> float: + return (self.left + self.right) * 0.5 + + def exactCenterY(self) -> float: + return (self.top + self.bottom) * 0.5 + + def set(self, left: int, top: int, right: int, bottom: int): + self.left, self.top, self.right, self.bottom = left, top, right, bottom + + def offset(self, dx: int, dy: int): + self.left += dx + self.top += dy + self.right += dx + self.bottom += dy + + def offsetTo(self, newLeft: int, newTop: int): + self.right += newLeft - self.left + self.bottom += newTop - self.top + self.left = newLeft + self.top = newTop + + def inset(self, dx: int, dy: int): + self.left += dx + self.top += dy + self.right -= dx + self.bottom -= dy + + def contains(self, x: int, y: int) -> bool: + return self.left < self.right and self.top < self.bottom \ + and x >= self.left and x < self.right and y >= self.top and y < self.bottom + + def contains(self, left: int, top: int, right: int, bottom: int) -> bool: + return self.left < self.right and self.top < self.bottom \ + and self.left <= left and self.top <= top \ + and self.right >= right and self.bottom >= bottom + + def intersect(self, left: int, top: int, right: int, bottom: int) -> bool: + nL = max(self.left, left) + nT = max(self.top, top) + nR = min(self.right, right) + nB = min(self.bottom, bottom) + if nL < nR and nT < nB: + self.left, self.top, self.right, self.bottom = nL, nT, nR, nB + return True + return False + + def intersects(self, left: int, top: int, right: int, bottom: int) -> bool: + return self.left < right and self.right > left \ + and self.top < bottom and self.bottom > top + + @staticmethod + def intersects(a: "Rect", b: "Rect") -> bool: + return a.left < b.right and a.right > b.left \ + and a.top < b.bottom and a.bottom > b.top + + def union(self, left: int, top: int, right: int, bottom: int): + if self.isEmpty(): + self.set(left, top, right, bottom) + else: + self.left = min(self.left, left) + self.top = min(self.top, top) + self.right = max(self.right, right) + self.bottom = max(self.bottom, bottom) + + def sort(self): + if self.left > self.right: + self.left, self.right = self.right, self.left + if self.top > self.bottom: + self.top, self.bottom = self.bottom, self.top + + def isEmpty(self) -> bool: + return self.left >= self.right or self.top >= self.bottom + + def setEmpty(self): + self.left = self.top = self.right = self.bottom = 0 + + def toShortString(self) -> str: + return f"[{self.left},{self.top}][{self.right},{self.bottom}]" + + def flattenToString(self) -> str: + return f"{self.left} {self.top} {self.right} {self.bottom}" + + @staticmethod + def unflattenFromString(s: str) -> "Rect": + parts = s.split() + if len(parts) >= 4: + return Rect(int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3])) + return Rect() + + def toString(self) -> str: + return f"Rect({self.left}, {self.top}, {self.right}, {self.bottom})" + + +# ═══════════════════════════════════════════════════════════════ +# RectF +# ═══════════════════════════════════════════════════════════════ + +class RectF(Stub): + """RectF — 矩形 (float 坐标)""" + + def __init__(self, left: float = 0.0, top: float = 0.0, right: float = 0.0, bottom: float = 0.0): + super().__init__() + self.left = left + self.top = top + self.right = right + self.bottom = bottom + + def width(self) -> float: + return self.right - self.left + + def height(self) -> float: + return self.bottom - self.top + + def centerX(self) -> float: + return (self.left + self.right) * 0.5 + + def centerY(self) -> float: + return (self.top + self.bottom) * 0.5 + + def set(self, left: float, top: float, right: float, bottom: float): + self.left, self.top, self.right, self.bottom = left, top, right, bottom + + def offset(self, dx: float, dy: float): + self.left += dx + self.top += dy + self.right += dx + self.bottom += dy + + def offsetTo(self, newLeft: float, newTop: float): + self.right += newLeft - self.left + self.bottom += newTop - self.top + self.left = newLeft + self.top = newTop + + def inset(self, dx: float, dy: float): + self.left += dx + self.top += dy + self.right -= dx + self.bottom -= dy + + def contains(self, x: float, y: float) -> bool: + return self.left < self.right and self.top < self.bottom \ + and x >= self.left and x < self.right and y >= self.top and y < self.bottom + + def intersect(self, left: float, top: float, right: float, bottom: float) -> bool: + nL = max(self.left, left) + nT = max(self.top, top) + nR = min(self.right, right) + nB = min(self.bottom, bottom) + if nL < nR and nT < nB: + self.left, self.top, self.right, self.bottom = nL, nT, nR, nB + return True + return False + + def intersects(self, left: float, top: float, right: float, bottom: float) -> bool: + return self.left < right and self.right > left \ + and self.top < bottom and self.bottom > top + + @staticmethod + def intersects(a: "RectF", b: "RectF") -> bool: + return a.left < b.right and a.right > b.left \ + and a.top < b.bottom and a.bottom > b.top + + def union(self, left: float, top: float, right: float, bottom: float): + if self.isEmpty(): + self.set(left, top, right, bottom) + else: + self.left = min(self.left, left) + self.top = min(self.top, top) + self.right = max(self.right, right) + self.bottom = max(self.bottom, bottom) + + def round(self, r: Rect): + r.left = int(self.left + 0.5) + r.top = int(self.top + 0.5) + r.right = int(self.right + 0.5) + r.bottom = int(self.bottom + 0.5) + + def roundOut(self, r: Rect): + r.left = int(math.floor(self.left)) + r.top = int(math.floor(self.top)) + r.right = int(math.ceil(self.right)) + r.bottom = int(math.ceil(self.bottom)) + + def sort(self): + if self.left > self.right: + self.left, self.right = self.right, self.left + if self.top > self.bottom: + self.top, self.bottom = self.bottom, self.top + + def isEmpty(self) -> bool: + return self.left >= self.right or self.top >= self.bottom + + def setEmpty(self): + self.left = self.top = self.right = self.bottom = 0.0 + + def toShortString(self) -> str: + return f"[{self.left:.1f},{self.top:.1f}][{self.right:.1f},{self.bottom:.1f}]" + + def toString(self) -> str: + return f"RectF({self.left}, {self.top}, {self.right}, {self.bottom})" + + +# ═══════════════════════════════════════════════════════════════ +# Point / PointF +# ═══════════════════════════════════════════════════════════════ + +class Point(Stub): + """Point — 整数坐标点""" + + def __init__(self, x: int = 0, y: int = 0): + super().__init__() + self.x = x + self.y = y + + def set(self, x: int, y: int): + self.x, self.y = x, y + + def offset(self, dx: int, dy: int): + self.x += dx + self.y += dy + + def equals(self, other) -> bool: + if isinstance(other, Point): + return self.x == other.x and self.y == other.y + return False + + def hashCode(self) -> int: + return self.x * 32713 + self.y + + def toString(self) -> str: + return f"Point({self.x}, {self.y})" + + +class PointF(Stub): + """PointF — 浮点坐标点""" + + def __init__(self, x: float = 0.0, y: float = 0.0): + super().__init__() + self.x = x + self.y = y + + def set(self, x: float, y: float): + self.x, self.y = x, y + + def offset(self, dx: float, dy: float): + self.x += dx + self.y += dy + + def equals(self, other) -> bool: + if isinstance(other, PointF): + return self.x == other.x and self.y == other.y + return False + + def hashCode(self) -> int: + return int(self.x * 32713 + self.y) + + def toString(self) -> str: + return f"PointF({self.x}, {self.y})" + + +# ═══════════════════════════════════════════════════════════════ +# Path +# ═══════════════════════════════════════════════════════════════ + +class Path(Stub): + """Path — 路径""" + + class Direction: + CW = 0 + CCW = 1 + + class FillType: + WINDING = 0 + EVEN_ODD = 1 + INVERSE_WINDING = 2 + INVERSE_EVEN_ODD = 3 + + class Op: + DIFFERENCE = 0 + INTERSECT = 1 + UNION = 2 + XOR = 3 + REVERSE_DIFFERENCE = 4 + + def __init__(self): + super().__init__() + self._segments: List[dict] = [] + self._fill_type = Path.FillType.WINDING + self._start_x = 0.0 + self._start_y = 0.0 + self._current_x = 0.0 + self._current_y = 0.0 + + def moveTo(self, x: float, y: float): + self._segments.append({"type": "move", "x": x, "y": y}) + self._current_x = x + self._current_y = y + + def lineTo(self, x: float, y: float): + self._segments.append({"type": "line", "x": x, "y": y}) + self._current_x = x + self._current_y = y + + def quadTo(self, x1: float, y1: float, x2: float, y2: float): + self._segments.append({"type": "quad", "x1": x1, "y1": y1, "x2": x2, "y2": y2}) + self._current_x = x2 + self._current_y = y2 + + def cubicTo(self, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float): + self._segments.append({"type": "cubic", "x1": x1, "y1": y1, "x2": x2, "y2": y2, "x3": x3, "y3": y3}) + self._current_x = x3 + self._current_y = y3 + + def rMoveTo(self, dx: float, dy: float): + self.moveTo(self._current_x + dx, self._current_y + dy) + + def rLineTo(self, dx: float, dy: float): + self.lineTo(self._current_x + dx, self._current_y + dy) + + def rQuadTo(self, dx1: float, dy1: float, dx2: float, dy2: float): + self.quadTo(self._current_x + dx1, self._current_y + dy1, + self._current_x + dx2, self._current_y + dy2) + + def rCubicTo(self, dx1: float, dy1: float, dx2: float, dy2: float, dx3: float, dy3: float): + self.cubicTo(self._current_x + dx1, self._current_y + dy1, + self._current_x + dx2, self._current_y + dy2, + self._current_x + dx3, self._current_y + dy3) + + def addRect(self, left: float, top: float, right: float, bottom: float, + dir: int = Direction.CW): + self.moveTo(left, top) + self.lineTo(right, top) + self.lineTo(right, bottom) + self.lineTo(left, bottom) + self.close() + + def addRoundRect(self, rect: Union["RectF", tuple], rx: float, ry: float, + dir: int = Direction.CW): + if isinstance(rect, RectF): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + else: + l, t, r, b = rect + self.moveTo(l + rx, t) + self.lineTo(r - rx, t) + self.quadTo(r, t, r, t + ry) + self.lineTo(r, b - ry) + self.quadTo(r, b, r - rx, b) + self.lineTo(l + rx, b) + self.quadTo(l, b, l, b - ry) + self.lineTo(l, t + ry) + self.quadTo(l, t, l + rx, t) + self.close() + + def addCircle(self, x: float, y: float, radius: float, dir: int = Direction.CW): + k = 0.5522847498 + self.moveTo(x + radius, y) + self.cubicTo(x + radius, y + radius * k, x + radius * k, y + radius, x, y + radius) + self.cubicTo(x - radius * k, y + radius, x - radius, y + radius * k, x - radius, y) + self.cubicTo(x - radius, y - radius * k, x - radius * k, y - radius, x, y - radius) + self.cubicTo(x + radius * k, y - radius, x + radius, y - radius * k, x + radius, y) + self.close() + + def addOval(self, rect: Union["RectF", tuple], dir: int = Direction.CW): + if isinstance(rect, RectF): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + else: + l, t, r, b = rect + cx = (l + r) / 2.0 + cy = (t + b) / 2.0 + rx = (r - l) / 2.0 + ry = (b - t) / 2.0 + kx = rx * 0.5522847498 + ky = ry * 0.5522847498 + self.moveTo(cx + rx, cy) + self.cubicTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry) + self.cubicTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy) + self.cubicTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry) + self.cubicTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy) + self.close() + + def addArc(self, rect: Union["RectF", tuple], startAngle: float, sweepAngle: float): + if isinstance(rect, RectF): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + else: + l, t, r, b = rect + cx = (l + r) / 2.0 + cy = (t + b) / 2.0 + rx = (r - l) / 2.0 + ry = (b - t) / 2.0 + start_rad = math.radians(startAngle) + end_rad = math.radians(startAngle + sweepAngle) + segments = 4 + da = (end_rad - start_rad) / segments + for i in range(segments + 1): + a = start_rad + i * da + px = cx + rx * math.cos(a) + py = cy + ry * math.sin(a) + if i == 0: + self.moveTo(px, py) + else: + self.lineTo(px, py) + + def addPath(self, path: "Path", dx: float = 0.0, dy: float = 0.0): + for seg in path._segments: + s = dict(seg) + if "x" in s: + s["x"] = s["x"] + dx + if "y" in s: + s["y"] = s["y"] + dy + if "x1" in s: + s["x1"] = s["x1"] + dx + if "y1" in s: + s["y1"] = s["y1"] + dy + if "x2" in s: + s["x2"] = s["x2"] + dx + if "y2" in s: + s["y2"] = s["y2"] + dy + if "x3" in s: + s["x3"] = s["x3"] + dx + if "y3" in s: + s["y3"] = s["y3"] + dy + self._segments.append(s) + self._current_x = path._current_x + dx + self._current_y = path._current_y + dy + + def arcTo(self, rect: Union["RectF", tuple], startAngle: float, sweepAngle: float, + forceMoveTo: bool = False): + if isinstance(rect, RectF): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + else: + l, t, r, b = rect + cx = (l + r) / 2.0 + cy = (t + b) / 2.0 + rx = (r - l) / 2.0 + ry = (b - t) / 2.0 + start_rad = math.radians(startAngle) + end_rad = math.radians(startAngle + sweepAngle) + segments = 4 + da = (end_rad - start_rad) / segments + for i in range(segments + 1): + a = start_rad + i * da + px = cx + rx * math.cos(a) + py = cy + ry * math.sin(a) + if i == 0 and forceMoveTo: + self.moveTo(px, py) + elif i == 0: + self.lineTo(px, py) + else: + self.lineTo(px, py) + + def close(self): + self._segments.append({"type": "close"}) + + def reset(self): + self._segments.clear() + self._current_x = 0.0 + self._current_y = 0.0 + + def rewind(self): + self._segments.clear() + self._current_x = 0.0 + self._current_y = 0.0 + + def set(self, src: "Path"): + self._segments = list(src._segments) + self._fill_type = src._fill_type + self._current_x = src._current_x + self._current_y = src._current_y + + def computeBounds(self, exact: bool = False) -> RectF: + if not self._segments: + return RectF(0, 0, 0, 0) + min_x = float("inf") + min_y = float("inf") + max_x = float("-inf") + max_y = float("-inf") + for seg in self._segments: + for key in ("x", "x1", "x2", "x3"): + if key in seg: + if seg[key] < min_x: + min_x = seg[key] + if seg[key] > max_x: + max_x = seg[key] + for key in ("y", "y1", "y2", "y3"): + if key in seg: + if seg[key] < min_y: + min_y = seg[key] + if seg[key] > max_y: + max_y = seg[key] + if min_x == float("inf"): + return RectF(0, 0, 0, 0) + return RectF(min_x, min_y, max_x, max_y) + + def incReserve(self, extraPtCount: int): + pass + + def op(self, other: "Path", op: int) -> bool: + return False + + def isRect(self) -> bool: + if len(self._segments) != 5: + return False + types = [s["type"] for s in self._segments] + return types == ["move", "line", "line", "line", "close"] + + def isConvex(self) -> bool: + return True + + def setFillType(self, ft: int): + self._fill_type = ft + + def getFillType(self) -> int: + return self._fill_type + + def toggleReverseFillType(self): + mapping = { + Path.FillType.WINDING: Path.FillType.INVERSE_WINDING, + Path.FillType.EVEN_ODD: Path.FillType.INVERSE_EVEN_ODD, + Path.FillType.INVERSE_WINDING: Path.FillType.WINDING, + Path.FillType.INVERSE_EVEN_ODD: Path.FillType.EVEN_ODD, + } + self._fill_type = mapping.get(self._fill_type, Path.FillType.WINDING) + + +# ═══════════════════════════════════════════════════════════════ +# PathMeasure +# ═══════════════════════════════════════════════════════════════ + +class PathMeasure(Stub): + """PathMeasure — 路径测量""" + + def __init__(self, path: Path = None, forceClosed: bool = False): + super().__init__() + self._path = path + self._force_closed = forceClosed + + def setPath(self, path: Path, forceClosed: bool): + self._path = path + self._force_closed = forceClosed + + def getLength(self) -> float: + if not self._path: + return 0.0 + bounds = self._path.computeBounds() + return math.sqrt(bounds.width() ** 2 + bounds.height() ** 2) + + def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool: + if not self._path: + return False + if len(pos) >= 2: + pos[0] = 0.0 + pos[1] = 0.0 + if len(tan) >= 2: + tan[0] = 1.0 + tan[1] = 0.0 + return True + + def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool: + return False + + def getSegment(self, startD: float, stopD: float, dst: Path, startWithMoveTo: bool) -> bool: + return False + + def isClosed(self) -> bool: + return self._force_closed + + def nextContour(self) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# PorterDuff +# ═══════════════════════════════════════════════════════════════ + +class PorterDuff(Stub): + """PorterDuff — 图形混合模式""" + + class Mode: + CLEAR = 0 + SRC = 1 + DST = 2 + SRC_OVER = 3 + DST_OVER = 4 + SRC_IN = 5 + DST_IN = 6 + SRC_OUT = 7 + DST_OUT = 8 + SRC_ATOP = 9 + DST_ATOP = 10 + XOR = 11 + ADD = 12 + MULTIPLY = 13 + SCREEN = 14 + OVERLAY = 15 + DARKEN = 16 + LIGHTEN = 17 + + +# ═══════════════════════════════════════════════════════════════ +# ClipOp +# ═══════════════════════════════════════════════════════════════ + +class ClipOp(Stub): + """ClipOp — 裁剪操作""" + DIFFERENCE = 0 + INTERSECT = 1 + REPLACE = 2 + + +# ═══════════════════════════════════════════════════════════════ +# BlendMode +# ═══════════════════════════════════════════════════════════════ + +class BlendMode(Stub): + """BlendMode — Android 29+ 混合模式""" + CLEAR = 0 + SRC = 1 + DST = 2 + SRC_OVER = 3 + DST_OVER = 4 + SRC_IN = 5 + DST_IN = 6 + SRC_OUT = 7 + DST_OUT = 8 + SRC_ATOP = 9 + DST_ATOP = 10 + XOR = 11 + PLUS = 12 + MODULATE = 13 + SCREEN = 14 + OVERLAY = 15 + DARKEN = 16 + LIGHTEN = 17 + + +# ═══════════════════════════════════════════════════════════════ +# BlendModeColorFilter +# ═══════════════════════════════════════════════════════════════ + +class BlendModeColorFilter(Stub): + """BlendModeColorFilter — 混合模式颜色过滤器""" + + def __init__(self, color: int, mode): + super().__init__() + self._color = color + self._mode = mode + + def getColor(self) -> int: + return self._color + + def getMode(self): + return self._mode + + +# ═══════════════════════════════════════════════════════════════ +# ColorFilter +# ═══════════════════════════════════════════════════════════════ + +class ColorFilter(Stub): + """ColorFilter — 颜色过滤器的抽象基类""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# PorterDuffColorFilter +# ═══════════════════════════════════════════════════════════════ + +class PorterDuffColorFilter(ColorFilter): + """PorterDuffColorFilter — PorterDuff 颜色过滤器""" + + def __init__(self, color: int, mode): + super().__init__() + self._color = color + self._mode = mode + + def getColor(self) -> int: + return self._color + + def getMode(self): + return self._mode + + +# ═══════════════════════════════════════════════════════════════ +# LightingColorFilter +# ═══════════════════════════════════════════════════════════════ + +class LightingColorFilter(ColorFilter): + """LightingColorFilter — 光照颜色过滤器""" + + def __init__(self, mul: int, add: int): + super().__init__() + self._mul = mul + self._add = add + + def getMultiply(self) -> int: + return self._mul + + def getAdd(self) -> int: + return self._add + + +# ═══════════════════════════════════════════════════════════════ +# ComposeColorFilter +# ═══════════════════════════════════════════════════════════════ + +class ComposeColorFilter(ColorFilter): + """ComposeColorFilter — 组合颜色过滤器""" + + def __init__(self, cf1: ColorFilter, cf2: ColorFilter): + super().__init__() + self._cf1 = cf1 + self._cf2 = cf2 + + def getColorFilters(self) -> Tuple[ColorFilter, ColorFilter]: + return (self._cf1, self._cf2) + + +# ═══════════════════════════════════════════════════════════════ +# ColorMatrix +# ═══════════════════════════════════════════════════════════════ + +class ColorMatrix(Stub): + """ColorMatrix — 4x5 颜色变换矩阵""" + + def __init__(self, src: "ColorMatrix" = None): + super().__init__() + if src: + self._array = list(src._array) + else: + self._array = [1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0] + + def getArray(self) -> List[float]: + return self._array + + def set(self, src: "ColorMatrix"): + self._array = list(src._array) + + def reset(self): + self._array = [1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0] + + def setSaturation(self, sat: float): + R_lum = 0.213 + G_lum = 0.715 + B_lum = 0.072 + one_minus = 1.0 - sat + self._array = [ + R_lum * one_minus + sat, G_lum * one_minus, B_lum * one_minus, 0, 0, + R_lum * one_minus, G_lum * one_minus + sat, B_lum * one_minus, 0, 0, + R_lum * one_minus, G_lum * one_minus, B_lum * one_minus + sat, 0, 0, + 0, 0, 0, 1, 0, + ] + + def setRotate(self, axis: int, degrees: float): + rad = math.radians(degrees) + c = math.cos(rad) + s = math.sin(rad) + self.reset() + if axis == 0: + self._array[5] = c + self._array[6] = s + self._array[9] = -s + self._array[10] = c + elif axis == 1: + self._array[0] = c + self._array[2] = -s + self._array[8] = s + self._array[10] = c + elif axis == 2: + self._array[0] = c + self._array[1] = s + self._array[4] = -s + self._array[5] = c + + def setConcat(self, a: "ColorMatrix", b: "ColorMatrix"): + a_arr = a._array + b_arr = b._array + result = [0.0] * 20 + for i in range(4): + for j in range(5): + s = 0.0 + for k in range(4): + s += a_arr[i * 5 + k] * b_arr[k * 5 + j] + result[i * 5 + j] = s + self._array = result + + def preConcat(self, other: "ColorMatrix"): + new = ColorMatrix() + new.setConcat(self, other) + self._array = new._array + + def postConcat(self, other: "ColorMatrix"): + new = ColorMatrix() + new.setConcat(other, self) + self._array = new._array + + def setScale(self, rScale: float, gScale: float, bScale: float, aScale: float): + self._array = [ + rScale, 0, 0, 0, 0, + 0, gScale, 0, 0, 0, + 0, 0, bScale, 0, 0, + 0, 0, 0, aScale, 0, + ] + + +# ═══════════════════════════════════════════════════════════════ +# ColorMatrixColorFilter +# ═══════════════════════════════════════════════════════════════ + +class ColorMatrixColorFilter(ColorFilter): + """ColorMatrixColorFilter — 颜色矩阵过滤器""" + + def __init__(self, matrix: ColorMatrix): + super().__init__() + self._matrix = matrix + + def getMatrix(self) -> ColorMatrix: + return self._matrix + + +# ═══════════════════════════════════════════════════════════════ +# Matrix +# ═══════════════════════════════════════════════════════════════ + +class Matrix(Stub): + """Matrix — 3x3 变换矩阵""" + + IDENTITY_MATRIX: "Matrix" = None + + MPERSP_0 = 6 + MPERSP_1 = 7 + MPERSP_2 = 8 + MSCALE_X = 0 + MSCALE_Y = 4 + MSKEW_X = 1 + MSKEW_Y = 3 + MTRANS_X = 2 + MTRANS_Y = 5 + + def __init__(self, src: "Matrix" = None): + super().__init__() + if src: + self._values = list(src._values) + else: + self._values = [1, 0, 0, + 0, 1, 0, + 0, 0, 1] + + def getValues(self) -> List[float]: + return self._values + + def setValues(self, values: List[float]): + self._values = list(values) + + def reset(self): + self._values = [1, 0, 0, 0, 1, 0, 0, 0, 1] + + def isIdentity(self) -> bool: + return self._values == [1, 0, 0, 0, 1, 0, 0, 0, 1] + + def invert(self, inverse: "Matrix") -> bool: + v = self._values + det = v[0] * (v[4] * v[8] - v[5] * v[7]) \ + - v[1] * (v[3] * v[8] - v[5] * v[6]) \ + + v[2] * (v[3] * v[7] - v[4] * v[6]) + if abs(det) < 1e-10: + return False + inv_det = 1.0 / det + inv = [ + (v[4] * v[8] - v[5] * v[7]) * inv_det, + (v[2] * v[7] - v[1] * v[8]) * inv_det, + (v[1] * v[5] - v[2] * v[4]) * inv_det, + (v[5] * v[6] - v[3] * v[8]) * inv_det, + (v[0] * v[8] - v[2] * v[6]) * inv_det, + (v[2] * v[3] - v[0] * v[5]) * inv_det, + (v[3] * v[7] - v[4] * v[6]) * inv_det, + (v[1] * v[6] - v[0] * v[7]) * inv_det, + (v[0] * v[4] - v[1] * v[3]) * inv_det, + ] + inverse._values = inv + return True + + def setTranslate(self, dx: float, dy: float): + self._values = [1, 0, dx, 0, 1, dy, 0, 0, 1] + + def setScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0): + self._values = [sx, 0, px - sx * px, 0, sy, py - sy * py, 0, 0, 1] + + def setRotate(self, degrees: float, px: float = 0.0, py: float = 0.0): + rad = math.radians(degrees) + c = math.cos(rad) + s = math.sin(rad) + tx = px - px * c + py * s + ty = py - px * s - py * c + self._values = [c, -s, tx, s, c, ty, 0, 0, 1] + + def setSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0): + self._values = [1, kx, -kx * px, ky, 1, -ky * py, 0, 0, 1] + + def setConcat(self, a: "Matrix", b: "Matrix"): + va, vb = a._values, b._values + self._values = [ + va[0] * vb[0] + va[1] * vb[3] + va[2] * vb[6], + va[0] * vb[1] + va[1] * vb[4] + va[2] * vb[7], + va[0] * vb[2] + va[1] * vb[5] + va[2] * vb[8], + va[3] * vb[0] + va[4] * vb[3] + va[5] * vb[6], + va[3] * vb[1] + va[4] * vb[4] + va[5] * vb[7], + va[3] * vb[2] + va[4] * vb[5] + va[5] * vb[8], + va[6] * vb[0] + va[7] * vb[3] + va[8] * vb[6], + va[6] * vb[1] + va[7] * vb[4] + va[8] * vb[7], + va[6] * vb[2] + va[7] * vb[5] + va[8] * vb[8], + ] + + def preConcat(self, other: "Matrix"): + new = Matrix() + new.setConcat(self, other) + self._values = new._values + + def postConcat(self, other: "Matrix"): + new = Matrix() + new.setConcat(other, self) + self._values = new._values + + def preTranslate(self, dx: float, dy: float): + m = Matrix() + m.setTranslate(dx, dy) + self.preConcat(m) + + def preScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0): + m = Matrix() + m.setScale(sx, sy, px, py) + self.preConcat(m) + + def preRotate(self, degrees: float, px: float = 0.0, py: float = 0.0): + m = Matrix() + m.setRotate(degrees, px, py) + self.preConcat(m) + + def preSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0): + m = Matrix() + m.setSkew(kx, ky, px, py) + self.preConcat(m) + + def postTranslate(self, dx: float, dy: float): + m = Matrix() + m.setTranslate(dx, dy) + self.postConcat(m) + + def postScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0): + m = Matrix() + m.setScale(sx, sy, px, py) + self.postConcat(m) + + def postRotate(self, degrees: float, px: float = 0.0, py: float = 0.0): + m = Matrix() + m.setRotate(degrees, px, py) + self.postConcat(m) + + def postSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0): + m = Matrix() + m.setSkew(kx, ky, px, py) + self.postConcat(m) + + def setRectToRect(self, src: RectF, dst: RectF, stf: int) -> bool: + if src.isEmpty() or dst.isEmpty(): + return False + sx = dst.width() / src.width() + sy = dst.height() / src.height() + tx = dst.left - src.left * sx + ty = dst.top - src.top * sy + self._values = [sx, 0, tx, 0, sy, ty, 0, 0, 1] + return True + + def mapRect(self, src: RectF, dst: RectF = None) -> RectF: + pts = [src.left, src.top, src.right, src.bottom] + self.mapPoints(pts) + if dst: + dst.set(pts[0], pts[1], pts[2], pts[3]) + return dst + return RectF(pts[0], pts[1], pts[2], pts[3]) + + def mapPoints(self, pts: List[float], offset: int = 0, count: int = -1): + if count < 0: + count = len(pts) - offset + v = self._values + for i in range(offset, offset + count, 2): + x = pts[i] + y = pts[i + 1] + pts[i] = x * v[0] + y * v[1] + v[2] + pts[i + 1] = x * v[3] + y * v[4] + v[5] + + def mapVectors(self, vecs: List[float]): + v = self._values + for i in range(0, len(vecs), 2): + x = vecs[i] + y = vecs[i + 1] + vecs[i] = x * v[0] + y * v[1] + vecs[i + 1] = x * v[3] + y * v[4] + + def mapRadius(self, radius: float) -> float: + v = self._values + sx = math.sqrt(v[0] * v[0] + v[3] * v[3]) + sy = math.sqrt(v[1] * v[1] + v[4] * v[4]) + return radius * max(sx, sy) + + def toString(self) -> str: + return f"Matrix({self._values})" + + +Matrix.IDENTITY_MATRIX = Matrix() + + +# ═══════════════════════════════════════════════════════════════ +# Shader +# ═══════════════════════════════════════════════════════════════ + +class Shader(Stub): + """Shader — 着色器基类""" + + class TileMode: + CLAMP = 0 + REPEAT = 1 + MIRROR = 2 + + def __init__(self): + super().__init__() + self._local_matrix = None + + def setLocalMatrix(self, matrix: Matrix): + self._local_matrix = matrix + + def getLocalMatrix(self) -> Optional[Matrix]: + return self._local_matrix + + +# ═══════════════════════════════════════════════════════════════ +# LinearGradient +# ═══════════════════════════════════════════════════════════════ + +class LinearGradient(Shader): + """LinearGradient — 线性渐变""" + + def __init__(self, x0: float, y0: float, x1: float, y1: float, + colors: Sequence[int], positions: Sequence[float], + tile: int = Shader.TileMode.CLAMP): + super().__init__() + self._x0, self._y0 = x0, y0 + self._x1, self._y1 = x1, y1 + self._colors = list(colors) + self._positions = list(positions) if positions else None + self._tile = tile + + +# ═══════════════════════════════════════════════════════════════ +# RadialGradient +# ═══════════════════════════════════════════════════════════════ + +class RadialGradient(Shader): + """RadialGradient — 径向渐变""" + + CENTER_COLOR = 0 + EDGE_COLOR = 1 + + def __init__(self, x: float, y: float, radius: float, + colors: Sequence[int], positions: Sequence[float], + tile: int = Shader.TileMode.CLAMP, + centerColor: int = Shader.TileMode.CLAMP): + """centerColor is ignored, kept for API compatibility""" + super().__init__() + self._x, self._y = x, y + self._radius = radius + self._colors = list(colors) + self._positions = list(positions) if positions else None + self._tile = tile + + def getX(self) -> float: + return self._x + + def getY(self) -> float: + return self._y + + def getRadius(self) -> float: + return self._radius + + +# ═══════════════════════════════════════════════════════════════ +# SweepGradient +# ═══════════════════════════════════════════════════════════════ + +class SweepGradient(Shader): + """SweepGradient — 扫描渐变""" + + def __init__(self, x: float, y: float, + colors: Sequence[int], positions: Sequence[float]): + super().__init__() + self._x, self._y = x, y + self._colors = list(colors) + self._positions = list(positions) if positions else None + + def getX(self) -> float: + return self._x + + def getY(self) -> float: + return self._y + + +# ═══════════════════════════════════════════════════════════════ +# BitmapShader +# ═══════════════════════════════════════════════════════════════ + +class BitmapShader(Shader): + """BitmapShader — 位图着色器""" + + def __init__(self, bitmap: "Bitmap", tileX: int, tileY: int): + super().__init__() + self._bitmap = bitmap + self._tile_x = tileX + self._tile_y = tileY + + def getBitmap(self) -> "Bitmap": + return self._bitmap + + +# ═══════════════════════════════════════════════════════════════ +# ComposeShader +# ═══════════════════════════════════════════════════════════════ + +class ComposeShader(Shader): + """ComposeShader — 组合着色器""" + + def __init__(self, shaderA: Shader, shaderB: Shader, mode): + super().__init__() + self._shader_a = shaderA + self._shader_b = shaderB + self._mode = mode + + +# ═══════════════════════════════════════════════════════════════ +# MaskFilter / BlurMaskFilter / EmbossMaskFilter +# ═══════════════════════════════════════════════════════════════ + +class MaskFilter(Stub): + """MaskFilter — 遮罩过滤器基类""" + pass + + +class BlurMaskFilter(MaskFilter): + """BlurMaskFilter — 模糊遮罩过滤器""" + + NORMAL = 0 + SOLID = 1 + OUTER = 2 + INNER = 3 + + def __init__(self, radius: float, style: int): + super().__init__() + self._radius = radius + self._style = style + + def getRadius(self) -> float: + return self._radius + + def getStyle(self) -> int: + return self._style + + +class EmbossMaskFilter(MaskFilter): + """EmbossMaskFilter — 浮雕遮罩过滤器""" + + def __init__(self, direction: List[float], ambient: float, specular: float, blurRadius: float): + super().__init__() + self._direction = list(direction) + self._ambient = ambient + self._specular = specular + self._blur_radius = blurRadius + + +# ═══════════════════════════════════════════════════════════════ +# PathEffect hierarchy +# ═══════════════════════════════════════════════════════════════ + +class PathEffect(Stub): + """PathEffect — 路径效果基类""" + pass + + +class ComposePathEffect(PathEffect): + """ComposePathEffect — 组合路径效果""" + + def __init__(self, outerpe: PathEffect, innerpe: PathEffect): + super().__init__() + self._outer = outerpe + self._inner = innerpe + + +class CornerPathEffect(PathEffect): + """CornerPathEffect — 圆角路径效果""" + + def __init__(self, radius: float): + super().__init__() + self._radius = radius + + +class DashPathEffect(PathEffect): + """DashPathEffect — 虚线路径效果""" + + def __init__(self, intervals: List[float], phase: float): + super().__init__() + self._intervals = list(intervals) + self._phase = phase + + +class DiscretePathEffect(PathEffect): + """DiscretePathEffect — 离散路径效果""" + + def __init__(self, segmentLength: float, deviation: float): + super().__init__() + self._segment_length = segmentLength + self._deviation = deviation + + +class PathDashPathEffect(PathEffect): + """PathDashPathEffect — 路径虚线效果""" + + ROTATE = 0 + MORPH = 1 + TRANSLATE = 2 + + def __init__(self, shape: Path, advance: float, phase: float, style: int): + super().__init__() + self._shape = shape + self._advance = advance + self._phase = phase + self._style = style + + +class SumPathEffect(PathEffect): + """SumPathEffect — 求和路径效果""" + + def __init__(self, first: PathEffect, second: PathEffect): + super().__init__() + self._first = first + self._second = second + + +# ═══════════════════════════════════════════════════════════════ +# Xfermode hierarchy +# ═══════════════════════════════════════════════════════════════ + +class Xfermode(Stub): + """Xfermode — 传输模式基类""" + pass + + +class PorterDuffXfermode(Xfermode): + """PorterDuffXfermode — PorterDuff 传输模式""" + + def __init__(self, mode): + super().__init__() + self._mode = mode + + def getMode(self): + return self._mode + + +class AvoidXfermode(Xfermode): + """AvoidXfermode — deprecated, kept for API compatibility""" + + TARGET = 0 + AVOID = 1 + + def __init__(self, opColor: int, tolerance: int, mode: int): + super().__init__() + self._op_color = opColor + self._tolerance = tolerance + self._mode = mode + + +class PixelXorXfermode(Xfermode): + """PixelXorXfermode — deprecated, kept for API compatibility""" + + def __init__(self, opColor: int): + super().__init__() + self._op_color = opColor + + +# ═══════════════════════════════════════════════════════════════ +# DrawFilter +# ═══════════════════════════════════════════════════════════════ + +class DrawFilter(Stub): + """DrawFilter — 绘制过滤器基类""" + pass + + +class PaintFlagsDrawFilter(DrawFilter): + """PaintFlagsDrawFilter — 画笔标志绘制过滤器""" + + def __init__(self, clearBits: int, setBits: int): + super().__init__() + self._clear_bits = clearBits + self._set_bits = setBits + + +# ═══════════════════════════════════════════════════════════════ +# Camera +# ═══════════════════════════════════════════════════════════════ + +class Camera(Stub): + """Camera — 3D 相机变换""" + + def __init__(self): + super().__init__() + self._location_x = 0.0 + self._location_y = 0.0 + self._location_z = -8.0 + + def save(self): + pass + + def restore(self): + pass + + def translate(self, x: float, y: float, z: float): + self._location_x += x + self._location_y += y + self._location_z += z + + def rotateX(self, deg: float): + pass + + def rotateY(self, deg: float): + pass + + def rotateZ(self, deg: float): + pass + + def rotate(self, x: float, y: float, z: float): + pass + + def getMatrix(self, matrix: Matrix): + matrix.reset() + + def applyToCanvas(self, canvas: "Canvas"): + pass + + def setLocation(self, x: float, y: float, z: float): + self._location_x = x + self._location_y = y + self._location_z = z + + def dotWithNormal(self, dx: float, dy: float, dz: float) -> float: + return 0.0 + + +# ═══════════════════════════════════════════════════════════════ +# LayerRasterizer / Rasterizer +# ═══════════════════════════════════════════════════════════════ + +class Rasterizer(Stub): + """Rasterizer — 光栅化基类 (deprecated)""" + pass + + +class LayerRasterizer(Rasterizer): + """LayerRasterizer — 分层光栅化 (deprecated)""" + + def __init__(self): + super().__init__() + + def addLayer(self, paint: "Paint", dx: float = 0.0, dy: float = 0.0): + pass + + +# ═══════════════════════════════════════════════════════════════ +# NinePatch +# ═══════════════════════════════════════════════════════════════ + +class NinePatch(Stub): + """NinePatch — 九宫格图片""" + + def __init__(self, bitmap: "Bitmap", chunk: bytes): + super().__init__() + self._bitmap = bitmap + self._chunk = chunk if chunk else b"" + + def getBitmap(self) -> "Bitmap": + return self._bitmap + + def getWidth(self) -> int: + return self._bitmap.getWidth() if self._bitmap else 0 + + def getHeight(self) -> int: + return self._bitmap.getHeight() if self._bitmap else 0 + + @staticmethod + def isNinePatchChunk(chunk: bytes) -> bool: + return bool(chunk and len(chunk) > 0) + + +# ═══════════════════════════════════════════════════════════════ +# Picture / PictureDrawable +# ═══════════════════════════════════════════════════════════════ + +class Picture(Stub): + """Picture — 图画录制与回放""" + + def __init__(self): + super().__init__() + self._recorded = [] + + def beginRecording(self, width: int, height: int) -> "Canvas": + canvas = Canvas(width, height) + self._canvas = canvas + return canvas + + def endRecording(self): + self._canvas = None + + def draw(self, canvas: "Canvas"): + for cmd in self._recorded: + pass + + def getWidth(self) -> int: + return self._canvas.getWidth() if hasattr(self, "_canvas") and self._canvas else 0 + + def getHeight(self) -> int: + return self._canvas.getHeight() if hasattr(self, "_canvas") and self._canvas else 0 + + def createFromStream(self, stream) -> "Picture": + return Picture() + + def writeToStream(self, stream) -> bool: + return True + + +class PictureDrawable(Stub): + """PictureDrawable — 图画 Drawable""" + + def __init__(self, picture: Picture): + super().__init__() + self._picture = picture + + def getPicture(self) -> Picture: + return self._picture + + def setPicture(self, picture: Picture): + self._picture = picture + + def draw(self, canvas: "Canvas"): + pass + + +# ═══════════════════════════════════════════════════════════════ +# SurfaceTexture +# ═══════════════════════════════════════════════════════════════ + +class SurfaceTexture(Stub): + """SurfaceTexture — 表面纹理""" + + def __init__(self, texName: int = 0): + super().__init__() + self._tex_name = texName + self._timestamp = 0 + + def getTimestamp(self) -> int: + return self._timestamp + + def updateTexImage(self): + self._timestamp = int(time.time() * 1e9) + + def release(self): + pass + + def setDefaultBufferSize(self, width: int, height: int): + pass + + def attachToGLContext(self, texName: int): + pass + + def detachFromGLContext(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# Region / RegionIterator +# ═══════════════════════════════════════════════════════════════ + +class Region(Stub): + """Region — 区域""" + + OP_DIFFERENCE = 0 + OP_INTERSECT = 1 + OP_UNION = 2 + OP_XOR = 3 + OP_REVERSE_DIFFERENCE = 4 + OP_REPLACE = 5 + + def __init__(self, *args): + super().__init__() + if len(args) == 4: + self._rect = Rect(args[0], args[1], args[2], args[3]) + elif len(args) == 1 and isinstance(args[0], Rect): + self._rect = Rect(args[0].left, args[0].top, args[0].right, args[0].bottom) + else: + self._rect = Rect() + + def set(self, *args) -> bool: + if len(args) == 4: + self._rect.set(args[0], args[1], args[2], args[3]) + elif len(args) == 1 and isinstance(args[0], Rect): + r = args[0] + self._rect.set(r.left, r.top, r.right, r.bottom) + return True + + def getBounds(self, r: Rect = None) -> Rect: + if r: + r.set(self._rect.left, self._rect.top, self._rect.right, self._rect.bottom) + return r + return Rect(self._rect.left, self._rect.top, self._rect.right, self._rect.bottom) + + def isEmpty(self) -> bool: + return self._rect.isEmpty() + + def contains(self, x: int, y: int) -> bool: + return self._rect.contains(x, y) + + def op(self, *args) -> bool: + return False + + def isRect(self) -> bool: + return True + + def isComplex(self) -> bool: + return False + + def toString(self) -> str: + return f"Region({self._rect})" + + +class RegionIterator(Stub): + """RegionIterator — 区域迭代器""" + + def __init__(self, region: Region): + super().__init__() + self._region = region + self._done = False + + def next(self, r: Rect) -> bool: + if self._done: + return False + r.set(self._region._rect.left, self._region._rect.top, + self._region._rect.right, self._region._rect.bottom) + self._done = True + return True + + +# ═══════════════════════════════════════════════════════════════ +# Paint +# ═══════════════════════════════════════════════════════════════ + +class Paint(Stub): + """Paint — 画笔""" + + class Style: + FILL = 0 + STROKE = 1 + FILL_AND_STROKE = 2 + + class Cap: + BUTT = 0 + ROUND = 1 + SQUARE = 2 + + class Join: + MITER = 0 + ROUND = 1 + BEVEL = 2 + + class Align: + LEFT = 0 + CENTER = 1 + RIGHT = 2 + + ANTI_ALIAS_FLAG = 0x01 + FILTER_BITMAP_FLAG = 0x02 + DITHER_FLAG = 0x04 + UNDERLINE_TEXT_FLAG = 0x08 + STRIKE_THRU_TEXT_FLAG = 0x10 + FAKE_BOLD_TEXT_FLAG = 0x20 + LINEAR_TEXT_FLAG = 0x40 + SUBPIXEL_TEXT_FLAG = 0x80 + DEV_KERN_TEXT_FLAG = 0x100 + + ALIGN_LEFT = 0 + ALIGN_CENTER = 1 + ALIGN_RIGHT = 2 + + def __init__(self, flags: int = 0): + super().__init__() + self._flags = flags + self._color = 0xFF000000 + self._alpha = 255 + self._style = Paint.Style.FILL + self._stroke_width = 0.0 + self._text_size = 12.0 + self._typeface = None + self._anti_alias = bool(flags & Paint.ANTI_ALIAS_FLAG) + self._color_filter = None + self._mask_filter = None + self._shader = None + self._path_effect = None + self._xfermode = None + self._filter_bitmap = bool(flags & Paint.FILTER_BITMAP_FLAG) + self._dither = bool(flags & Paint.DITHER_FLAG) + self._subpixel_text = bool(flags & Paint.SUBPIXEL_TEXT_FLAG) + self._underline_text = bool(flags & Paint.UNDERLINE_TEXT_FLAG) + self._strike_thru_text = bool(flags & Paint.STRIKE_THRU_TEXT_FLAG) + self._text_align = Paint.Align.LEFT + self._text_scale_x = 1.0 + self._text_skew_x = 0.0 + self._shadow_layer = None + + def setColor(self, color: int): + self._color = color + self._alpha = (color >> 24) & 0xFF + + def getColor(self) -> int: + return self._color + + def setARGB(self, a: int, r: int, g: int, b: int): + self.setColor(Color.argb(a, r, g, b)) + + def setAlpha(self, alpha: int): + self._alpha = max(0, min(255, alpha)) + self._color = (self._color & 0x00FFFFFF) | (self._alpha << 24) + + def getAlpha(self) -> int: + return self._alpha + + def setStyle(self, style: int): + self._style = style + + def getStyle(self) -> int: + return self._style + + def setStrokeWidth(self, width: float): + self._stroke_width = width + + def getStrokeWidth(self) -> float: + return self._stroke_width + + def setTextSize(self, textSize: float): + self._text_size = textSize + + def getTextSize(self) -> float: + return self._text_size + + def setTypeface(self, typeface: Optional[Typeface]): + self._typeface = typeface + + def getTypeface(self) -> Optional[Typeface]: + return self._typeface + + def setAntiAlias(self, aa: bool): + self._anti_alias = aa + + def isAntiAlias(self) -> bool: + return self._anti_alias + + def setColorFilter(self, filter: Optional[ColorFilter]): + self._color_filter = filter + + def getColorFilter(self) -> Optional[ColorFilter]: + return self._color_filter + + def setMaskFilter(self, maskfilter: Optional[MaskFilter]): + self._mask_filter = maskfilter + + def getMaskFilter(self) -> Optional[MaskFilter]: + return self._mask_filter + + def setShader(self, shader: Optional[Shader]): + self._shader = shader + + def getShader(self) -> Optional[Shader]: + return self._shader + + def setPathEffect(self, effect: Optional[PathEffect]): + self._path_effect = effect + + def getPathEffect(self) -> Optional[PathEffect]: + return self._path_effect + + def setXfermode(self, xfermode: Optional[Xfermode]): + self._xfermode = xfermode + + def getXfermode(self) -> Optional[Xfermode]: + return self._xfermode + + def setFlags(self, flags: int): + self._flags = flags + + def getFlags(self) -> int: + return self._flags + + def setFilterBitmap(self, filter: bool): + self._filter_bitmap = filter + + def isFilterBitmap(self) -> bool: + return self._filter_bitmap + + def setDither(self, dither: bool): + self._dither = dither + + def isDither(self) -> bool: + return self._dither + + def setSubpixelText(self, subpixel: bool): + self._subpixel_text = subpixel + + def isSubpixelText(self) -> bool: + return self._subpixel_text + + def setUnderlineText(self, underline: bool): + self._underline_text = underline + + def isUnderlineText(self) -> bool: + return self._underline_text + + def setStrikeThruText(self, strike: bool): + self._strike_thru_text = strike + + def isStrikeThruText(self) -> bool: + return self._strike_thru_text + + def setTextScaleX(self, scaleX: float): + self._text_scale_x = scaleX + + def getTextScaleX(self) -> float: + return self._text_scale_x + + def setTextSkewX(self, skewX: float): + self._text_skew_x = skewX + + def getTextSkewX(self) -> float: + return self._text_skew_x + + def setTextAlign(self, align: int): + self._text_align = align + + def getTextAlign(self) -> int: + return self._text_align + + def setTextLocales(self, locales): + pass + + def setShadowLayer(self, radius: float, dx: float, dy: float, color: int): + self._shadow_layer = (radius, dx, dy, color) + + def clearShadowLayer(self): + self._shadow_layer = None + + def measureText(self, text: str) -> float: + return len(text) * self._text_size * 0.5 + + def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int: + width = 0.0 + count = 0 + for i, ch in enumerate(text): + cw = self._text_size * 0.5 + if width + cw > maxWidth: + break + width += cw + count += 1 + measuredWidth[0] = width + return count + + def getTextWidths(self, text: str, widths: List[float]) -> int: + cw = self._text_size * 0.5 + for i, ch in enumerate(text): + widths[i] = cw + return len(text) + + def getTextPath(self, text: str, start: int, end: int, x: float, y: float, path: Path): + path.moveTo(x, y) + for i in range(start, end): + path.lineTo(x + (i - start + 1) * self._text_size * 0.3, y) + + def getFontMetrics(self) -> Dict[str, float]: + size = self._text_size + return { + "ascent": -size * 0.8, + "descent": size * 0.2, + "top": -size * 0.95, + "bottom": size * 0.25, + "leading": 0.0, + } + + def getFontMetricsInt(self) -> Dict[str, int]: + fm = self.getFontMetrics() + return {k: int(v) for k, v in fm.items()} + + def ascent(self) -> float: + return -self._text_size * 0.8 + + def descent(self) -> float: + return self._text_size * 0.2 + + +# ═══════════════════════════════════════════════════════════════ +# Bitmap +# ═══════════════════════════════════════════════════════════════ + +class Bitmap(Stub): + """Bitmap — 位图""" + + class Config: + ALPHA_8 = "ALPHA_8" + ARGB_4444 = "ARGB_4444" + ARGB_8888 = "ARGB_8888" + RGB_565 = "RGB_565" + RGBA_F16 = "RGBA_F16" + + class CompressFormat: + JPEG = "JPEG" + PNG = "PNG" + WEBP = "WEBP" + WEBP_LOSSY = "WEBP_LOSSY" + WEBP_LOSSLESS = "WEBP_LOSSLESS" + + def __init__(self, width: int, height: int, config: str = Config.ARGB_8888): + super().__init__() + self._width = width + self._height = height + self._config = config + self._recycled = False + self._has_alpha = config in (Bitmap.Config.ARGB_8888, Bitmap.Config.ARGB_4444, + Bitmap.Config.ALPHA_8, Bitmap.Config.RGBA_F16) + self._generation_id = 1 + + if HAS_PIL: + from PIL import Image + if config == Bitmap.Config.ALPHA_8: + mode = "L" + elif config == Bitmap.Config.RGB_565: + mode = "RGB" + else: + mode = "RGBA" + self._image = Image.new(mode, (width, height), (0, 0, 0, 0)) + else: + bpp = 4 if config == Bitmap.Config.ARGB_8888 else ( + 2 if config in (Bitmap.Config.ARGB_4444, Bitmap.Config.RGB_565) else 1 + ) + self._pixels = bytearray(width * height * bpp) + + @staticmethod + def createBitmap(width, height=None, config=None): + if isinstance(width, Bitmap): + src = width + dst = Bitmap(src._width, src._height, src._config) + if HAS_PIL and hasattr(src, "_image"): + from PIL import Image + dst._image = src._image.copy() + elif hasattr(src, "_pixels"): + dst._pixels = bytearray(src._pixels) + return dst + return Bitmap(width, height, config) + + @staticmethod + def createScaledBitmap(src: "Bitmap", dstWidth: int, dstHeight: int, + filter: bool = True) -> "Bitmap": + if HAS_PIL and hasattr(src, "_image"): + from PIL import Image + dst = Bitmap(dstWidth, dstHeight, src._config) + dst._image = src._image.resize((dstWidth, dstHeight), Image.LANCZOS if filter else Image.NEAREST) + return dst + dst = Bitmap(dstWidth, dstHeight, src._config) + return dst + + def copy(self, config: str, isMutable: bool) -> "Bitmap": + dst = Bitmap(self._width, self._height, config) + if HAS_PIL and hasattr(self, "_image"): + from PIL import Image + if config != self._config: + mode_map = {Bitmap.Config.ARGB_8888: "RGBA", Bitmap.Config.RGB_565: "RGB", + Bitmap.Config.ALPHA_8: "L"} + dst._image = self._image.convert(mode_map.get(config, "RGBA")) + else: + dst._image = self._image.copy() + elif hasattr(self, "_pixels"): + dst._pixels = bytearray(self._pixels) + return dst + + def compress(self, format: str, quality: int, stream) -> bool: + if self._recycled: + return False + if HAS_PIL and hasattr(self, "_image"): + pil_fmt = format.lower() + if pil_fmt == "jpeg": + pil_fmt = "jpeg" + elif pil_fmt == "webp": + pil_fmt = "webp" + else: + pil_fmt = "png" + if pil_fmt == "jpeg" and self._image.mode == "RGBA": + self._image = self._image.convert("RGB") + self._image.save(stream, format=pil_fmt.upper(), quality=quality) + return True + return False + + def recycle(self): + self._recycled = True + if HAS_PIL and hasattr(self, "_image"): + self._image = None + if hasattr(self, "_pixels"): + self._pixels = None + + def isRecycled(self) -> bool: + return self._recycled + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def getConfig(self) -> str: + return self._config + + def getRowBytes(self) -> int: + bpp = {"ALPHA_8": 1, "ARGB_4444": 2, "ARGB_8888": 4, "RGB_565": 2, "RGBA_F16": 8} + return self._width * bpp.get(self._config, 4) + + def getByteCount(self) -> int: + return self.getRowBytes() * self._height + + def getAllocationByteCount(self) -> int: + return self.getByteCount() + + def getPixel(self, x: int, y: int) -> int: + if self._recycled: + return 0 + if HAS_PIL and hasattr(self, "_image"): + p = self._image.getpixel((x, y)) + if self._image.mode == "RGBA": + return Color.argb(p[3], p[0], p[1], p[2]) + elif self._image.mode == "RGB": + return Color.rgb(p[0], p[1], p[2]) + elif self._image.mode == "L": + return Color.argb(255, p, p, p) + return 0 + + def getPixels(self, pixels: List[int], offset: int, stride: int, + x: int, y: int, width: int, height: int): + if self._recycled: + return + for row in range(height): + for col in range(width): + px = x + col + py = y + row + idx = offset + row * stride + col + if 0 <= idx < len(pixels) and 0 <= px < self._width and 0 <= py < self._height: + pixels[idx] = self.getPixel(px, py) + + def setPixel(self, x: int, y: int, color: int): + if self._recycled: + return + if HAS_PIL and hasattr(self, "_image"): + if self._image.mode == "RGBA": + self._image.putpixel((x, y), ( + Color.red(color), Color.green(color), + Color.blue(color), Color.alpha(color) + )) + elif self._image.mode == "RGB": + self._image.putpixel((x, y), ( + Color.red(color), Color.green(color), Color.blue(color) + )) + elif self._image.mode == "L": + self._image.putpixel((x, y), Color.red(color)) + else: + pass + + def setPixels(self, pixels: List[int], offset: int, stride: int, + x: int, y: int, width: int, height: int): + for row in range(height): + for col in range(width): + idx = offset + row * stride + col + if idx < len(pixels): + self.setPixel(x + col, y + row, pixels[idx]) + + def eraseColor(self, color: int): + if self._recycled: + return + if HAS_PIL and hasattr(self, "_image"): + from PIL import Image + w, h = self._image.size + if self._image.mode == "RGBA": + self._image = Image.new("RGBA", (w, h), + (Color.red(color), Color.green(color), + Color.blue(color), Color.alpha(color))) + elif self._image.mode == "RGB": + self._image = Image.new("RGB", (w, h), + (Color.red(color), Color.green(color), Color.blue(color))) + + def extractAlpha(self, paint: Paint = None) -> "Bitmap": + return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8) + + def sameAs(self, other: "Bitmap") -> bool: + if not isinstance(other, Bitmap): + return False + if self._width != other._width or self._height != other._height: + return False + return True + + def prepareToDraw(self): + pass + + def hasAlpha(self) -> bool: + return self._has_alpha + + def setHasAlpha(self, hasAlpha: bool): + self._has_alpha = hasAlpha + + def getGenerationId(self) -> int: + return self._generation_id + + def describeContents(self) -> int: + return 0 + + def writeToParcel(self, dest, flags: int): + pass + + def copyPixelsToBuffer(self, dst): + if HAS_PIL and hasattr(self, "_image"): + buf = self._image.tobytes() + dst.write(buf) + + def copyPixelsFromBuffer(self, src): + if HAS_PIL and hasattr(self, "_image"): + data = src.read() + import io as _io + buf = _io.BytesIO(data) + from PIL import Image + self._image = Image.open(buf) + self._image = self._image.resize((self._width, self._height)) + + def asMutable(self) -> "Bitmap": + return Bitmap.createBitmap(self) + + def toString(self) -> str: + return f"Bitmap({self._width}x{self._height}, config={self._config})" + + +# ═══════════════════════════════════════════════════════════════ +# BitmapFactory +# ═══════════════════════════════════════════════════════════════ + +class BitmapFactory(Stub): + """BitmapFactory — 位图工厂""" + + class Options(Stub): + """BitmapFactory.Options — 解码选项""" + + def __init__(self): + super().__init__() + self.inJustDecodeBounds = False + self.outWidth = 0 + self.outHeight = 0 + self.inSampleSize = 1 + self.inPreferredConfig = Bitmap.Config.ARGB_8888 + self.inDensity = 0 + self.inTargetDensity = 0 + self.inScaled = True + self.inMutable = False + self.outMimeType = "" + self.inPurgeable = False + self.inInputShareable = False + + @staticmethod + def decodeFile(pathName: str, opts: Options = None) -> Optional[Bitmap]: + resolved = resolve_android_path(pathName) + if not os.path.exists(resolved): + return None + if HAS_PIL: + from PIL import Image + try: + img = Image.open(resolved) + if opts and opts.inJustDecodeBounds: + opts.outWidth, opts.outHeight = img.size + opts.outMimeType = f"image/{img.format.lower() if img.format else 'png'}" + return None + w, h = img.size + bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888) + if img.mode != "RGBA": + img = img.convert("RGBA") + bm._image = img + return bm + except Exception: + return None + return Bitmap(100, 100, Bitmap.Config.ARGB_8888) + + @staticmethod + def decodeResource(res, resId: int, opts: Options = None) -> Optional[Bitmap]: + return Bitmap(100, 100, Bitmap.Config.ARGB_8888) + + @staticmethod + def decodeStream(stream, outPadding: "Rect" = None, opts: Options = None) -> Optional[Bitmap]: + if stream is None: + return None + if HAS_PIL: + from PIL import Image + try: + img = Image.open(stream) + if opts and opts.inJustDecodeBounds: + opts.outWidth, opts.outHeight = img.size + return None + w, h = img.size + bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888) + if img.mode != "RGBA": + img = img.convert("RGBA") + bm._image = img + return bm + except Exception: + return None + return Bitmap(100, 100, Bitmap.Config.ARGB_8888) + + @staticmethod + def decodeByteArray(data: bytes, offset: int, length: int, opts: Options = None) -> Optional[Bitmap]: + if not data: + return None + if HAS_PIL: + from PIL import Image + import io as _io + try: + buf = _io.BytesIO(data[offset:offset + length]) + img = Image.open(buf) + if opts and opts.inJustDecodeBounds: + opts.outWidth, opts.outHeight = img.size + return None + w, h = img.size + bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888) + if img.mode != "RGBA": + img = img.convert("RGBA") + bm._image = img + return bm + except Exception: + return None + return Bitmap(100, 100, Bitmap.Config.ARGB_8888) + + @staticmethod + def decodeResourceStream(res, value, stream, pad: "Rect", opts: Options = None) -> Optional[Bitmap]: + return BitmapFactory.decodeStream(stream, pad, opts) + + +# ═══════════════════════════════════════════════════════════════ +# Canvas +# ═══════════════════════════════════════════════════════════════ + +class Canvas(Stub): + """Canvas — 画布""" + + ALL_SAVE_FLAG = 0x1F + + def __init__(self, width: int = 0, height: int = 0): + super().__init__() + if HAS_PIL: + from PIL import Image, ImageDraw + self._width = width + self._height = height + if width > 0 and height > 0: + self._image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + self._draw = ImageDraw.Draw(self._image) + else: + self._image = None + self._draw = None + else: + self._width = width + self._height = height + self._pixels = None + self._bitmap = None + self._save_count = 0 + self._matrix_stack = [Matrix()] + self._clip_stack = [Rect(0, 0, width, height)] + self._matrix = Matrix() + + def setBitmap(self, bitmap: Bitmap): + self._bitmap = bitmap + self._width = bitmap.getWidth() + self._height = bitmap.getHeight() + if HAS_PIL: + from PIL import ImageDraw + if hasattr(bitmap, "_image") and bitmap._image: + self._image = bitmap._image + self._draw = ImageDraw.Draw(self._image) + + def getBitmap(self) -> Optional[Bitmap]: + return self._bitmap + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def save(self) -> int: + self._save_count += 1 + self._matrix_stack.append(Matrix(self._matrix)) + self._clip_stack.append(Rect( + self._clip_stack[-1].left, self._clip_stack[-1].top, + self._clip_stack[-1].right, self._clip_stack[-1].bottom + )) + return self._save_count + + def restore(self): + if len(self._matrix_stack) > 1: + self._matrix = self._matrix_stack.pop() + self._clip_stack.pop() + self._save_count -= 1 + + def saveLayer(self, bounds: RectF, paint: Paint = None, saveFlags: int = ALL_SAVE_FLAG) -> int: + return self.save() + + def getSaveCount(self) -> int: + return self._save_count + + def restoreToCount(self, saveCount: int): + while self._save_count > saveCount: + self.restore() + + def translate(self, dx: float, dy: float): + self._matrix.postTranslate(dx, dy) + + def scale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0): + self._matrix.postScale(sx, sy, px, py) + + def rotate(self, degrees: float, px: float = 0.0, py: float = 0.0): + self._matrix.postRotate(degrees, px, py) + + def skew(self, sx: float, sy: float): + self._matrix.postSkew(sx, sy) + + def concat(self, matrix: Matrix): + self._matrix.postConcat(matrix) + + def setMatrix(self, matrix: Matrix): + self._matrix = Matrix(matrix) + + def getMatrix(self, ctm: Matrix = None) -> Matrix: + if ctm: + ctm._values = list(self._matrix._values) + return ctm + return Matrix(self._matrix) + + def getLocalTransform(self, transform: Matrix = None) -> Matrix: + return self.getMatrix(transform) + + def getDeviceTransform(self, transform: Matrix = None) -> Matrix: + if transform: + transform.reset() + return transform + return Matrix() + + def clipRect(self, left: float, top: float, right: float, bottom: float, + op: int = ClipOp.INTERSECT): + r = self._clip_stack[-1] + if op == ClipOp.INTERSECT: + self._clip_stack[-1] = Rect( + max(r.left, int(left)), max(r.top, int(top)), + min(r.right, int(right)), min(r.bottom, int(bottom)) + ) + elif op == ClipOp.DIFFERENCE: + pass + + def clipPath(self, path: Path, op: int = ClipOp.INTERSECT): + bounds = path.computeBounds() + self.clipRect(bounds.left, bounds.top, bounds.right, bounds.bottom, op) + + def clipRegion(self, region: Region, op: int = ClipOp.INTERSECT): + r = region._rect + self.clipRect(float(r.left), float(r.top), float(r.right), float(r.bottom), op) + + def getClipBounds(self, bounds: Rect = None) -> Rect: + r = self._clip_stack[-1] + if bounds: + bounds.set(r.left, r.top, r.right, r.bottom) + return bounds + return Rect(r.left, r.top, r.right, r.bottom) + + def quickReject(self, *args) -> bool: + return False + + def drawColor(self, color: int): + if HAS_PIL and self._draw is not None: + r, g, b = Color.red(color), Color.green(color), Color.blue(color) + a = Color.alpha(color) + self._draw.rectangle([0, 0, self._width, self._height], + fill=(r, g, b, a)) + + def drawARGB(self, a: int, r: int, g: int, b: int): + self.drawColor(Color.argb(a, r, g, b)) + + def drawRGB(self, r: int, g: int, b: int): + self.drawARGB(255, r, g, b) + + def drawPaint(self, paint: Paint): + self.drawColor(paint.getColor()) + + def drawPoint(self, x: float, y: float, paint: Paint): + if HAS_PIL and self._draw is not None: + c = paint.getColor() + self._draw.point([(x, y)], fill=( + Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c) + )) + + def drawPoints(self, pts: List[float], offset: int = 0, count: int = -1, paint: Paint = None): + if paint is None: + paint = Paint() + if count < 0: + count = len(pts) - offset + for i in range(offset, offset + count, 2): + self.drawPoint(pts[i], pts[i + 1], paint) + + def drawLine(self, startX: float, startY: float, stopX: float, stopY: float, paint: Paint): + if HAS_PIL and self._draw is not None: + c = paint.getColor() + w = paint.getStrokeWidth() if paint.getStyle() != Paint.Style.FILL else 1 + self._draw.line([(startX, startY), (stopX, stopY)], + fill=(Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)), + width=max(1, int(w))) + + def drawLines(self, pts: List[float], offset: int = 0, count: int = -1, paint: Paint = None): + if paint is None: + paint = Paint() + if count < 0: + count = len(pts) - offset + for i in range(offset, offset + count, 4) if count >= 4 else []: + self.drawLine(pts[i], pts[i + 1], pts[i + 2], pts[i + 3], paint) + + def drawRect(self, rect: Union[Rect, RectF, tuple], paint: Paint): + if isinstance(rect, Rect): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + elif isinstance(rect, RectF): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + else: + l, t, r, b = rect + if HAS_PIL and self._draw is not None: + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + if paint.getStyle() == Paint.Style.FILL: + self._draw.rectangle([l, t, r, b], fill=fill_color) + elif paint.getStyle() == Paint.Style.STROKE: + w = max(1, int(paint.getStrokeWidth())) + self._draw.rectangle([l, t, r, b], outline=fill_color, width=w) + else: + self._draw.rectangle([l, t, r, b], fill=fill_color) + + def drawRoundRect(self, rect: Union[RectF, tuple], rx: float, ry: float, paint: Paint): + if isinstance(rect, RectF): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + else: + l, t, r, b = rect + if HAS_PIL and self._draw is not None: + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + self._draw.rounded_rectangle( + [l, t, r, b], radius=rx, + fill=fill_color if paint.getStyle() != Paint.Style.STROKE else None, + outline=fill_color if paint.getStyle() != Paint.Style.FILL else None, + width=max(1, int(paint.getStrokeWidth())) if paint.getStyle() != Paint.Style.FILL else 1 + ) + + def drawCircle(self, cx: float, cy: float, radius: float, paint: Paint): + if HAS_PIL and self._draw is not None: + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + bbox = [cx - radius, cy - radius, cx + radius, cy + radius] + if paint.getStyle() == Paint.Style.FILL: + self._draw.ellipse(bbox, fill=fill_color) + elif paint.getStyle() == Paint.Style.STROKE: + w = max(1, int(paint.getStrokeWidth())) + self._draw.ellipse(bbox, outline=fill_color, width=w) + else: + self._draw.ellipse(bbox, fill=fill_color) + + def drawOval(self, rect: Union[RectF, tuple], paint: Paint): + if isinstance(rect, RectF): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + else: + l, t, r, b = rect + if HAS_PIL and self._draw is not None: + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + self._draw.ellipse([l, t, r, b], fill=fill_color) + + def drawArc(self, rect: Union[RectF, tuple], startAngle: float, sweepAngle: float, + useCenter: bool, paint: Paint): + if isinstance(rect, RectF): + l, t, r, b = rect.left, rect.top, rect.right, rect.bottom + else: + l, t, r, b = rect + if HAS_PIL and self._draw is not None: + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + if not HAS_PIL: + return + from PIL import ImageDraw + self._draw.pieslice( + [l, t, r, b], start=startAngle, end=startAngle + sweepAngle, + fill=fill_color if useCenter or paint.getStyle() != Paint.Style.STROKE else None, + outline=fill_color if paint.getStyle() != Paint.Style.FILL else None, + width=max(1, int(paint.getStrokeWidth())) if paint.getStyle() != Paint.Style.FILL else 1 + ) + + def drawPath(self, path: Path, paint: Paint): + if HAS_PIL and self._draw is not None: + pil_path = [] + for seg in path._segments: + if seg["type"] == "move": + pil_path.append(("M", seg["x"], seg["y"])) + elif seg["type"] == "line": + pil_path.append(("L", seg["x"], seg["y"])) + elif seg["type"] == "close": + pil_path.append(("Z",)) + elif seg["type"] in ("quad", "cubic"): + pass + if pil_path: + from PIL import ImageDraw + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + try: + if paint.getStyle() == Paint.Style.FILL: + self._draw.polygon( + [(s[1], s[2]) for s in pil_path if s[0] in ("M", "L")], + fill=fill_color + ) + else: + w = max(1, int(paint.getStrokeWidth())) + if len(pil_path) > 1: + pts = [(s[1], s[2]) for s in pil_path if s[0] in ("M", "L")] + for i in range(len(pts) - 1): + self._draw.line([pts[i], pts[i + 1]], fill=fill_color, width=w) + except Exception: + pass + + def drawBitmap(self, bitmap: Bitmap, left: float, top: float, paint: Paint = None): + if HAS_PIL and self._draw is not None and hasattr(bitmap, "_image") and bitmap._image: + self._image.paste(bitmap._image, (int(left), int(top))) + elif HAS_PIL and self._image is not None: + pass + + def drawBitmap(self, bitmap: Bitmap, src: Rect, dst: RectF, paint: Paint = None): + if HAS_PIL and self._image is not None and hasattr(bitmap, "_image") and bitmap._image: + if src: + cropped = bitmap._image.crop((src.left, src.top, src.right, src.bottom)) + resized = cropped.resize((int(dst.width()), int(dst.height()))) + self._image.paste(resized, (int(dst.left), int(dst.top))) + else: + resized = bitmap._image.resize((int(dst.width()), int(dst.height()))) + self._image.paste(resized, (int(dst.left), int(dst.top))) + + def drawText(self, text: str, x: float, y: float, paint: Paint): + if HAS_PIL and self._draw is not None: + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + self._draw.text((x, y), text, fill=fill_color) + + def drawTextOnPath(self, text: str, path: Path, hOffset: float, vOffset: float, paint: Paint): + pass + + def drawPosText(self, text: str, pos: List[float], paint: Paint): + if HAS_PIL and self._draw is not None: + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + for i, ch in enumerate(text): + if i * 2 + 1 < len(pos): + self._draw.text((pos[i * 2], pos[i * 2 + 1]), ch, fill=fill_color) + + def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None): + pass + + def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None): + pass + + def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float, + inner: RectF, innerRx: float, innerRy: float, paint: Paint): + pass + + def drawBitmapMesh(self, bitmap: Bitmap, meshWidth: int, meshHeight: int, + verts: List[float], vertOffset: int, colors: List[int], + colorOffset: int, paint: Paint = None): + pass + + def drawVertices(self, mode: int, vertexCount: int, verts: List[float], + vertOffset: int, texs: List[float], texOffset: int, + colors: List[int], colorOffset: int, indices: List[int], + indexOffset: int, indexCount: int, paint: Paint): + pass + + def isHardwareAccelerated(self) -> bool: + return False + + def isOpaque(self) -> bool: + return False + + def setDensity(self, density: int): + pass + + def getDensity(self) -> int: + return 0 + + def toString(self) -> str: + return f"Canvas({self._width}x{self._height})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + # Core + "Bitmap", "BitmapFactory", "Canvas", "Paint", "Color", "ColorFilter", + "PorterDuffColorFilter", "LightingColorFilter", "ComposeColorFilter", + "ColorMatrix", "ColorMatrixColorFilter", "Path", "PathMeasure", + "Rect", "RectF", "Point", "PointF", "Typeface", "Matrix", + + # Shader hierarchy + "Shader", "LinearGradient", "RadialGradient", "SweepGradient", + "BitmapShader", "ComposeShader", + + # MaskFilter hierarchy + "MaskFilter", "BlurMaskFilter", "EmbossMaskFilter", + + # PorterDuff + "PorterDuff", "PorterDuffXfermode", "Xfermode", + "AvoidXfermode", "PixelXorXfermode", + + # DrawFilter + "DrawFilter", "PaintFlagsDrawFilter", + + # PathEffect hierarchy + "PathEffect", "SumPathEffect", "CornerPathEffect", "DashPathEffect", + "DiscretePathEffect", "PathDashPathEffect", "ComposePathEffect", + + # Misc + "Camera", "SurfaceTexture", "Region", "RegionIterator", + "NinePatch", "Picture", "PictureDrawable", + "Rasterizer", "LayerRasterizer", + + # BlendMode + "BlendMode", "BlendModeColorFilter", + "ClipOp", +] \ No newline at end of file diff --git a/android_pyapi/android/graphics_drawable.py b/android_pyapi/android/graphics_drawable.py new file mode 100644 index 0000000..ea929de --- /dev/null +++ b/android_pyapi/android/graphics_drawable.py @@ -0,0 +1,1128 @@ +""" +Android Graphics Drawable 组件 — Drawable, BitmapDrawable, ColorDrawable 等 +方法签名完全对齐 Android SDK +""" + +import math +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union + +from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, resolve_android_path +from .graphics import ( + Bitmap, Canvas, Color, ColorFilter, Paint, PorterDuff, PorterDuffColorFilter, + Rect, RectF, Matrix, Path, Shader, Rect, + BlendMode, BlendModeColorFilter, +) + + +# ═══════════════════════════════════════════════════════════════ +# ColorStateList +# ═══════════════════════════════════════════════════════════════ + +class ColorStateList(Stub): + """ColorStateList — 颜色状态列表""" + + def __init__(self, states: List[List[int]], colors: List[int]): + super().__init__() + self._states = list(states) + self._colors = list(colors) + self._default_color = colors[-1] if colors else 0 + + def getColorForState(self, stateSet: List[int], defaultColor: int = 0) -> int: + return self._default_color + + def getDefaultColor(self) -> int: + return self._default_color + + def isStateful(self) -> bool: + return len(self._states) > 0 + + def withAlpha(self, alpha: int) -> "ColorStateList": + new_colors = [] + for c in self._colors: + a = Color.alpha(c) + new_a = int(a * alpha / 255) + new_colors.append(Color.argb(new_a, Color.red(c), Color.green(c), Color.blue(c))) + return ColorStateList(self._states, new_colors) + + @staticmethod + def valueOf(color: int) -> "ColorStateList": + return ColorStateList([[]], [color]) + + def toString(self) -> str: + return f"ColorStateList({len(self._states)} states)" + + +# ═══════════════════════════════════════════════════════════════ +# Drawable +# ═══════════════════════════════════════════════════════════════ + +class Drawable(Stub): + """Drawable — 可绘制对象的抽象基类""" + + def __init__(self): + super().__init__() + self._bounds = Rect(0, 0, 0, 0) + self._alpha = 255 + self._color_filter = None + self._callback = None + self._visible = True + self._level = 0 + self._intrinsic_width = -1 + self._intrinsic_height = -1 + + def setBounds(self, left: int, top: int, right: int, bottom: int): + self._bounds.set(left, top, right, bottom) + + def setBounds(self, bounds: Rect): + self._bounds.set(bounds.left, bounds.top, bounds.right, bounds.bottom) + + def getBounds(self) -> Rect: + return self._bounds + + def copyBounds(self) -> Rect: + return Rect(self._bounds.left, self._bounds.top, + self._bounds.right, self._bounds.bottom) + + def copyBounds(self, out: Rect) -> Rect: + out.set(self._bounds.left, self._bounds.top, + self._bounds.right, self._bounds.bottom) + return out + + def getIntrinsicWidth(self) -> int: + return self._intrinsic_width + + def getIntrinsicHeight(self) -> int: + return self._intrinsic_height + + def setAlpha(self, alpha: int): + self._alpha = alpha + + def getAlpha(self) -> int: + return self._alpha + + def setColorFilter(self, colorFilter: Optional[ColorFilter]): + self._color_filter = colorFilter + + def getColorFilter(self) -> Optional[ColorFilter]: + return self._color_filter + + def getOpacity(self) -> int: + return 0 # PixelFormat.UNKNOWN + + def invalidateSelf(self): + if self._callback: + self._callback.invalidateDrawable(self) + + def setCallback(self, cb): + self._callback = cb + + def getCallback(self): + return self._callback + + def scheduleDrawable(self, what, who, when: int): + if self._callback: + self._callback.scheduleDrawable(what, who, when) + + def unscheduleDrawable(self, what, who): + if self._callback: + self._callback.unscheduleDrawable(what, who) + + def draw(self, canvas: Canvas): + pass + + def setVisible(self, visible: bool, restart: bool = False) -> bool: + old = self._visible + self._visible = visible + return old + + def isVisible(self) -> bool: + return self._visible + + def setLevel(self, level: int) -> bool: + self._level = level + return True + + def getLevel(self) -> int: + return self._level + + def getConstantState(self) -> Optional["DrawableConstantState"]: + return None + + def getCurrent(self) -> "Drawable": + return self + + def mutate(self) -> "Drawable": + return self + + def getMinimumWidth(self) -> int: + return 0 + + def getMinimumHeight(self) -> int: + return 0 + + def setDither(self, dither: bool): + pass + + def setFilterBitmap(self, filter: bool): + pass + + def setHotspot(self, x: float, y: float): + pass + + def setHotspotBounds(self, left: int, top: int, right: int, bottom: int): + pass + + def getHotspotBounds(self, outRect: Rect): + outRect.set(0, 0, 0, 0) + + def isStateful(self) -> bool: + return False + + def setState(self, stateSet: List[int]) -> bool: + return False + + def getState(self) -> List[int]: + return [] + + def jumpToCurrentState(self): + pass + + def clearColorFilter(self): + self._color_filter = None + + def getTransparentRegion(self): + from .graphics import Region + return Region(self._bounds) + + def onBoundsChange(self, bounds: Rect): + pass + + def onLevelChange(self, level: int) -> bool: + return False + + def onStateChange(self, stateSet: List[int]) -> bool: + return False + + def toString(self) -> str: + return f"{self.__class__.__name__}({self._bounds})" + + +class DrawableConstantState(Stub): + """DrawableConstantState — Drawable 常量状态""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# BitmapDrawable +# ═══════════════════════════════════════════════════════════════ + +class BitmapDrawable(Drawable): + """BitmapDrawable — 位图 Drawable""" + + def __init__(self, res=None, bitmap: Bitmap = None): + super().__init__() + self._bitmap = bitmap if bitmap else Bitmap(0, 0, Bitmap.Config.ARGB_8888) + self._paint = Paint() + self._target_density = 160 + self._gravity = 0x33 # Gravity.FILL + self._tile_mode_x = None + self._tile_mode_y = None + self._mutated = False + if self._bitmap: + self._intrinsic_width = self._bitmap.getWidth() + self._intrinsic_height = self._bitmap.getHeight() + + def getBitmap(self) -> Bitmap: + return self._bitmap + + def getPaint(self) -> Paint: + return self._paint + + def setAntiAlias(self, aa: bool): + self._paint.setAntiAlias(aa) + + def setFilterBitmap(self, filter: bool): + self._paint.setFilterBitmap(filter) + + def setDither(self, dither: bool): + self._paint.setDither(dither) + + def setTileModeX(self, mode): + self._tile_mode_x = mode + + def setTileModeY(self, mode): + self._tile_mode_y = mode + + def getTileModeX(self): + return self._tile_mode_x + + def getTileModeY(self): + return self._tile_mode_y + + def setGravity(self, gravity: int): + self._gravity = gravity + + def getGravity(self) -> int: + return self._gravity + + def setTargetDensity(self, density: int): + self._target_density = density + + def getTargetDensity(self) -> int: + return self._target_density + + def draw(self, canvas: Canvas): + if self._bitmap: + bounds = self._bounds + if HAS_PIL and hasattr(canvas, "_image") and hasattr(self._bitmap, "_image"): + bm_img = self._bitmap._image + if bm_img: + resized = bm_img.resize((bounds.width(), bounds.height())) + canvas._image.paste(resized, (bounds.left, bounds.top)) + + def getIntrinsicWidth(self) -> int: + return self._bitmap.getWidth() if self._bitmap else -1 + + def getIntrinsicHeight(self) -> int: + return self._bitmap.getHeight() if self._bitmap else -1 + + def getOpacity(self) -> int: + if self._bitmap: + return 1 if self._bitmap.hasAlpha() else -1 + return 0 + + def mutate(self) -> "BitmapDrawable": + if not self._mutated: + self._bitmap = Bitmap.createBitmap(self._bitmap) + self._paint = Paint() + self._mutated = True + return self + + +# ═══════════════════════════════════════════════════════════════ +# ColorDrawable +# ═══════════════════════════════════════════════════════════════ + +class ColorDrawable(Drawable): + """ColorDrawable — 纯色 Drawable""" + + def __init__(self, color: int = 0): + super().__init__() + self._color = color + self._paint = Paint() + self._paint.setColor(color) + + def getColor(self) -> int: + return self._color + + def setColor(self, color: int): + self._color = color + self._paint.setColor(color) + + def setAlpha(self, alpha: int): + self._paint.setAlpha(alpha) + + def getAlpha(self) -> int: + return self._paint.getAlpha() + + def draw(self, canvas: Canvas): + canvas.drawRect(self._bounds, self._paint) + + def getOpacity(self) -> int: + if Color.alpha(self._color) == 255: + return -1 # OPAQUE + return -3 # TRANSLUCENT + + def mutate(self) -> "ColorDrawable": + self._paint = Paint() + self._paint.setColor(self._color) + return self + + +# ═══════════════════════════════════════════════════════════════ +# GradientDrawable +# ═══════════════════════════════════════════════════════════════ + +class GradientDrawable(Drawable): + """GradientDrawable — 渐变 Drawable""" + + RECTANGLE = 0 + OVAL = 1 + LINE = 2 + RING = 3 + + class Orientation: + TOP_BOTTOM = 0 + TR_BL = 1 + RIGHT_LEFT = 2 + BR_TL = 3 + BOTTOM_TOP = 4 + BL_TR = 5 + LEFT_RIGHT = 6 + TL_BR = 7 + + def __init__(self, orientation: int = Orientation.TOP_BOTTOM, colors: List[int] = None): + super().__init__() + self._orientation = orientation + self._colors = colors if colors else [Color.BLACK, Color.WHITE] + self._shape = GradientDrawable.RECTANGLE + self._corner_radius = 0.0 + self._corner_radii = None + self._stroke_width = 0 + self._stroke_color = 0 + self._gradient_type = 0 # LINEAR_GRADIENT + self._gradient_center_x = 0.5 + self._gradient_center_y = 0.5 + self._use_level = False + self._width = -1 + self._height = -1 + self._color = None + + def setShape(self, shape: int): + self._shape = shape + + def getShape(self) -> int: + return self._shape + + def setSize(self, width: int, height: int): + self._width = width + self._height = height + + def setStroke(self, width: int, color: int, dashWidth: float = 0, dashGap: float = 0): + self._stroke_width = width + self._stroke_color = color + + def setCornerRadius(self, radius: float): + self._corner_radius = radius + self._corner_radii = None + + def setCornerRadii(self, radii: List[float]): + self._corner_radii = radii + self._corner_radius = radii[0] if radii else 0 + + def setColors(self, colors: List[int]): + self._colors = list(colors) + self._color = None + + def setColor(self, color: int): + self._color = color + self._colors = None + + def setGradientType(self, gradientType: int): + self._gradient_type = gradientType + + def setGradientCenter(self, x: float, y: float): + self._gradient_center_x = x + self._gradient_center_y = y + + def setOrientation(self, orientation: int): + self._orientation = orientation + + def setUseLevel(self, useLevel: bool): + self._use_level = useLevel + + def getIntrinsicWidth(self) -> int: + return self._width if self._width > 0 else super().getIntrinsicWidth() + + def getIntrinsicHeight(self) -> int: + return self._height if self._height > 0 else super().getIntrinsicHeight() + + def draw(self, canvas: Canvas): + bounds = self._bounds + if HAS_PIL and hasattr(canvas, "_draw") and canvas._draw: + if self._color is not None: + c = self._color + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + elif self._colors: + c = self._colors[0] + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + else: + fill_color = (0, 0, 0, 255) + + if self._shape == GradientDrawable.OVAL: + canvas._draw.ellipse([bounds.left, bounds.top, bounds.right, bounds.bottom], + fill=fill_color) + elif self._shape == GradientDrawable.RECTANGLE and self._corner_radius > 0: + canvas._draw.rounded_rectangle( + [bounds.left, bounds.top, bounds.right, bounds.bottom], + radius=self._corner_radius, fill=fill_color) + else: + canvas._draw.rectangle([bounds.left, bounds.top, bounds.right, bounds.bottom], + fill=fill_color) + + +# ═══════════════════════════════════════════════════════════════ +# LayerDrawable +# ═══════════════════════════════════════════════════════════════ + +class LayerDrawable(Drawable): + """LayerDrawable — 图层 Drawable""" + + def __init__(self, layers: List[Drawable]): + super().__init__() + self._layers = list(layers) + self._ids = list(range(len(layers))) + self._layer_insets = [Rect(0, 0, 0, 0) for _ in layers] + + def findDrawableByLayerId(self, id: int) -> Optional[Drawable]: + for i, lid in enumerate(self._ids): + if lid == id: + return self._layers[i] + return None + + def setId(self, index: int, id: int): + if 0 <= index < len(self._ids): + self._ids[index] = id + + def getNumberOfLayers(self) -> int: + return len(self._layers) + + def getDrawable(self, index: int) -> Optional[Drawable]: + if 0 <= index < len(self._layers): + return self._layers[index] + return None + + def setDrawableByLayerId(self, id: int, drawable: Drawable) -> bool: + for i, lid in enumerate(self._ids): + if lid == id: + self._layers[i] = drawable + return True + return False + + def setLayerInset(self, index: int, l: int, t: int, r: int, b: int): + if 0 <= index < len(self._layer_insets): + self._layer_insets[index].set(l, t, r, b) + + def setLayerInsetLeft(self, index: int, left: int): + if 0 <= index < len(self._layer_insets): + self._layer_insets[index].left = left + + def setLayerInsetTop(self, index: int, top: int): + if 0 <= index < len(self._layer_insets): + self._layer_insets[index].top = top + + def setLayerInsetRight(self, index: int, right: int): + if 0 <= index < len(self._layer_insets): + self._layer_insets[index].right = right + + def setLayerInsetBottom(self, index: int, bottom: int): + if 0 <= index < len(self._layer_insets): + self._layer_insets[index].bottom = bottom + + def draw(self, canvas: Canvas): + for i, layer in enumerate(self._layers): + ins = self._layer_insets[i] if i < len(self._layer_insets) else Rect(0, 0, 0, 0) + b = self._bounds + layer.setBounds(b.left + ins.left, b.top + ins.top, + b.right - ins.right, b.bottom - ins.bottom) + layer.draw(canvas) + + def mutate(self) -> "LayerDrawable": + return self + + def getOpacity(self) -> int: + return 0 + + def isStateful(self) -> bool: + return any(d.isStateful() for d in self._layers) + + def setVisible(self, visible: bool, restart: bool = False) -> bool: + old = super().setVisible(visible, restart) + for d in self._layers: + d.setVisible(visible, restart) + return old + + +# ═══════════════════════════════════════════════════════════════ +# StateListDrawable +# ═══════════════════════════════════════════════════════════════ + +class StateListDrawable(Drawable): + """StateListDrawable — 状态列表 Drawable""" + + def __init__(self): + super().__init__() + self._state_sets: List[List[int]] = [] + self._drawables: List[Drawable] = [] + self._current_index = -1 + + def addState(self, stateSet: List[int], drawable: Drawable): + self._state_sets.append(stateSet) + self._drawables.append(drawable) + + def selectDrawable(self, index: int) -> bool: + if 0 <= index < len(self._drawables): + self._current_index = index + return True + return False + + def getCurrent(self) -> Drawable: + if 0 <= self._current_index < len(self._drawables): + return self._drawables[self._current_index] + if self._drawables: + return self._drawables[0] + return self + + def draw(self, canvas: Canvas): + d = self.getCurrent() + d.setBounds(self._bounds) + d.draw(canvas) + + def isStateful(self) -> bool: + return True + + def setState(self, stateSet: List[int]) -> bool: + for i, ss in enumerate(self._state_sets): + if not ss or all(s in stateSet for s in ss): + self._current_index = i + return True + return False + + def mutate(self) -> "StateListDrawable": + self._drawables = [d.mutate() for d in self._drawables] + return self + + +# ═══════════════════════════════════════════════════════════════ +# LevelListDrawable +# ═══════════════════════════════════════════════════════════════ + +class LevelListDrawable(Drawable): + """LevelListDrawable — 级别列表 Drawable""" + + def __init__(self): + super().__init__() + self._levels: List[Tuple[int, int, Drawable]] = [] + self._current_index = -1 + + def addLevel(self, low: int, high: int, drawable: Drawable): + self._levels.append((low, high, drawable)) + + def onLevelChange(self, level: int) -> bool: + for i, (low, high, d) in enumerate(self._levels): + if low <= level <= high: + self._current_index = i + return True + return False + + def getCurrent(self) -> Drawable: + if 0 <= self._current_index < len(self._levels): + return self._levels[self._current_index][2] + if self._levels: + return self._levels[0][2] + return self + + def draw(self, canvas: Canvas): + d = self.getCurrent() + d.setBounds(self._bounds) + d.draw(canvas) + + +# ═══════════════════════════════════════════════════════════════ +# TransitionDrawable +# ═══════════════════════════════════════════════════════════════ + +class TransitionDrawable(Drawable): + """TransitionDrawable — 过渡 Drawable""" + + def __init__(self, layers: List[Drawable]): + super().__init__() + self._layers = list(layers) + self._transition_progress = 0.0 + self._reverse = False + + def startTransition(self, durationMillis: int): + self._transition_progress = 0.0 + self._reverse = False + + def resetTransition(self): + self._transition_progress = 0.0 + self._reverse = False + + def reverse(self, durationMillis: int): + self._transition_progress = 1.0 + self._reverse = True + + def startReverse(self, durationMillis: int = 0): + self._transition_progress = 1.0 + self._reverse = True + + def draw(self, canvas: Canvas): + for layer in self._layers: + layer.setBounds(self._bounds) + layer.draw(canvas) + + def getNumberOfLayers(self) -> int: + return len(self._layers) + + def getDrawable(self, index: int) -> Optional[Drawable]: + if 0 <= index < len(self._layers): + return self._layers[index] + return None + + def isCrossFadeEnabled(self) -> bool: + return True + + def setCrossFadeEnabled(self, enabled: bool): + pass + + +# ═══════════════════════════════════════════════════════════════ +# RotateDrawable +# ═══════════════════════════════════════════════════════════════ + +class RotateDrawable(Drawable): + """RotateDrawable — 旋转 Drawable""" + + def __init__(self): + super().__init__() + self._drawable = None + self._from_degrees = 0.0 + self._to_degrees = 360.0 + self._pivot_x = 0.5 + self._pivot_y = 0.5 + self._current_degrees = 0.0 + + def setDrawable(self, drawable: Drawable): + self._drawable = drawable + + def getDrawable(self) -> Optional[Drawable]: + return self._drawable + + def setFromDegrees(self, fromDegrees: float): + self._from_degrees = fromDegrees + + def setToDegrees(self, toDegrees: float): + self._to_degrees = toDegrees + + def setPivotX(self, pivotX: float): + self._pivot_x = pivotX + + def setPivotY(self, pivotY: float): + self._pivot_y = pivotY + + def draw(self, canvas: Canvas): + if self._drawable: + self._drawable.setBounds(self._bounds) + self._drawable.draw(canvas) + + +# ═══════════════════════════════════════════════════════════════ +# ScaleDrawable +# ═══════════════════════════════════════════════════════════════ + +class ScaleDrawable(Drawable): + """ScaleDrawable — 缩放 Drawable""" + + def __init__(self, drawable: Drawable = None, gravity: int = 0x33, scaleWidth: float = 0.0, + scaleHeight: float = 0.0): + super().__init__() + self._drawable = drawable + self._gravity = gravity + self._scale_width = scaleWidth + self._scale_height = scaleHeight + self._level = 0 + + def getDrawable(self) -> Optional[Drawable]: + return self._drawable + + def setLevel(self, level: int) -> bool: + self._level = level + if self._drawable: + self._drawable.setLevel(level) + return True + + def draw(self, canvas: Canvas): + if self._drawable: + self._drawable.setBounds(self._bounds) + self._drawable.draw(canvas) + + +# ═══════════════════════════════════════════════════════════════ +# ClipDrawable +# ═══════════════════════════════════════════════════════════════ + +class ClipDrawable(Drawable): + """ClipDrawable — 裁剪 Drawable""" + + HORIZONTAL = 1 + VERTICAL = 2 + + def __init__(self, drawable: Drawable = None, gravity: int = 0x33, orientation: int = HORIZONTAL): + super().__init__() + self._drawable = drawable + self._gravity = gravity + self._orientation = orientation + self._level = 0 + + def setLevel(self, level: int) -> bool: + self._level = level + if self._drawable: + self._drawable.setLevel(level) + return True + + def draw(self, canvas: Canvas): + if self._drawable: + self._drawable.setBounds(self._bounds) + self._drawable.draw(canvas) + + +# ═══════════════════════════════════════════════════════════════ +# InsetDrawable +# ═══════════════════════════════════════════════════════════════ + +class InsetDrawable(Drawable): + """InsetDrawable — 内嵌 Drawable""" + + def __init__(self, drawable: Drawable, inset: float): + super().__init__() + self._drawable = drawable + self._inset = int(inset) + self._inset_left = int(inset) + self._inset_top = int(inset) + self._inset_right = int(inset) + self._inset_bottom = int(inset) + + def getDrawable(self) -> Optional[Drawable]: + return self._drawable + + def getInset(self) -> int: + return self._inset + + def draw(self, canvas: Canvas): + if self._drawable: + b = self._bounds + self._drawable.setBounds( + b.left + self._inset_left, b.top + self._inset_top, + b.right - self._inset_right, b.bottom - self._inset_bottom + ) + self._drawable.draw(canvas) + + +# ═══════════════════════════════════════════════════════════════ +# ShapeDrawable +# ═══════════════════════════════════════════════════════════════ + +class ShapeDrawable(Drawable): + """ShapeDrawable — 形状 Drawable""" + + def __init__(self, shape: "Shape" = None): + super().__init__() + self._shape = shape + self._paint = Paint() + self._intrinsic_width = -1 + self._intrinsic_height = -1 + + def getShape(self): + return self._shape + + def setShape(self, shape): + self._shape = shape + + def getPaint(self) -> Paint: + return self._paint + + def setIntrinsicWidth(self, width: int): + self._intrinsic_width = width + + def setIntrinsicHeight(self, height: int): + self._intrinsic_height = height + + def getIntrinsicWidth(self) -> int: + return self._intrinsic_width if self._intrinsic_width > 0 else super().getIntrinsicWidth() + + def getIntrinsicHeight(self) -> int: + return self._intrinsic_height if self._intrinsic_height > 0 else super().getIntrinsicHeight() + + def draw(self, canvas: Canvas): + if self._shape: + self._shape.draw(canvas, self._paint) + + +class Shape(Stub): + """Shape — 形状基类""" + def draw(self, canvas: Canvas, paint: Paint): + pass + + +class ArcShape(Shape): + """ArcShape — 弧形""" + def __init__(self, startAngle: float, sweepAngle: float): + super().__init__() + self._start_angle = startAngle + self._sweep_angle = sweepAngle + + +class OvalShape(Shape): + """OvalShape — 椭圆""" + def draw(self, canvas: Canvas, paint: Paint): + if HAS_PIL and hasattr(canvas, "_draw"): + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + b = canvas.getClipBounds() + canvas._draw.ellipse([b.left, b.top, b.right, b.bottom], fill=fill_color) + + +class RectShape(Shape): + """RectShape — 矩形""" + def draw(self, canvas: Canvas, paint: Paint): + if HAS_PIL and hasattr(canvas, "_draw"): + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + b = canvas.getClipBounds() + canvas._draw.rectangle([b.left, b.top, b.right, b.bottom], fill=fill_color) + + +class RoundRectShape(Shape): + """RoundRectShape — 圆角矩形""" + def __init__(self, outerRadii: List[float], inset: RectF, innerRadii: List[float]): + super().__init__() + self._outer_radii = outerRadii + self._inset = inset + self._inner_radii = innerRadii + + def draw(self, canvas: Canvas, paint: Paint): + if HAS_PIL and hasattr(canvas, "_draw"): + c = paint.getColor() + fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)) + b = canvas.getClipBounds() + radius = self._outer_radii[0] if self._outer_radii else 0 + canvas._draw.rounded_rectangle([b.left, b.top, b.right, b.bottom], + radius=radius, fill=fill_color) + + +class PaintDrawable(ShapeDrawable): + """PaintDrawable — 画笔 Drawable (ShapeDrawable 子类)""" + + def __init__(self, color: int = 0): + super().__init__(RectShape()) + self.getPaint().setColor(color) + + def setCornerRadius(self, radius: float): + self.setShape(RoundRectShape([radius, radius, radius, radius], None, None)) + + +# ═══════════════════════════════════════════════════════════════ +# VectorDrawable +# ═══════════════════════════════════════════════════════════════ + +class VectorDrawable(Drawable): + """VectorDrawable — 矢量 Drawable""" + + def __init__(self): + super().__init__() + self._alpha = 255 + + def setAlpha(self, alpha: int): + self._alpha = alpha + + def getAlpha(self) -> int: + return self._alpha + + def draw(self, canvas: Canvas): + pass + + @staticmethod + def createFromXml(res, parser) -> "VectorDrawable": + return VectorDrawable() + + +# ═══════════════════════════════════════════════════════════════ +# RippleDrawable +# ═══════════════════════════════════════════════════════════════ + +class RippleDrawable(Drawable): + """RippleDrawable — 水波纹 Drawable (API 21+)""" + + def __init__(self, colorStates: ColorStateList, content: Drawable = None, + mask: Drawable = None): + super().__init__() + self._color_states = colorStates + self._content = content + self._mask = mask + self._radius = -1 + + def setRadius(self, radius: int): + self._radius = radius + + def getRadius(self) -> int: + return self._radius + + def draw(self, canvas: Canvas): + if self._content: + self._content.setBounds(self._bounds) + self._content.draw(canvas) + + def setColor(self, color: ColorStateList): + self._color_states = color + + def getColor(self) -> Optional[ColorStateList]: + return self._color_states + + def mutate(self) -> "RippleDrawable": + return self + + +# ═══════════════════════════════════════════════════════════════ +# AnimatedVectorDrawable +# ═══════════════════════════════════════════════════════════════ + +class AnimatedVectorDrawable(Drawable): + """AnimatedVectorDrawable — 动画矢量 Drawable (API 21+)""" + + def __init__(self): + super().__init__() + self._running = False + + def start(self): + self._running = True + + def stop(self): + self._running = False + + def isRunning(self) -> bool: + return self._running + + def reset(self): + self._running = False + + def draw(self, canvas: Canvas): + pass + + @staticmethod + def createFromXml(res, parser) -> "AnimatedVectorDrawable": + return AnimatedVectorDrawable() + + +# ═══════════════════════════════════════════════════════════════ +# AnimationDrawable +# ═══════════════════════════════════════════════════════════════ + +class AnimationDrawable(Drawable): + """AnimationDrawable — 帧动画 Drawable""" + + def __init__(self): + super().__init__() + self._frames: List[Tuple[Drawable, int]] = [] + self._current_frame = 0 + self._oneshot = False + self._running = False + + def addFrame(self, frame: Drawable, duration: int): + self._frames.append((frame, duration)) + + def getFrame(self, index: int) -> Optional[Drawable]: + if 0 <= index < len(self._frames): + return self._frames[index][0] + return None + + def getNumberOfFrames(self) -> int: + return len(self._frames) + + def getDuration(self, index: int) -> int: + if 0 <= index < len(self._frames): + return self._frames[index][1] + return 0 + + def setOneShot(self, oneshot: bool): + self._oneshot = oneshot + + def isOneShot(self) -> bool: + return self._oneshot + + def start(self): + self._running = True + + def stop(self): + self._running = False + + def isRunning(self) -> bool: + return self._running + + def run(self): + pass + + def selectDrawable(self, index: int) -> bool: + if 0 <= index < len(self._frames): + self._current_frame = index + return True + return False + + def getCurrent(self) -> Drawable: + if self._frames: + return self._frames[self._current_frame][0] + return self + + def draw(self, canvas: Canvas): + d = self.getCurrent() + d.setBounds(self._bounds) + d.draw(canvas) + + def setVisible(self, visible: bool, restart: bool = False) -> bool: + old = super().setVisible(visible, restart) + if visible and restart: + self._current_frame = 0 + return old + + +# ═══════════════════════════════════════════════════════════════ +# DrawableContainer +# ═══════════════════════════════════════════════════════════════ + +class DrawableContainer(Drawable): + """DrawableContainer — Drawable 容器""" + + def __init__(self): + super().__init__() + self._drawables: List[Drawable] = [] + self._current_index = 0 + + def selectDrawable(self, index: int) -> bool: + if 0 <= index < len(self._drawables): + self._current_index = index + return True + return False + + def getCurrent(self) -> Drawable: + if self._drawables: + return self._drawables[self._current_index] + return self + + def draw(self, canvas: Canvas): + d = self.getCurrent() + d.setBounds(self._bounds) + d.draw(canvas) + + def setAlpha(self, alpha: int): + for d in self._drawables: + d.setAlpha(alpha) + + def setColorFilter(self, cf: Optional[ColorFilter]): + for d in self._drawables: + d.setColorFilter(cf) + + def getOpacity(self) -> int: + return 0 + + def isStateful(self) -> bool: + return any(d.isStateful() for d in self._drawables) + + def mutate(self) -> "DrawableContainer": + return self + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Drawable", "BitmapDrawable", "ColorDrawable", "GradientDrawable", + "LayerDrawable", "StateListDrawable", "LevelListDrawable", + "TransitionDrawable", "RotateDrawable", "ScaleDrawable", + "ClipDrawable", "InsetDrawable", "ShapeDrawable", "PaintDrawable", + "VectorDrawable", "RippleDrawable", "AnimatedVectorDrawable", + "AnimationDrawable", "DrawableContainer", "DrawableConstantState", + "ColorStateList", + # Shapes + "Shape", "ArcShape", "OvalShape", "RectShape", "RoundRectShape", +] \ No newline at end of file diff --git a/android_pyapi/android/graphics_pdf.py b/android_pyapi/android/graphics_pdf.py new file mode 100644 index 0000000..ea06db5 --- /dev/null +++ b/android_pyapi/android/graphics_pdf.py @@ -0,0 +1,335 @@ +""" +Android Graphics PDF 组件 — PdfDocument, PdfRenderer, ParcelFileDescriptor +方法签名完全对齐 Android SDK +""" + +import os +import io +import tempfile +from typing import Any, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, HAS_PDF2IMAGE, resolve_android_path + + +# ═══════════════════════════════════════════════════════════════ +# ParcelFileDescriptor +# ═══════════════════════════════════════════════════════════════ + +class ParcelFileDescriptor(Stub): + """ParcelFileDescriptor — 文件描述符""" + + MODE_READ_ONLY = 0x10000000 + MODE_WRITE_ONLY = 0x20000000 + MODE_READ_WRITE = 0x30000000 + MODE_CREATE = 0x08000000 + MODE_TRUNCATE = 0x04000000 + MODE_APPEND = 0x02000000 + + def __init__(self, fd: int = -1, file_path: str = ""): + super().__init__() + self._fd = fd + self._file_path = file_path + self._closed = False + self._file_obj = None + if file_path and fd < 0: + resolved = resolve_android_path(file_path) + self._file_path = resolved + + @staticmethod + def open(file_path: str, mode: int) -> "ParcelFileDescriptor": + resolved = resolve_android_path(file_path) + fmode = "rb" + if mode & ParcelFileDescriptor.MODE_WRITE_ONLY: + fmode = "wb" + elif mode & ParcelFileDescriptor.MODE_READ_WRITE: + fmode = "r+b" + if mode & ParcelFileDescriptor.MODE_CREATE: + if not os.path.exists(os.path.dirname(resolved)): + os.makedirs(os.path.dirname(resolved), exist_ok=True) + if not os.path.exists(resolved): + open(resolved, "w").close() + return ParcelFileDescriptor(-1, file_path) + + @staticmethod + def parseFromFd(fd: int) -> "ParcelFileDescriptor": + return ParcelFileDescriptor(fd) + + @staticmethod + def parseFromString(path: str) -> "ParcelFileDescriptor": + resolved = resolve_android_path(path) + return ParcelFileDescriptor(-1, resolved) + + @staticmethod + def adoptFd(fd: int) -> "ParcelFileDescriptor": + return ParcelFileDescriptor(fd) + + @staticmethod + def createPipe() -> List["ParcelFileDescriptor"]: + r, w = os.pipe() + return [ParcelFileDescriptor(r), ParcelFileDescriptor(w)] + + def getFileDescriptor(self) -> int: + return self._fd + + def getStatSize(self) -> int: + if self._file_path and os.path.exists(self._file_path): + return os.path.getsize(self._file_path) + return 0 + + def getFd(self) -> int: + return self._fd + + def close(self): + if not self._closed: + self._closed = True + if self._file_obj: + self._file_obj.close() + elif self._fd >= 0: + try: + os.close(self._fd) + except OSError: + pass + + def getFilePath(self) -> str: + return self._file_path + + def toString(self) -> str: + return f"ParcelFileDescriptor(fd={self._fd})" + + +# ═══════════════════════════════════════════════════════════════ +# PdfDocument +# ═══════════════════════════════════════════════════════════════ + +class PdfDocument(Stub): + """PdfDocument — PDF 文档""" + + class Page(Stub): + """PdfDocument.Page — PDF 页面""" + + def __init__(self, page_number: int, width: int, height: int): + super().__init__() + self._page_number = page_number + self._width = width + self._height = height + self._canvas = None + + def getCanvas(self) -> "Canvas": + from .graphics import Canvas + if self._canvas is None: + self._canvas = Canvas(self._width, self._height) + return self._canvas + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def start(self): + pass + + def finish(self): + pass + + def __init__(self): + super().__init__() + self._pages: List[PdfDocument.Page] = [] + + def startPage(self, page_size: "Page") -> "Canvas": + width = page_size.getWidth() if hasattr(page_size, "getWidth") else 595 + height = page_size.getHeight() if hasattr(page_size, "getHeight") else 842 + page = PdfDocument.Page(len(self._pages), width, height) + self._pages.append(page) + return page.getCanvas() + + def finishPage(self, page: "Page"): + page.finish() + + def getPages(self) -> List["Page"]: + return list(self._pages) + + def writeTo(self, out_stream, close: bool = True): + if HAS_PYMUPDF: + import fitz + doc = fitz.open() + for p in self._pages: + page = doc.new_page(width=p._width, height=p._height) + if HAS_PIL and hasattr(p._canvas, "_image") and p._canvas._image: + img_bytes = io.BytesIO() + p._canvas._image.save(img_bytes, format="PNG") + img_bytes.seek(0) + rect = page.rect + page.insert_image(rect, stream=img_bytes.read()) + doc.save(out_stream) + doc.close() + elif HAS_PIL: + from PIL import Image + images = [] + for p in self._pages: + if hasattr(p._canvas, "_image") and p._canvas._image: + images.append(p._canvas._image.convert("RGB")) + if images: + images[0].save(out_stream, format="PDF", save_all=True, + append_images=images[1:]) + else: + blank = Image.new("RGB", (595, 842), (255, 255, 255)) + blank.save(out_stream, format="PDF") + else: + if close: + out_stream.close() + + def close(self): + self._pages.clear() + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +# ═══════════════════════════════════════════════════════════════ +# PdfRenderer +# ═══════════════════════════════════════════════════════════════ + +class PdfRenderer(Stub): + """PdfRenderer — PDF 渲染器""" + + RENDER_MODE_FOR_DISPLAY = 1 + RENDER_MODE_FOR_PRINT = 2 + RENDER_MODE_FOR_ALL = 3 + + class Page(Stub): + """PdfRenderer.Page — PDF 渲染页面""" + + def __init__(self, renderer: "PdfRenderer", index: int): + super().__init__() + self._renderer = renderer + self._index = index + self._opened = True + if renderer._doc: + self._fitz_page = renderer._doc[index] + rect = self._fitz_page.rect + self._width = int(rect.width) + self._height = int(rect.height) + else: + self._fitz_page = None + self._width = 595 + self._height = 842 + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def getIndex(self) -> int: + return self._index + + def render(self, canvas: "Canvas", destLeft: int, destTop: int, + destRight: int, destBottom: int, flags: int = 0): + if not self._opened: + return + if HAS_PYMUPDF and self._fitz_page: + from .graphics import Bitmap + mat = fitz.Matrix(2, 2) # 2x zoom for quality + pix = self._fitz_page.get_pixmap(matrix=mat) + img_data = pix.tobytes("png") + if HAS_PIL and hasattr(canvas, "_image") and canvas._image: + from PIL import Image + import io as _io + img = Image.open(io.BytesIO(img_data)) + img = img.resize((destRight - destLeft, destBottom - destTop)) + canvas._image.paste(img, (destLeft, destTop)) + elif HAS_PDF2IMAGE and HAS_PIL and hasattr(canvas, "_image"): + from pdf2image import convert_from_bytes + if self._renderer._pdf_bytes: + images = convert_from_bytes( + self._renderer._pdf_bytes, + first_page=self._index + 1, + last_page=self._index + 1 + ) + if images: + img = images[0].resize((destRight - destLeft, destBottom - destTop)) + canvas._image.paste(img.convert("RGBA"), (destLeft, destTop)) + + def start(self): + self._opened = True + + def finish(self): + self._opened = False + + def close(self): + self._opened = False + + def __init__(self, pfd: ParcelFileDescriptor, closePfd: bool = True): + super().__init__() + self._pfd = pfd + self._close_pfd = closePfd + self._page_count = 0 + self._pdf_bytes = None + self._doc = None + + file_path = pfd.getFilePath() + if file_path and os.path.exists(file_path): + with open(file_path, "rb") as f: + self._pdf_bytes = f.read() + elif pfd._fd >= 0: + try: + self._pdf_bytes = os.read(pfd._fd, 10 * 1024 * 1024) + except OSError: + self._pdf_bytes = None + + if HAS_PYMUPDF and self._pdf_bytes: + import fitz + self._doc = fitz.open(stream=self._pdf_bytes, filetype="pdf") + self._page_count = len(self._doc) + elif self._pdf_bytes: + self._page_count = 1 # fallback + else: + self._page_count = 0 + + def openPage(self, index: int) -> "Page": + if 0 <= index < self._page_count: + return PdfRenderer.Page(self, index) + raise IndexError(f"Page index {index} out of range (0-{self._page_count - 1})") + + def close(self): + if self._doc and HAS_PYMUPDF: + self._doc.close() + self._doc = None + if self._close_pfd and self._pfd: + self._pfd.close() + self._pdf_bytes = None + + def getPageCount(self) -> int: + return self._page_count + + def shouldScale(self) -> bool: + return False + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +# ═══════════════════════════════════════════════════════════════ +# PdfDocumentPage (alias for PdfDocument.Page) +# ═══════════════════════════════════════════════════════════════ + +PdfDocumentPage = PdfDocument.Page + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "PdfDocument", + "PdfDocumentPage", + "PdfRenderer", + "ParcelFileDescriptor", +] \ No newline at end of file diff --git a/android_pyapi/android/hardware.py b/android_pyapi/android/hardware.py new file mode 100644 index 0000000..b855a1a --- /dev/null +++ b/android_pyapi/android/hardware.py @@ -0,0 +1,533 @@ +""" +Android Hardware 组件 — Sensor, SensorManager, SensorEvent, 等 +方法签名完全对齐 Android SDK +""" + +import math +import time +import random +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL + + +# ═══════════════════════════════════════════════════════════════ +# Sensor +# ═══════════════════════════════════════════════════════════════ + +class Sensor(Stub): + """Sensor — 传感器""" + + TYPE_ACCELEROMETER = 1 + TYPE_MAGNETIC_FIELD = 2 + TYPE_ORIENTATION = 3 + TYPE_GYROSCOPE = 4 + TYPE_LIGHT = 5 + TYPE_PRESSURE = 6 + TYPE_PROXIMITY = 8 + TYPE_GRAVITY = 9 + TYPE_LINEAR_ACCELERATION = 10 + TYPE_ROTATION_VECTOR = 11 + TYPE_RELATIVE_HUMIDITY = 12 + TYPE_AMBIENT_TEMPERATURE = 13 + TYPE_STEP_COUNTER = 19 + TYPE_STEP_DETECTOR = 18 + TYPE_HEART_RATE = 21 + TYPE_LOW_LATENCY_OFFBODY_DETECT = 34 + TYPE_ACCELEROMETER_LIMITED_AXES = 35 + TYPE_DEVICE_ORIENTATION = 27 + TYPE_STATIONARY_DETECT = 28 + + REPORTING_MODE_CONTINUOUS = 0 + REPORTING_MODE_ON_CHANGE = 1 + REPORTING_MODE_ONE_SHOT = 2 + REPORTING_MODE_SPECIAL_TRIGGER = 3 + + def __init__(self, type_: int, name: str = "Simulated Sensor", + vendor: str = "Android-pyApi", version: int = 1, + resolution: float = 1.0, max_range: float = 100.0, + power: float = 0.5, min_delay: int = 10000): + super().__init__() + self._type = type_ + self._name = name + self._vendor = vendor + self._version = version + self._resolution = resolution + self._max_range = max_range + self._power = power + self._min_delay = min_delay + + # Assign default values based on sensor type + type_defaults = { + Sensor.TYPE_ACCELEROMETER: ("Accelerometer", 0.01, 39.24, 3.0, 10000), + Sensor.TYPE_GYROSCOPE: ("Gyroscope", 0.001, 34.91, 6.1, 10000), + Sensor.TYPE_MAGNETIC_FIELD: ("Magnetic Field", 0.01, 2000.0, 4.0, 10000), + Sensor.TYPE_LIGHT: ("Light", 1.0, 40000.0, 0.5, 10000), + Sensor.TYPE_PROXIMITY: ("Proximity", 1.0, 10.0, 0.5, 10000), + Sensor.TYPE_PRESSURE: ("Pressure", 0.01, 1100.0, 1.0, 10000), + Sensor.TYPE_GRAVITY: ("Gravity", 0.01, 39.24, 3.0, 10000), + Sensor.TYPE_LINEAR_ACCELERATION: ("Linear Acceleration", 0.01, 39.24, 3.0, 10000), + Sensor.TYPE_ROTATION_VECTOR: ("Rotation Vector", 0.0001, 1.0, 5.0, 10000), + Sensor.TYPE_RELATIVE_HUMIDITY: ("Relative Humidity", 0.1, 100.0, 0.5, 10000), + Sensor.TYPE_AMBIENT_TEMPERATURE: ("Ambient Temperature", 0.01, 80.0, 0.5, 10000), + Sensor.TYPE_STEP_COUNTER: ("Step Counter", 1.0, 100000.0, 0.1, 100000), + Sensor.TYPE_STEP_DETECTOR: ("Step Detector", 1.0, 1.0, 0.1, 100000), + Sensor.TYPE_HEART_RATE: ("Heart Rate", 1.0, 220.0, 0.5, 100000), + } + + if type_ in type_defaults: + n, res, mr, p, md = type_defaults[type_] + self._name = name if name != "Simulated Sensor" else n + self._resolution = resolution if resolution != 1.0 else res + self._max_range = max_range if max_range != 100.0 else mr + self._power = power if power != 0.5 else p + self._min_delay = min_delay if min_delay != 10000 else md + + def getType(self) -> int: + return self._type + + def getName(self) -> str: + return self._name + + def getVendor(self) -> str: + return self._vendor + + def getVersion(self) -> int: + return self._version + + def getMaximumRange(self) -> float: + return self._max_range + + def getMinDelay(self) -> int: + return self._min_delay + + def getPower(self) -> float: + return self._power + + def getResolution(self) -> float: + return self._resolution + + def getFifoMaxEventCount(self) -> int: + return 0 + + def getFifoReservedEventCount(self) -> int: + return 0 + + def getReportingMode(self) -> int: + return Sensor.REPORTING_MODE_CONTINUOUS + + def isWakeUpSensor(self) -> bool: + return False + + def getStringType(self) -> str: + type_names = { + Sensor.TYPE_ACCELEROMETER: "android.sensor.accelerometer", + Sensor.TYPE_MAGNETIC_FIELD: "android.sensor.magnetic_field", + Sensor.TYPE_GYROSCOPE: "android.sensor.gyroscope", + Sensor.TYPE_LIGHT: "android.sensor.light", + Sensor.TYPE_PRESSURE: "android.sensor.pressure", + Sensor.TYPE_PROXIMITY: "android.sensor.proximity", + Sensor.TYPE_GRAVITY: "android.sensor.gravity", + Sensor.TYPE_LINEAR_ACCELERATION: "android.sensor.linear_acceleration", + Sensor.TYPE_ROTATION_VECTOR: "android.sensor.rotation_vector", + Sensor.TYPE_RELATIVE_HUMIDITY: "android.sensor.relative_humidity", + Sensor.TYPE_AMBIENT_TEMPERATURE: "android.sensor.ambient_temperature", + Sensor.TYPE_STEP_COUNTER: "android.sensor.step_counter", + Sensor.TYPE_STEP_DETECTOR: "android.sensor.step_detector", + Sensor.TYPE_HEART_RATE: "android.sensor.heart_rate", + } + return type_names.get(self._type, "android.sensor.unknown") + + def toString(self) -> str: + return f"{self._name} (type={self._type}, vendor={self._vendor}, ver={self._version})" + + +# ═══════════════════════════════════════════════════════════════ +# SensorEvent +# ═══════════════════════════════════════════════════════════════ + +class SensorEvent(Stub): + """SensorEvent — 传感器事件""" + + def __init__(self, sensor: Sensor, accuracy: int = 3): + super().__init__() + self.sensor = sensor + self.accuracy = accuracy + self.timestamp = int(time.time() * 1_000_000_000) # nanoseconds + self.values: List[float] = [] + + # Generate appropriate values based on sensor type + if sensor.getType() == Sensor.TYPE_ACCELEROMETER: + self.values = [random.uniform(-1, 1), random.uniform(-1, 1), + 9.8 + random.uniform(-0.5, 0.5)] + elif sensor.getType() == Sensor.TYPE_GYROSCOPE: + self.values = [random.uniform(-0.1, 0.1)] * 3 + elif sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD: + self.values = [random.uniform(-50, 50)] * 3 + elif sensor.getType() == Sensor.TYPE_LIGHT: + self.values = [random.uniform(100, 1000)] + elif sensor.getType() == Sensor.TYPE_PROXIMITY: + self.values = [5.0 if random.random() > 0.5 else 0.0] + elif sensor.getType() == Sensor.TYPE_PRESSURE: + self.values = [random.uniform(980, 1050)] + elif sensor.getType() in (Sensor.TYPE_GRAVITY, Sensor.TYPE_LINEAR_ACCELERATION): + self.values = [random.uniform(-1, 1)] * 3 + elif sensor.getType() == Sensor.TYPE_ROTATION_VECTOR: + self.values = [random.uniform(-1, 1)] * 5 + elif sensor.getType() == Sensor.TYPE_RELATIVE_HUMIDITY: + self.values = [random.uniform(30, 70)] + elif sensor.getType() == Sensor.TYPE_AMBIENT_TEMPERATURE: + self.values = [random.uniform(15, 35)] + elif sensor.getType() == Sensor.TYPE_STEP_COUNTER: + self.values = [float(random.randint(0, 10000))] + elif sensor.getType() == Sensor.TYPE_STEP_DETECTOR: + self.values = [1.0 if random.random() > 0.95 else 0.0] + elif sensor.getType() == Sensor.TYPE_HEART_RATE: + self.values = [random.uniform(60, 100)] + else: + self.values = [0.0] + + +# ═══════════════════════════════════════════════════════════════ +# SensorEventListener +# ═══════════════════════════════════════════════════════════════ + +class SensorEventListener(Stub): + """SensorEventListener — 传感器事件监听器""" + + def onSensorChanged(self, event: SensorEvent): + pass + + def onAccuracyChanged(self, sensor: Sensor, accuracy: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# SensorDelay +# ═══════════════════════════════════════════════════════════════ + +class SensorDelay(Stub): + """SensorDelay — 传感器采样延迟常量""" + FASTEST = 0 + GAME = 1 + UI = 2 + NORMAL = 3 + + +# ═══════════════════════════════════════════════════════════════ +# SensorManager +# ═══════════════════════════════════════════════════════════════ + +class SensorManager(Stub): + """SensorManager — 传感器管理器""" + + SENSOR_DELAY_FASTEST = 0 + SENSOR_DELAY_GAME = 1 + SENSOR_DELAY_UI = 2 + SENSOR_DELAY_NORMAL = 3 + + AXIS_X = 1 + AXIS_Y = 2 + AXIS_Z = 3 + AXIS_MINUS_X = 129 + AXIS_MINUS_Y = 130 + AXIS_MINUS_Z = 131 + + def __init__(self): + super().__init__() + self._sensors: Dict[int, Sensor] = {} + self._listeners: Dict[int, List[Tuple[SensorEventListener, Sensor, int]]] = {} + + # Create default sensors + for type_ in [ + Sensor.TYPE_ACCELEROMETER, Sensor.TYPE_GYROSCOPE, + Sensor.TYPE_MAGNETIC_FIELD, Sensor.TYPE_LIGHT, + Sensor.TYPE_PROXIMITY, Sensor.TYPE_PRESSURE, + Sensor.TYPE_GRAVITY, Sensor.TYPE_LINEAR_ACCELERATION, + Sensor.TYPE_ROTATION_VECTOR, Sensor.TYPE_RELATIVE_HUMIDITY, + Sensor.TYPE_AMBIENT_TEMPERATURE, Sensor.TYPE_STEP_COUNTER, + Sensor.TYPE_STEP_DETECTOR, Sensor.TYPE_HEART_RATE, + ]: + self._sensors[type_] = Sensor(type_) + + def getDefaultSensor(self, type_: int) -> Optional[Sensor]: + return self._sensors.get(type_) + + def getSensorList(self, type_: int) -> List[Sensor]: + if type_ == -1: # ALL + return list(self._sensors.values()) + sensor = self._sensors.get(type_) + return [sensor] if sensor else [] + + def getSensors(self) -> List[Sensor]: + return list(self._sensors.values()) + + def registerListener(self, listener: SensorEventListener, sensor: Sensor, + samplingPeriodUs: int, maxReportLatencyUs: int = 0) -> bool: + if sensor.getType() not in self._sensors: + return False + if listener not in self._listeners: + self._listeners[id(listener)] = [] + self._listeners[id(listener)].append((listener, sensor, samplingPeriodUs)) + return True + + def unregisterListener(self, listener: SensorEventListener, sensor: Sensor = None) -> bool: + lid = id(listener) + if lid in self._listeners: + if sensor: + self._listeners[lid] = [ + (l, s, d) for l, s, d in self._listeners[lid] + if s.getType() != sensor.getType() + ] + if not self._listeners[lid]: + del self._listeners[lid] + else: + del self._listeners[lid] + return True + return False + + @staticmethod + def getOrientation(R: List[float], values: List[float]) -> List[float]: + if len(R) >= 9 and len(values) >= 3: + values[0] = math.degrees(math.atan2(R[1], R[4])) # azimuth + values[1] = math.degrees(math.asin(-R[7])) # pitch + values[2] = math.degrees(math.atan2(-R[6], R[8])) # roll + return values + + @staticmethod + def getRotationMatrix(R: List[float], I: List[float], + gravity: List[float], geomagnetic: List[float]) -> bool: + if len(R) < 9: + R.extend([0] * (9 - len(R))) + if len(R) >= 9: + R[0] = 1.0 + R[1] = 0.0 + R[2] = 0.0 + R[3] = 0.0 + R[4] = 1.0 + R[5] = 0.0 + R[6] = 0.0 + R[7] = 0.0 + R[8] = 1.0 + return True + + @staticmethod + def getAltitude(p0: float, p: float) -> float: + return 44330.0 * (1.0 - math.pow(p / p0, 1.0 / 5.255)) + + @staticmethod + def getInclination(I: List[float]) -> float: + return 0.0 + + @staticmethod + def remapCoordinateSystem(inR: List[float], X: int, Y: int, + outR: List[float]) -> bool: + if len(inR) >= 9 and len(outR) >= 9: + for i in range(9): + outR[i] = inR[i] + return True + + def getDeviceId(self) -> str: + return "sensor_hub_0" + + +# ═══════════════════════════════════════════════════════════════ +# HardwareBuffer +# ═══════════════════════════════════════════════════════════════ + +class HardwareBuffer(Stub): + """HardwareBuffer — 硬件缓冲 (API 26+)""" + + USAGE_CPU_READ_OFTEN = 0x00000003 + USAGE_CPU_WRITE_OFTEN = 0x00000030 + USAGE_GPU_SAMPLED_IMAGE = 0x00000100 + USAGE_GPU_COLOR_OUTPUT = 0x00000200 + USAGE_GPU_DATA_BUFFER = 0x00001000 + USAGE_SENSOR_DIRECT_DATA = 0x00400000 + USAGE_PROTECTED_CONTENT = 0x00004000 + USAGE_VIDEO_ENCODE = 0x00010000 + USAGE_GPU_FRAMEBUFFER = 0x00020000 + USAGE_COMPOSER_OVERLAY = 0x00000800 + USAGE_FRONT_BUFFER_QUEUE = 0x00008000 + + BLOB = 0x21 + RGBA_8888 = 1 + RGBA_FP16 = 22 + RGBA_1010102 = 43 + RGBX_8888 = 2 + RGB_888 = 3 + RGB_565 = 4 + YCBCR_420_888 = 35 + + def __init__(self, width: int, height: int, format: int, layers: int, + usage: int, buffer_size: int = 0): + super().__init__() + self._width = width + self._height = height + self._format = format + self._layers = layers + self._usage = usage + self._buffer_size = buffer_size + self._closed = False + + @property + def width(self) -> int: + return self._width + + @property + def height(self) -> int: + return self._height + + @property + def format(self) -> int: + return self._format + + @property + def layers(self) -> int: + return self._layers + + @property + def usage(self) -> int: + return self._usage + + def destroy(self): + self.close() + + def close(self): + self._closed = True + + def isClosed(self) -> bool: + return self._closed + + def isEmpty(self) -> bool: + return self._width == 0 or self._height == 0 + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def getFormat(self) -> int: + return self._format + + def getLayers(self) -> int: + return self._layers + + def getUsage(self) -> int: + return self._usage + + def getStride(self) -> int: + return self._width + + +# ═══════════════════════════════════════════════════════════════ +# BatteryManager (Constants) +# ═══════════════════════════════════════════════════════════════ + +class BatteryManager(Stub): + """BatteryManager — 电池管理器常量 (实际实现在 android.os)""" + + BATTERY_PROPERTY_CHARGE_COUNTER = 1 + BATTERY_PROPERTY_CURRENT_NOW = 2 + BATTERY_PROPERTY_CURRENT_AVERAGE = 3 + BATTERY_PROPERTY_CAPACITY = 4 + BATTERY_PROPERTY_STATUS = 5 + BATTERY_PROPERTY_ENERGY_COUNTER = 6 + + BATTERY_STATUS_UNKNOWN = 1 + BATTERY_STATUS_CHARGING = 2 + BATTERY_STATUS_DISCHARGING = 3 + BATTERY_STATUS_NOT_CHARGING = 4 + BATTERY_STATUS_FULL = 5 + + BATTERY_HEALTH_UNKNOWN = 1 + BATTERY_HEALTH_GOOD = 2 + BATTERY_HEALTH_OVERHEAT = 3 + BATTERY_HEALTH_DEAD = 4 + BATTERY_HEALTH_OVER_VOLTAGE = 5 + BATTERY_HEALTH_UNSPECIFIED_FAILURE = 6 + BATTERY_HEALTH_COLD = 7 + + BATTERY_PLUGGED_AC = 1 + BATTERY_PLUGGED_USB = 2 + BATTERY_PLUGGED_WIRELESS = 4 + + ACTION_CHARGING = "android.intent.action.ACTION_CHARGING" + ACTION_DISCHARGING = "android.intent.action.ACTION_DISCHARGING" + EXTRA_STATUS = "status" + EXTRA_HEALTH = "health" + EXTRA_LEVEL = "level" + EXTRA_SCALE = "scale" + EXTRA_PLUGGED = "plugged" + EXTRA_TEMPERATURE = "temperature" + EXTRA_VOLTAGE = "voltage" + EXTRA_TECHNOLOGY = "technology" + EXTRA_CHARGE_COUNTER = "charge_counter" + EXTRA_PRESENT = "present" + + def __init__(self): + super().__init__() + + def getIntProperty(self, id: int) -> int: + id_values = { + BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER: 3500, + BatteryManager.BATTERY_PROPERTY_CAPACITY: 85, + BatteryManager.BATTERY_PROPERTY_STATUS: BatteryManager.BATTERY_STATUS_DISCHARGING, + } + return id_values.get(id, 0) + + def getLongProperty(self, id: int) -> int: + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# BiometricManager (lightweight) +# ═══════════════════════════════════════════════════════════════ + +class BiometricManager(Stub): + """BiometricManager — 生物识别管理器 (轻量)""" + + BIOMETRIC_SUCCESS = 0 + BIOMETRIC_ERROR_HW_UNAVAILABLE = 1 + BIOMETRIC_ERROR_NONE_ENROLLED = 11 + BIOMETRIC_ERROR_NO_HARDWARE = 12 + BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15 + + def __init__(self): + super().__init__() + + def canAuthenticate(self) -> int: + return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE + + +# ═══════════════════════════════════════════════════════════════ +# Flashlight support +# ═══════════════════════════════════════════════════════════════ + +class CameraFlashlight(Stub): + """CameraFlashlight — 手电筒支持""" + + def __init__(self): + super().__init__() + self._on = False + + def setTorchMode(self, cameraId: str, enabled: bool): + self._on = enabled + + def isTorchModeEnabled(self, cameraId: str) -> bool: + return self._on + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Sensor", "SensorManager", "SensorEvent", "SensorEventListener", + "SensorDelay", + "HardwareBuffer", + "BatteryManager", + "BiometricManager", + "CameraFlashlight", +] \ No newline at end of file diff --git a/android_pyapi/android/hardware_biometrics.py b/android_pyapi/android/hardware_biometrics.py new file mode 100644 index 0000000..8567b80 --- /dev/null +++ b/android_pyapi/android/hardware_biometrics.py @@ -0,0 +1,291 @@ +""" +Android Biometrics 组件 — BiometricManager, BiometricPrompt, 等 +方法签名完全对齐 Android SDK +""" + +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError, android_context + + +# ═══════════════════════════════════════════════════════════════ +# BiometricConstants +# ═══════════════════════════════════════════════════════════════ + +class BiometricConstants(Stub): + """BiometricConstants — 生物识别常量""" + + BIOMETRIC_SUCCESS = 0 + BIOMETRIC_ERROR_HW_UNAVAILABLE = 1 + BIOMETRIC_ERROR_VENDOR = 8 + BIOMETRIC_ERROR_LOCKOUT = 7 + BIOMETRIC_ERROR_LOCKOUT_PERMANENT = 9 + BIOMETRIC_ERROR_USER_CANCELED = 10 + BIOMETRIC_ERROR_NO_BIOMETRIC = 11 + BIOMETRIC_ERROR_HW_NOT_PRESENT = 12 + BIOMETRIC_ERROR_NEGATIVE_BUTTON = 13 + BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL = 14 + BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15 + BIOMETRIC_ERROR_UNSUPPORTED = 65536 + BIOMETRIC_ERROR_CANCELED = 5 + BIOMETRIC_ERROR_TIMEOUT = 3 + BIOMETRIC_ERROR_NONE_ENROLLED = 11 + BIOMETRIC_ERROR_NO_HARDWARE = 12 + BIOMETRIC_STATUS_OK = 0 + + # Authenticator types + BIOMETRIC_STRONG = 0x0F + BIOMETRIC_WEAK = 0xFF + DEVICE_CREDENTIAL = 0x8000 + + +# ═══════════════════════════════════════════════════════════════ +# BiometricPrompt +# ═══════════════════════════════════════════════════════════════ + +class BiometricPrompt(Stub): + """BiometricPrompt — 生物识别提示 (API 28+)""" + + class AuthenticationCallback(Stub): + """BiometricPrompt.AuthenticationCallback — 认证回调""" + + def onAuthenticationError(self, errorCode: int, errString: str): + pass + + def onAuthenticationSucceeded(self, result: "BiometricPrompt.AuthenticationResult"): + pass + + def onAuthenticationFailed(self): + pass + + def onAuthenticationAcquired(self, acquireInfo: int): + pass + + class AuthenticationResult(Stub): + """BiometricPrompt.AuthenticationResult — 认证结果""" + + def __init__(self, cryptoObject: "BiometricPromptCryptoObject" = None, + authenticationType: int = BiometricConstants.BIOMETRIC_STRONG): + super().__init__() + self._crypto_object = cryptoObject + self._authentication_type = authenticationType + + def getCryptoObject(self) -> Optional["BiometricPromptCryptoObject"]: + return self._crypto_object + + def getAuthenticationType(self) -> int: + return self._authentication_type + + class PromptInfo(Stub): + """BiometricPrompt.PromptInfo — 提示信息""" + + class Builder(Stub): + """BiometricPrompt.PromptInfo.Builder — 提示信息构建器""" + + def __init__(self): + super().__init__() + self._title = "" + self._subtitle = "" + self._description = "" + self._device_credential_allowed = False + self._allowed_authenticators = 0 + self._confirmation_required = True + self._negative_button_text = "" + + def setTitle(self, title: str) -> "BiometricPrompt.PromptInfo.Builder": + self._title = title + return self + + def setSubtitle(self, subtitle: str) -> "BiometricPrompt.PromptInfo.Builder": + self._subtitle = subtitle + return self + + def setDescription(self, description: str) -> "BiometricPrompt.PromptInfo.Builder": + self._description = description + return self + + def setDeviceCredentialAllowed(self, allowed: bool) -> "BiometricPrompt.PromptInfo.Builder": + self._device_credential_allowed = allowed + return self + + def setAllowedAuthenticators(self, authenticators: int) -> "BiometricPrompt.PromptInfo.Builder": + self._allowed_authenticators = authenticators + return self + + def setConfirmationRequired(self, required: bool) -> "BiometricPrompt.PromptInfo.Builder": + self._confirmation_required = required + return self + + def setNegativeButtonText(self, text: str) -> "BiometricPrompt.PromptInfo.Builder": + self._negative_button_text = text + return self + + def build(self) -> "BiometricPrompt.PromptInfo": + info = BiometricPrompt.PromptInfo() + info._title = self._title + info._subtitle = self._subtitle + info._description = self._description + info._device_credential_allowed = self._device_credential_allowed + info._allowed_authenticators = self._allowed_authenticators + info._confirmation_required = self._confirmation_required + info._negative_button_text = self._negative_button_text + return info + + def __init__(self): + super().__init__() + self._title = "" + self._subtitle = "" + self._description = "" + self._device_credential_allowed = False + self._allowed_authenticators = 0 + self._confirmation_required = True + self._negative_button_text = "" + + def getTitle(self) -> str: + return self._title + + def getSubtitle(self) -> str: + return self._subtitle + + def getDescription(self) -> str: + return self._description + + def isDeviceCredentialAllowed(self) -> bool: + return self._device_credential_allowed + + def getAllowedAuthenticators(self) -> int: + return self._allowed_authenticators + + def isConfirmationRequired(self) -> bool: + return self._confirmation_required + + def getNegativeButtonText(self) -> str: + return self._negative_button_text + + def __init__(self, context, callback: AuthenticationCallback = None, + handler=None): + super().__init__() + self._context = context + self._callback = callback + self._cancelled = False + + def authenticate(self, crypto: "BiometricPromptCryptoObject" = None, + cancel: Any = None, executor=None, + callback: AuthenticationCallback = None): + self._cancelled = False + if callback: + self._callback = callback + if self._callback: + # Simulate failed authentication on desktop + self._callback.onAuthenticationError( + BiometricConstants.BIOMETRIC_ERROR_NO_HARDWARE, + "No biometric hardware available on this device" + ) + + def authenticate(self, promptInfo: PromptInfo, crypto: "BiometricPromptCryptoObject" = None): + self._cancelled = False + if self._callback: + self._callback.onAuthenticationError( + BiometricConstants.BIOMETRIC_ERROR_NO_HARDWARE, + "No biometric hardware available on this device" + ) + + def cancelAuthentication(self): + self._cancelled = True + if self._callback: + self._callback.onAuthenticationError( + BiometricConstants.BIOMETRIC_ERROR_CANCELED, + "Authentication cancelled" + ) + + def cancel(self): + self.cancelAuthentication() + + def isCancelled(self) -> bool: + return self._cancelled + + +# ═══════════════════════════════════════════════════════════════ +# BiometricPromptCryptoObject +# ═══════════════════════════════════════════════════════════════ + +class BiometricPromptCryptoObject(Stub): + """BiometricPromptCryptoObject — 加密对象""" + + def __init__(self, signature=None, cipher=None, mac=None): + super().__init__() + self._signature = signature + self._cipher = cipher + self._mac = mac + + def getSignature(self): + return self._signature + + def getCipher(self): + return self._cipher + + def getMac(self): + return self._mac + + +# ═══════════════════════════════════════════════════════════════ +# BiometricPromptAuthenticationResult +# ═══════════════════════════════════════════════════════════════ + +BiometricPromptAuthenticationResult = BiometricPrompt.AuthenticationResult + + +# ═══════════════════════════════════════════════════════════════ +# BiometricManager +# ═══════════════════════════════════════════════════════════════ + +class BiometricManager(Stub): + """BiometricManager — 生物识别管理器 (API 29+)""" + + BIOMETRIC_SUCCESS = BiometricConstants.BIOMETRIC_SUCCESS + BIOMETRIC_ERROR_HW_UNAVAILABLE = BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE + BIOMETRIC_ERROR_NONE_ENROLLED = BiometricConstants.BIOMETRIC_ERROR_NONE_ENROLLED + BIOMETRIC_ERROR_NO_HARDWARE = BiometricConstants.BIOMETRIC_ERROR_NO_HARDWARE + BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = BiometricConstants.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED + BIOMETRIC_ERROR_UNSUPPORTED = BiometricConstants.BIOMETRIC_ERROR_UNSUPPORTED + BIOMETRIC_STATUS_OK = BiometricConstants.BIOMETRIC_STATUS_OK + BIOMETRIC_ERROR_VENDOR = BiometricConstants.BIOMETRIC_ERROR_VENDOR + BIOMETRIC_ERROR_USER_CANCELED = BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED + BIOMETRIC_ERROR_CANCELED = BiometricConstants.BIOMETRIC_ERROR_CANCELED + BIOMETRIC_ERROR_TIMEOUT = BiometricConstants.BIOMETRIC_ERROR_TIMEOUT + BIOMETRIC_ERROR_LOCKOUT = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT + BIOMETRIC_ERROR_LOCKOUT_PERMANENT = BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT + + # Authenticators + AUTHENTICATOR_BIOMETRIC_STRONG = BiometricConstants.BIOMETRIC_STRONG + AUTHENTICATOR_BIOMETRIC_WEAK = BiometricConstants.BIOMETRIC_WEAK + AUTHENTICATOR_DEVICE_CREDENTIAL = BiometricConstants.DEVICE_CREDENTIAL + + def __init__(self, context=None): + super().__init__() + self._context = context + + def canAuthenticate(self, authenticators: int = AUTHENTICATOR_BIOMETRIC_STRONG) -> int: + """Check if biometric authentication is available""" + return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE + + @staticmethod + def fromContext(context) -> "BiometricManager": + """Get BiometricManager from context""" + return BiometricManager(context) + + def toString(self) -> str: + return "BiometricManager(desktop - no hardware)" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "BiometricManager", + "BiometricPrompt", + "BiometricPromptAuthenticationResult", + "BiometricPromptCryptoObject", + "BiometricConstants", +] \ No newline at end of file diff --git a/android_pyapi/android/hardware_camera2.py b/android_pyapi/android/hardware_camera2.py new file mode 100644 index 0000000..4025211 --- /dev/null +++ b/android_pyapi/android/hardware_camera2.py @@ -0,0 +1,761 @@ +""" +Android Camera2 组件 — CameraManager, CameraDevice, CaptureRequest 等 +方法签名完全对齐 Android SDK +""" + +import time +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL + + +# ═══════════════════════════════════════════════════════════════ +# CameraMetadata +# ═══════════════════════════════════════════════════════════════ + +class CameraMetadata(Stub): + """CameraMetadata — 相机元数据基类""" + + # CONTROL_AE modes + CONTROL_AE_MODE_OFF = 0 + CONTROL_AE_MODE_ON = 1 + CONTROL_AE_MODE_ON_AUTO_FLASH = 2 + CONTROL_AE_MODE_ON_ALWAYS_FLASH = 3 + CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE = 4 + CONTROL_AE_MODE_EXTERNAL_FLASH = 5 + + # CONTROL_AF modes + CONTROL_AF_MODE_OFF = 0 + CONTROL_AF_MODE_AUTO = 1 + CONTROL_AF_MODE_MACRO = 2 + CONTROL_AF_MODE_CONTINUOUS_VIDEO = 3 + CONTROL_AF_MODE_CONTINUOUS_PICTURE = 4 + CONTROL_AF_MODE_EDOF = 5 + + # CONTROL_AWB modes + CONTROL_AWB_MODE_OFF = 0 + CONTROL_AWB_MODE_AUTO = 1 + CONTROL_AWB_MODE_INCANDESCENT = 2 + CONTROL_AWB_MODE_FLUORESCENT = 3 + CONTROL_AWB_MODE_WARM_FLUORESCENT = 4 + CONTROL_AWB_MODE_DAYLIGHT = 5 + CONTROL_AWB_MODE_CLOUDY_DAYLIGHT = 6 + CONTROL_AWB_MODE_TWILIGHT = 7 + CONTROL_AWB_MODE_SHADE = 8 + + # CONTROL modes + CONTROL_MODE_OFF = 0 + CONTROL_MODE_AUTO = 1 + CONTROL_MODE_USE_SCENE_MODE = 2 + CONTROL_MODE_OFF_KEEP_STATE = 3 + + # FLASH modes + FLASH_MODE_OFF = 0 + FLASH_MODE_SINGLE = 1 + FLASH_MODE_TORCH = 2 + + # STATISTICS_FACE_DETECT modes + STATISTICS_FACE_DETECT_MODE_OFF = 0 + STATISTICS_FACE_DETECT_MODE_SIMPLE = 1 + STATISTICS_FACE_DETECT_MODE_FULL = 2 + + # EDGE modes + EDGE_MODE_OFF = 0 + EDGE_MODE_FAST = 1 + EDGE_MODE_HIGH_QUALITY = 2 + EDGE_MODE_ZERO_SHUTTER_LAG = 3 + + # NOISE_REDUCTION modes + NOISE_REDUCTION_MODE_OFF = 0 + NOISE_REDUCTION_MODE_FAST = 1 + NOISE_REDUCTION_MODE_HIGH_QUALITY = 2 + NOISE_REDUCTION_MODE_MINIMAL = 3 + NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG = 4 + + # TONEMAP modes + TONEMAP_MODE_CONTRAST_CURVE = 0 + TONEMAP_MODE_FAST = 1 + TONEMAP_MODE_HIGH_QUALITY = 2 + TONEMAP_MODE_GAMMA_VALUE = 3 + TONEMAP_MODE_PRESET_CURVE = 4 + + # COLOR_CORRECTION modes + COLOR_CORRECTION_MODE_TRANSFORM_MATRIX = 0 + COLOR_CORRECTION_MODE_FAST = 1 + COLOR_CORRECTION_MODE_HIGH_QUALITY = 2 + + # COLOR_CORRECTION_ABERRATION modes + COLOR_CORRECTION_ABERRATION_MODE_OFF = 0 + COLOR_CORRECTION_ABERRATION_MODE_FAST = 1 + COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY = 2 + + # CONTROL_AE_PRECAPTURE_TRIGGER + CONTROL_AE_PRECAPTURE_TRIGGER_IDLE = 0 + CONTROL_AE_PRECAPTURE_TRIGGER_START = 1 + CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL = 2 + + # CONTROL_EFFECT modes + CONTROL_EFFECT_MODE_OFF = 0 + CONTROL_EFFECT_MODE_MONO = 1 + CONTROL_EFFECT_MODE_NEGATIVE = 2 + CONTROL_EFFECT_MODE_SOLARIZE = 3 + CONTROL_EFFECT_MODE_SEPIA = 4 + CONTROL_EFFECT_MODE_POSTERIZE = 5 + CONTROL_EFFECT_MODE_WHITEBOARD = 6 + CONTROL_EFFECT_MODE_BLACKBOARD = 7 + CONTROL_EFFECT_MODE_AQUA = 8 + + # CONTROL_SCENE modes + CONTROL_SCENE_MODE_DISABLED = 0 + CONTROL_SCENE_MODE_FACE_PRIORITY = 1 + CONTROL_SCENE_MODE_ACTION = 2 + CONTROL_SCENE_MODE_PORTRAIT = 3 + CONTROL_SCENE_MODE_LANDSCAPE = 4 + CONTROL_SCENE_MODE_NIGHT = 5 + CONTROL_SCENE_MODE_NIGHT_PORTRAIT = 6 + CONTROL_SCENE_MODE_THEATRE = 7 + CONTROL_SCENE_MODE_BEACH = 8 + CONTROL_SCENE_MODE_SNOW = 9 + CONTROL_SCENE_MODE_SUNSET = 10 + CONTROL_SCENE_MODE_STEADYPHOTO = 11 + CONTROL_SCENE_MODE_FIREWORKS = 12 + CONTROL_SCENE_MODE_SPORTS = 13 + CONTROL_SCENE_MODE_PARTY = 14 + CONTROL_SCENE_MODE_CANDLELIGHT = 15 + CONTROL_SCENE_MODE_BARCODE = 16 + CONTROL_SCENE_MODE_HIGH_SPEED_VIDEO = 17 + CONTROL_SCENE_MODE_HDR = 18 + + # CONTROL_VIDEO_STABILIZATION modes + CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0 + CONTROL_VIDEO_STABILIZATION_MODE_ON = 1 + + # DISTORTION_CORRECTION modes + DISTORTION_CORRECTION_MODE_OFF = 0 + DISTORTION_CORRECTION_MODE_FAST = 1 + DISTORTION_CORRECTION_MODE_HIGH_QUALITY = 2 + + # HOT_PIXEL modes + HOT_PIXEL_MODE_OFF = 0 + HOT_PIXEL_MODE_FAST = 1 + HOT_PIXEL_MODE_HIGH_QUALITY = 2 + + # LENS_OPTICAL_STABILIZATION modes + LENS_OPTICAL_STABILIZATION_MODE_OFF = 0 + LENS_OPTICAL_STABILIZATION_MODE_ON = 1 + + # LENS_FACING + LENS_FACING_FRONT = 0 + LENS_FACING_BACK = 1 + LENS_FACING_EXTERNAL = 2 + + # INFO_SUPPORTED_HARDWARE_LEVEL + INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED = 0 + INFO_SUPPORTED_HARDWARE_LEVEL_FULL = 1 + INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY = 2 + INFO_SUPPORTED_HARDWARE_LEVEL_3 = 3 + INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL = 4 + + +# ═══════════════════════════════════════════════════════════════ +# CameraCharacteristics +# ═══════════════════════════════════════════════════════════════ + +class CameraCharacteristics(CameraMetadata): + """CameraCharacteristics — 相机特性""" + + LENS_FACING = "android.lens.facing" + SENSOR_ORIENTATION = "android.sensor.orientation" + FLASH_INFO_AVAILABLE = "android.flash.info.available" + INFO_SUPPORTED_HARDWARE_LEVEL = "android.info.supportedHardwareLevel" + SCALER_STREAM_CONFIGURATION_MAP = "android.scaler.streamConfigurationMap" + CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES = "android.control.aeAvailableTargetFpsRanges" + CONTROL_AE_AVAILABLE_MODES = "android.control.aeAvailableModes" + CONTROL_AF_AVAILABLE_MODES = "android.control.afAvailableModes" + CONTROL_AVAILABLE_EFFECTS = "android.control.availableEffects" + CONTROL_AVAILABLE_SCENE_MODES = "android.control.availableSceneModes" + CONTROL_MAX_REGIONS_AE = "android.control.maxRegionsAe" + CONTROL_MAX_REGIONS_AF = "android.control.maxRegionsAf" + CONTROL_MAX_REGIONS_AWB = "android.control.maxRegionsAwb" + JPEG_AVAILABLE_THUMBNAIL_SIZES = "android.jpeg.availableThumbnailSizes" + JPEG_MAX_SIZE = "android.jpeg.maxSize" + SENSOR_INFO_PIXEL_ARRAY_SIZE = "android.sensor.info.pixelArraySize" + SENSOR_INFO_ACTIVE_ARRAY_SIZE = "android.sensor.info.activeArraySize" + FLASH_MODE = "android.flash.mode" + + def __init__(self, camera_id: str = "0"): + super().__init__() + self._properties: Dict[str, Any] = { + CameraCharacteristics.LENS_FACING: CameraCharacteristics.LENS_FACING_BACK, + CameraCharacteristics.SENSOR_ORIENTATION: 90, + CameraCharacteristics.FLASH_INFO_AVAILABLE: True, + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL: + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP: { + "output_sizes": [(1920, 1080), (1280, 720), (640, 480)], + "input_sizes": [(1920, 1080)], + }, + CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES: [(15, 30), (30, 30)], + CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES: [0, 1, 2, 3, 4], + CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES: [0, 1, 2, 3, 4, 5], + CameraCharacteristics.CONTROL_AVAILABLE_EFFECTS: list(range(9)), + CameraCharacteristics.CONTROL_AVAILABLE_SCENE_MODES: list(range(19)), + CameraCharacteristics.CONTROL_MAX_REGIONS_AE: 1, + CameraCharacteristics.CONTROL_MAX_REGIONS_AF: 1, + CameraCharacteristics.CONTROL_MAX_REGIONS_AWB: 0, + CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES: [(320, 240), (160, 120)], + CameraCharacteristics.JPEG_MAX_SIZE: 5000000, + CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE: (4032, 3024), + CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE: (4032, 3024), + } + + def get(self, key: str) -> Any: + return self._properties.get(key) + + def keys(self) -> Set[str]: + return set(self._properties.keys()) + + def query(self, key: str) -> Any: + return self._properties.get(key) + + +# ═══════════════════════════════════════════════════════════════ +# CameraId +# ═══════════════════════════════════════════════════════════════ + +class CameraId(Stub): + """CameraId — 相机ID封装""" + + def __init__(self, id_string: str): + super().__init__() + self._id = id_string + + def getId(self) -> str: + return self._id + + def toString(self) -> str: + return self._id + + def equals(self, other) -> bool: + if isinstance(other, CameraId): + return self._id == other._id + if isinstance(other, str): + return self._id == other + return False + + def hashCode(self) -> int: + return hash(self._id) + + +# ═══════════════════════════════════════════════════════════════ +# CameraCharacteristicsKey +# ═══════════════════════════════════════════════════════════════ + +class CameraCharacteristicsKey(Stub): + """CameraCharacteristicsKey — 相机特性键""" + + def __init__(self, name: str, native_type: type = object): + super().__init__() + self._name = name + self._native_type = native_type + + def getName(self) -> str: + return self._name + + def getNativeType(self) -> type: + return self._native_type + + def equals(self, other) -> bool: + if isinstance(other, CameraCharacteristicsKey): + return self._name == other._name + return False + + def hashCode(self) -> int: + return hash(self._name) + + def toString(self) -> str: + return f"CameraCharacteristicsKey({self._name})" + + +# ═══════════════════════════════════════════════════════════════ +# CaptureRequest +# ═══════════════════════════════════════════════════════════════ + +class CaptureRequest(CameraMetadata): + """CaptureRequest — 捕获请求""" + + CONTROL_AE_MODE = "android.control.aeMode" + CONTROL_AF_MODE = "android.control.afMode" + CONTROL_AWB_MODE = "android.control.awbMode" + CONTROL_MODE = "android.control.mode" + FLASH_MODE = "android.flash.mode" + SENSOR_EXPOSURE_TIME = "android.sensor.exposureTime" + SENSOR_SENSITIVITY = "android.sensor.sensitivity" + LENS_FOCUS_DISTANCE = "android.lens.focusDistance" + STATISTICS_FACE_DETECT_MODE = "android.statistics.faceDetectMode" + JPEG_QUALITY = "android.jpeg.quality" + JPEG_ORIENTATION = "android.jpeg.orientation" + JPEG_THUMBNAIL_QUALITY = "android.jpeg.thumbnailQuality" + JPEG_THUMBNAIL_SIZE = "android.jpeg.thumbnailSize" + SCALER_CROP_REGION = "android.scaler.cropRegion" + EDGE_MODE = "android.edge.mode" + NOISE_REDUCTION_MODE = "android.noiseReduction.mode" + TONEMAP_MODE = "android.tonemap.mode" + BLACK_LEVEL_LOCK = "android.blackLevel.lock" + CONTROL_AE_TARGET_FPS_RANGE = "android.control.aeTargetFpsRange" + CONTROL_AE_EXPOSURE_COMPENSATION = "android.control.aeExposureCompensation" + CONTROL_AE_LOCK = "android.control.aeLock" + CONTROL_AE_PRECAPTURE_TRIGGER = "android.control.aePrecaptureTrigger" + CONTROL_AWB_LOCK = "android.control.awbLock" + CONTROL_EFFECT_MODE = "android.control.effectMode" + CONTROL_SCENE_MODE = "android.control.sceneMode" + CONTROL_VIDEO_STABILIZATION_MODE = "android.control.videoStabilizationMode" + COLOR_CORRECTION_MODE = "android.colorCorrection.mode" + COLOR_CORRECTION_GAINS = "android.colorCorrection.gains" + COLOR_CORRECTION_TRANSFORM = "android.colorCorrection.transform" + COLOR_CORRECTION_ABERRATION_MODE = "android.colorCorrection.aberrationMode" + CONTROL_POST_RAW_SENSITIVITY_BOOST = "android.control.postRawSensitivityBoost" + DISTORTION_CORRECTION_MODE = "android.distortionCorrection.mode" + HOT_PIXEL_MODE = "android.hotPixel.mode" + LENS_OPTICAL_STABILIZATION_MODE = "android.lens.opticalStabilizationMode" + REQUEST_PIPELINE_MAX_DEPTH = "android.request.pipelineMaxDepth" + SENSOR_TIMESTAMP = "android.sensor.timestamp" + STATISTICS_LENS_SHADING_MAP_MODE = "android.statistics.lensShadingMapMode" + STATISTICS_OIS_DATA_MODE = "android.statistics.oisDataMode" + STATISTICS_SECTION = "android.statistics.section" + TONEMAP_CURVE = "android.tonemap.curve" + TONEMAP_GAMMA = "android.tonemap.gamma" + + class Builder(Stub): + """CaptureRequest.Builder — 捕获请求构建器""" + + def __init__(self, camera_device: "CameraDevice"): + super().__init__() + self._parameters: Dict[str, Any] = {} + self._tag = None + self._camera_device = camera_device + + def set(self, key: str, value: Any): + self._parameters[key] = value + + def get(self, key: str) -> Any: + return self._parameters.get(key) + + def setTag(self, tag: Any): + self._tag = tag + + def build(self) -> "CaptureRequest": + req = CaptureRequest() + req._parameters = dict(self._parameters) + req._tag = self._tag + return req + + def addTarget(self, surface): + pass + + def removeTarget(self, surface): + pass + + def __init__(self): + super().__init__() + self._parameters: Dict[str, Any] = { + CaptureRequest.CONTROL_AE_MODE: CameraMetadata.CONTROL_AE_MODE_ON, + CaptureRequest.CONTROL_AF_MODE: CameraMetadata.CONTROL_AF_MODE_AUTO, + CaptureRequest.CONTROL_AWB_MODE: CameraMetadata.CONTROL_AWB_MODE_AUTO, + CaptureRequest.CONTROL_MODE: CameraMetadata.CONTROL_MODE_AUTO, + CaptureRequest.FLASH_MODE: CameraMetadata.FLASH_MODE_OFF, + CaptureRequest.JPEG_QUALITY: 95, + CaptureRequest.JPEG_ORIENTATION: 0, + } + self._tag = None + self._targets: List[Any] = [] + + def get(self, key: str) -> Any: + return self._parameters.get(key) + + def keys(self) -> Set[str]: + return set(self._parameters.keys()) + + def getTag(self) -> Any: + return self._tag + + +# ═══════════════════════════════════════════════════════════════ +# CaptureResult +# ═══════════════════════════════════════════════════════════════ + +class CaptureResult(CameraMetadata): + """CaptureResult — 捕获结果""" + + # Same keys as CaptureRequest + CONTROL_AE_MODE = CaptureRequest.CONTROL_AE_MODE + CONTROL_AF_MODE = CaptureRequest.CONTROL_AF_MODE + CONTROL_AWB_MODE = CaptureRequest.CONTROL_AWB_MODE + CONTROL_MODE = CaptureRequest.CONTROL_MODE + FLASH_MODE = CaptureRequest.FLASH_MODE + SENSOR_EXPOSURE_TIME = CaptureRequest.SENSOR_EXPOSURE_TIME + SENSOR_SENSITIVITY = CaptureRequest.SENSOR_SENSITIVITY + LENS_FOCUS_DISTANCE = CaptureRequest.LENS_FOCUS_DISTANCE + STATISTICS_FACE_DETECT_MODE = CaptureRequest.STATISTICS_FACE_DETECT_MODE + JPEG_QUALITY = CaptureRequest.JPEG_QUALITY + JPEG_ORIENTATION = CaptureRequest.JPEG_ORIENTATION + JPEG_THUMBNAIL_QUALITY = CaptureRequest.JPEG_THUMBNAIL_QUALITY + JPEG_THUMBNAIL_SIZE = CaptureRequest.JPEG_THUMBNAIL_SIZE + SCALER_CROP_REGION = CaptureRequest.SCALER_CROP_REGION + EDGE_MODE = CaptureRequest.EDGE_MODE + NOISE_REDUCTION_MODE = CaptureRequest.NOISE_REDUCTION_MODE + TONEMAP_MODE = CaptureRequest.TONEMAP_MODE + BLACK_LEVEL_LOCK = CaptureRequest.BLACK_LEVEL_LOCK + CONTROL_AE_TARGET_FPS_RANGE = CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE + CONTROL_AE_EXPOSURE_COMPENSATION = CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION + CONTROL_AE_LOCK = CaptureRequest.CONTROL_AE_LOCK + CONTROL_AE_PRECAPTURE_TRIGGER = CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER + CONTROL_AWB_LOCK = CaptureRequest.CONTROL_AWB_LOCK + CONTROL_EFFECT_MODE = CaptureRequest.CONTROL_EFFECT_MODE + CONTROL_SCENE_MODE = CaptureRequest.CONTROL_SCENE_MODE + CONTROL_VIDEO_STABILIZATION_MODE = CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE + COLOR_CORRECTION_MODE = CaptureRequest.COLOR_CORRECTION_MODE + COLOR_CORRECTION_GAINS = CaptureRequest.COLOR_CORRECTION_GAINS + COLOR_CORRECTION_TRANSFORM = CaptureRequest.COLOR_CORRECTION_TRANSFORM + COLOR_CORRECTION_ABERRATION_MODE = CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE + CONTROL_POST_RAW_SENSITIVITY_BOOST = CaptureRequest.CONTROL_POST_RAW_SENSITIVITY_BOOST + DISTORTION_CORRECTION_MODE = CaptureRequest.DISTORTION_CORRECTION_MODE + HOT_PIXEL_MODE = CaptureRequest.HOT_PIXEL_MODE + LENS_OPTICAL_STABILIZATION_MODE = CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE + REQUEST_PIPELINE_MAX_DEPTH = CaptureRequest.REQUEST_PIPELINE_MAX_DEPTH + SENSOR_TIMESTAMP = CaptureRequest.SENSOR_TIMESTAMP + STATISTICS_LENS_SHADING_MAP_MODE = CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE + STATISTICS_OIS_DATA_MODE = CaptureRequest.STATISTICS_OIS_DATA_MODE + STATISTICS_SECTION = CaptureRequest.STATISTICS_SECTION + TONEMAP_CURVE = CaptureRequest.TONEMAP_CURVE + TONEMAP_GAMMA = CaptureRequest.TONEMAP_GAMMA + + def __init__(self, request: CaptureRequest = None): + super().__init__() + self._results: Dict[str, Any] = { + CaptureResult.CONTROL_AE_MODE: CameraMetadata.CONTROL_AE_MODE_ON, + CaptureResult.CONTROL_AF_MODE: CameraMetadata.CONTROL_AF_MODE_AUTO, + CaptureResult.CONTROL_AWB_MODE: CameraMetadata.CONTROL_AWB_MODE_AUTO, + CaptureResult.CONTROL_MODE: CameraMetadata.CONTROL_MODE_AUTO, + CaptureResult.FLASH_MODE: CameraMetadata.FLASH_MODE_OFF, + CaptureResult.SENSOR_EXPOSURE_TIME: 33333333, + CaptureResult.SENSOR_SENSITIVITY: 100, + CaptureResult.SENSOR_TIMESTAMP: int(time.time() * 1e9), + } + self._request = request + self._sequence_id = -1 + self._frame_number = -1 + + def get(self, key: str) -> Any: + return self._results.get(key) + + def keys(self) -> Set[str]: + return set(self._results.keys()) + + def getRequest(self) -> Optional[CaptureRequest]: + return self._request + + def getSequenceId(self) -> int: + return self._sequence_id + + def getFrameNumber(self) -> int: + return self._frame_number + + +# ═══════════════════════════════════════════════════════════════ +# TotalCaptureResult +# ═══════════════════════════════════════════════════════════════ + +class TotalCaptureResult(CaptureResult): + """TotalCaptureResult — 完整捕获结果""" + + def __init__(self, request: CaptureRequest = None, + partial_results: List[CaptureResult] = None): + super().__init__(request) + self._partial_results = partial_results or [] + + def getPartialResults(self) -> List[CaptureResult]: + return list(self._partial_results) + + +# ═══════════════════════════════════════════════════════════════ +# CameraCaptureSession +# ═══════════════════════════════════════════════════════════════ + +class CameraCaptureSession(Stub): + """CameraCaptureSession — 捕获会话""" + + class StateCallback(Stub): + """CameraCaptureSession.StateCallback — 会话状态回调""" + + def onConfigured(self, session: "CameraCaptureSession"): + pass + + def onConfigureFailed(self, session: "CameraCaptureSession"): + pass + + def onReady(self, session: "CameraCaptureSession"): + pass + + def onActive(self, session: "CameraCaptureSession"): + pass + + def onClosed(self, session: "CameraCaptureSession"): + pass + + def onSurfacePrepared(self, session: "CameraCaptureSession", surface): + pass + + class CaptureCallback(Stub): + """CameraCaptureSession.CaptureCallback — 捕获回调""" + + def onCaptureStarted(self, session: "CameraCaptureSession", request: CaptureRequest, + timestamp: int, frameNumber: int): + pass + + def onCaptureCompleted(self, session: "CameraCaptureSession", request: CaptureRequest, + result: TotalCaptureResult): + pass + + def onCaptureFailed(self, session: "CameraCaptureSession", request: CaptureRequest, + failure): + pass + + def onCaptureProgressed(self, session: "CameraCaptureSession", request: CaptureRequest, + result: CaptureResult): + pass + + def onCaptureSequenceCompleted(self, session: "CameraCaptureSession", + sequenceId: int, frameNumber: int): + pass + + def onCaptureSequenceAborted(self, session: "CameraCaptureSession", sequenceId: int): + pass + + def onCaptureBufferLost(self, session: "CameraCaptureSession", request: CaptureRequest, + target, frameNumber: int): + pass + + def __init__(self, device: "CameraDevice"): + super().__init__() + self._device = device + self._closed = False + self._repeating_request = None + self._state_callback = None + self._session_id = int(time.time() * 1000) + + def capture(self, request: CaptureRequest, callback: CaptureCallback = None, + handler=None) -> int: + seq_id = int(time.time() * 1000) + result = TotalCaptureResult(request) + if callback: + callback.onCaptureStarted(self, request, int(time.time() * 1e9), 0) + callback.onCaptureCompleted(self, request, result) + callback.onCaptureSequenceCompleted(self, seq_id, 0) + return seq_id + + def setRepeatingRequest(self, request: CaptureRequest, callback: CaptureCallback = None, + handler=None) -> int: + self._repeating_request = request + seq_id = int(time.time() * 1000) + result = TotalCaptureResult(request) + if callback: + callback.onCaptureStarted(self, request, int(time.time() * 1e9), 0) + callback.onCaptureCompleted(self, request, result) + return seq_id + + def stopRepeatingRequest(self) -> None: + self._repeating_request = None + + def abortCaptures(self) -> None: + pass + + def close(self): + self._closed = True + + def getDevice(self) -> "CameraDevice": + return self._device + + def isClosed(self) -> bool: + return self._closed + + +# ═══════════════════════════════════════════════════════════════ +# CameraDevice +# ═══════════════════════════════════════════════════════════════ + +class CameraDevice(Stub): + """CameraDevice — 相机设备""" + + STATE_OPENED = 0 + STATE_CLOSED = 3 + STATE_DISCONNECTED = 1 + STATE_ERROR = 2 + + class StateCallback(Stub): + """CameraDevice.StateCallback — 设备状态回调""" + + def onOpened(self, device: "CameraDevice"): + pass + + def onClosed(self, device: "CameraDevice"): + pass + + def onDisconnected(self, device: "CameraDevice"): + pass + + def onError(self, device: "CameraDevice", error: int): + pass + + def __init__(self, camera_id: str = "0"): + super().__init__() + self._id = camera_id + self._state = CameraDevice.STATE_OPENED + self._state_callback = None + self._sessions: List[CameraCaptureSession] = [] + + def getId(self) -> str: + return self._id + + def createCaptureRequest(self, templateType: int) -> CaptureRequest.Builder: + return CaptureRequest.Builder(self) + + def createCaptureSession(self, outputs: List[Any], + callback: CameraCaptureSession.StateCallback, + handler=None): + session = CameraCaptureSession(self) + session._state_callback = callback + self._sessions.append(session) + if callback: + callback.onConfigured(session) + return session + + def close(self): + self._state = CameraDevice.STATE_CLOSED + for session in self._sessions: + session.close() + + def getState(self) -> int: + return self._state + + def toString(self) -> str: + return f"CameraDevice@{self._id}" + + +# ═══════════════════════════════════════════════════════════════ +# CameraManager +# ═══════════════════════════════════════════════════════════════ + +class CameraManager(Stub): + """CameraManager — 相机管理器""" + + def __init__(self): + super().__init__() + self._cameras: Dict[str, CameraCharacteristics] = { + "0": CameraCharacteristics("0"), + } + self._open_devices: Dict[str, CameraDevice] = {} + self._availability_callbacks: List[Any] = [] + self._torch_callbacks: List[Any] = [] + + def getCameraIdList(self) -> List[str]: + return list(self._cameras.keys()) + + def getCameraCharacteristics(self, cameraId: str) -> CameraCharacteristics: + if cameraId not in self._cameras: + raise AndroidOnlyError(f"Camera {cameraId} not available") + return self._cameras[cameraId] + + def openCamera(self, cameraId: str, callback: CameraDevice.StateCallback, + handler=None): + if cameraId not in self._cameras: + if callback: + callback.onError(None, CameraDevice.STATE_ERROR) + return + device = CameraDevice(cameraId) + self._open_devices[cameraId] = device + if callback: + callback.onOpened(device) + + def registerAvailabilityCallback(self, callback): + self._availability_callbacks.append(callback) + + def unregisterAvailabilityCallback(self, callback): + if callback in self._availability_callbacks: + self._availability_callbacks.remove(callback) + + def registerTorchCallback(self, callback, handler=None): + self._torch_callbacks.append(callback) + + def unregisterTorchCallback(self, callback): + if callback in self._torch_callbacks: + self._torch_callbacks.remove(callback) + + def setTorchMode(self, cameraId: str, enabled: bool): + pass + + +# ═══════════════════════════════════════════════════════════════ +# CameraOfflineSession +# ═══════════════════════════════════════════════════════════════ + +class CameraOfflineSession(CameraCaptureSession): + """CameraOfflineSession — 离线会话 (API 30+)""" + + def __init__(self, device: CameraDevice): + super().__init__(device) + + def switchToOffline(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# CameraExtensionSession +# ═══════════════════════════════════════════════════════════════ + +class CameraExtensionSession(Stub): + """CameraExtensionSession — 扩展会话 (API 31+)""" + + class StateCallback(Stub): + def onConfigured(self, session: "CameraExtensionSession"): + pass + + def onConfigureFailed(self, session: "CameraExtensionSession"): + pass + + def onClosed(self, session: "CameraExtensionSession"): + pass + + class ExtensionCaptureCallback(Stub): + def onCaptureStarted(self, session: "CameraExtensionSession", request: CaptureRequest, + timestamp: int): + pass + + def onCaptureProcessStarted(self, session: "CameraExtensionSession", request: CaptureRequest): + pass + + def onCaptureFailed(self, session: "CameraExtensionSession", request: CaptureRequest): + pass + + def onCaptureResultAvailable(self, session: "CameraExtensionSession", + request: CaptureRequest, result: TotalCaptureResult): + pass + + def __init__(self): + super().__init__() + self._closed = False + + def close(self): + self._closed = True + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "CameraManager", "CameraDevice", "CameraCaptureSession", + "CaptureRequest", "CaptureResult", "CameraCharacteristics", + "CameraMetadata", "CameraId", "CameraCharacteristicsKey", + "TotalCaptureResult", "CameraOfflineSession", "CameraExtensionSession", +] \ No newline at end of file diff --git a/android_pyapi/android/inputmethod.py b/android_pyapi/android/inputmethod.py new file mode 100644 index 0000000..553deef --- /dev/null +++ b/android_pyapi/android/inputmethod.py @@ -0,0 +1,804 @@ +""" +Android InputMethod 组件 — InputMethodService, Keyboard, KeyboardView, Key 等 +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError, android_context + + +# ═══════════════════════════════════════════════════════════════ +# InputMethod +# ═══════════════════════════════════════════════════════════════ + +class InputMethod(Stub): + """InputMethod — 输入法接口""" + + def __init__(self): + super().__init__() + + def createSession(self, callback: Any) -> Optional["InputMethodSession"]: + """创建输入法会话""" + return None + + def startInput(self, input_connection: Any, editor_info: Any) -> None: + """开始输入""" + pass + + def restartInput(self, input_connection: Any, editor_info: Any) -> None: + """重启输入""" + pass + + def dispatchKeyEvent(self, event: Any) -> None: + """分发按键事件""" + pass + + def dispatchTrackballEvent(self, event: Any) -> None: + """分发轨迹球事件""" + pass + + def dispatchGenericMotionEvent(self, event: Any) -> None: + """分发通用运动事件""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# InputMethodSession +# ═══════════════════════════════════════════════════════════════ + +class InputMethodSession(Stub): + """InputMethodSession — 输入法会话接口""" + + def __init__(self): + super().__init__() + + def finishInput(self): + """结束输入""" + pass + + def updateSelection(self, old_sel_start: int, old_sel_end: int, + new_sel_start: int, new_sel_end: int, + candidates_start: int, candidates_end: int): + """更新选区""" + pass + + def updateCursor(self, old_cursor: Any, new_cursor: Any): + """更新光标""" + pass + + def displayCompletions(self, completions: List["CompletionInfo"]): + """显示补全""" + pass + + def appPrivateCommand(self, action: str, data: Any): + """应用私有命令""" + pass + + def viewClicked(self, focus_changed: bool): + """视图点击""" + pass + + def updateExtractedText(self, token: int, extracted_text: Any): + """更新提取文本""" + pass + + def dispatchKeyEvent(self, key_code: int, event: Any) -> None: + """分发按键事件""" + pass + + def dispatchTrackballEvent(self, event: Any) -> None: + pass + + def dispatchGenericMotionEvent(self, event: Any) -> None: + pass + + def toggleSoftInput(self, show_flags: int, hide_flags: int): + """切换软键盘""" + pass + + def notifyImeHidden(self): + """通知输入法隐藏""" + pass + + def removeImeSurface(self): + """移除输入法界面""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# AbstractInputMethodService +# ═══════════════════════════════════════════════════════════════ + +class AbstractInputMethodService(Stub): + """AbstractInputMethodService — 抽象输入法服务基类""" + + def __init__(self): + super().__init__() + self._input_method: Optional[InputMethod] = None + self._input_method_session: Optional[InputMethodSession] = None + + def onCreate(self): + """服务创建""" + pass + + def onDestroy(self): + """服务销毁""" + pass + + def onBind(self, intent: Any) -> Any: + """绑定""" + return None + + def onUnbind(self, intent: Any) -> bool: + """解绑""" + return False + + def onCreateInputMethod(self) -> InputMethod: + """创建输入法实例""" + return InputMethod() + + def onCreateInputMethodSession(self) -> InputMethodSession: + """创建输入法会话""" + return InputMethodSession() + + def getKeyDispatcherState(self) -> Any: + """获取按键调度状态""" + return None + + +# ═══════════════════════════════════════════════════════════════ +# InputMethodImpl +# ═══════════════════════════════════════════════════════════════ + +class InputMethodImpl(InputMethod): + """InputMethodImpl — 输入法实现""" + + def __init__(self): + super().__init__() + self._session: Optional[InputMethodSession] = None + + def createSession(self, callback: Any) -> Optional["InputMethodSession"]: + self._session = InputMethodSessionImpl() + return self._session + + def startInput(self, input_connection: Any, editor_info: Any): + sys.stderr.write("[InputMethodImpl] startInput\n") + + def restartInput(self, input_connection: Any, editor_info: Any): + sys.stderr.write("[InputMethodImpl] restartInput\n") + + def dispatchKeyEvent(self, event: Any): + sys.stderr.write("[InputMethodImpl] dispatchKeyEvent\n") + + def dispatchTrackballEvent(self, event: Any): + pass + + def dispatchGenericMotionEvent(self, event: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# InputMethodSessionImpl +# ═══════════════════════════════════════════════════════════════ + +class InputMethodSessionImpl(InputMethodSession): + """InputMethodSessionImpl — 输入法会话实现""" + + def __init__(self): + super().__init__() + + def finishInput(self): + sys.stderr.write("[InputMethodSessionImpl] finishInput\n") + + def updateSelection(self, old_sel_start: int, old_sel_end: int, + new_sel_start: int, new_sel_end: int, + candidates_start: int, candidates_end: int): + pass + + def displayCompletions(self, completions: List["CompletionInfo"]): + pass + + def dispatchKeyEvent(self, key_code: int, event: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# InputMethodService +# ═══════════════════════════════════════════════════════════════ + +class InputMethodService(AbstractInputMethodService): + """InputMethodService — 输入法服务""" + + SHOW_EXPLICIT = 0x0001 + SHOW_IMPLICIT = 0x0002 + + def __init__(self): + super().__init__() + self._input_view: Optional[Any] = None + self._candidates_view: Optional[Any] = None + self._input_connection: Optional[Any] = None + self._input_editor_info: Optional["EditorInfo"] = None + self._input_binding: Optional["InputBinding"] = None + self._input_view_shown: bool = False + self._fullscreen_mode: bool = False + self._extract_to_fullscreen_mode: bool = False + self._is_fullscreen: bool = False + self._keyboard: Optional["Keyboard"] = None + self._settings_observer: Any = None + self._candidates_view_shown: bool = False + self._show_input_requested: bool = False + self._token: Any = None + self._window_visible: bool = False + self._input_started: bool = False + + def onCreate(self): + """创建服务""" + sys.stderr.write("[InputMethodService] onCreate\n") + + def onCreateInputView(self) -> Optional[Any]: + """创建输入视图""" + return None + + def onCreateCandidatesView(self) -> Optional[Any]: + """创建候选视图""" + return None + + def onStartInputView(self, info: "EditorInfo", restarting: bool): + """开始输入视图""" + pass + + def onFinishInputView(self, finishing_input: bool): + """结束输入视图""" + pass + + def onStartInput(self, attribute: "EditorInfo", restarting: bool): + """开始输入""" + self._input_editor_info = attribute + self._input_started = True + + def onFinishInput(self): + """结束输入""" + self._input_started = False + + def onBindInput(self, binding: "InputBinding"): + """绑定输入""" + self._input_binding = binding + + def onUnbindInput(self) -> bool: + """解绑输入""" + self._input_binding = None + return False + + def onDestroy(self): + """销毁""" + self._input_view = None + self._candidates_view = None + self._input_connection = None + + def showWindow(self, show_flags: int): + """显示窗口""" + self._window_visible = True + sys.stderr.write(f"[InputMethodService] showWindow: flags={show_flags}\n") + + def hideWindow(self): + """隐藏窗口""" + self._window_visible = False + + def setInputView(self, view: Any): + """设置输入视图""" + self._input_view = view + + def setCandidatesView(self, view: Any): + """设置候选视图""" + self._candidates_view = view + + def updateInputViewShown(self): + """更新输入视图显示状态""" + pass + + def updateFullscreenMode(self): + """更新全屏模式""" + pass + + def updateExtractToFullscreenMode(self): + """更新提取全屏模式""" + pass + + def requestHideSelf(self, flags: int): + """请求隐藏自身""" + sys.stderr.write(f"[InputMethodService] requestHideSelf: flags={flags}\n") + + def switchInputMethod(self, id: str): + """切换输入法""" + sys.stderr.write(f"[InputMethodService] switchInputMethod: id={id}\n") + + def sendDefaultEditorAction(self, from_enter_key: bool): + """发送默认编辑器动作""" + pass + + def sendDownUpKeyEvents(self, key_event_code: int): + """发送按键按下和释放事件""" + pass + + def getCurrentInputBinding(self) -> Optional["InputBinding"]: + """获取当前输入绑定""" + return self._input_binding + + def getCurrentInputEditorInfo(self) -> Optional["EditorInfo"]: + """获取当前编辑器信息""" + return self._input_editor_info + + def getCurrentInputConnection(self) -> Optional["InputConnection"]: + """获取当前输入连接""" + return self._input_connection + + def getTextForImeAction(self, ime_options: int) -> str: + """获取IME动作的文本""" + action_map = { + 0x0001: "None", + 0x0002: "Go", + 0x0003: "Search", + 0x0004: "Send", + 0x0005: "Next", + 0x0006: "Done", + 0x0007: "Previous", + } + action = ime_options & 0x000000FF + return action_map.get(action, "Unknown") + + def doStartInput(self, input_connection: Any, editor_info: "EditorInfo", restarting: bool): + """执行启动输入""" + self._input_connection = input_connection + self._input_editor_info = editor_info + self.onStartInput(editor_info, restarting) + if self._window_visible: + self.onStartInputView(editor_info, restarting) + + def doFinishInput(self): + """执行结束输入""" + self.onFinishInput() + self.onFinishInputView(True) + + def onEvaluateInputViewShown(self) -> bool: + """评估是否显示输入视图""" + return self._input_view is not None + + def onEvaluateFullscreenMode(self) -> bool: + """评估是否全屏模式""" + return self._fullscreen_mode + + def onConfigureWindow(self, win: Any, is_fullscreen: bool, is_candidates: bool): + """配置窗口""" + pass + + def isInputViewShown(self) -> bool: + """输入视图是否已显示""" + return self._input_view_shown + + def updateWindow(self, show_input: bool = False): + """更新窗口""" + pass + + def setExtractView(self, view: Any): + """设置提取视图""" + pass + + def getExtractView(self) -> Optional[Any]: + """获取提取视图""" + return None + + def getInputView(self) -> Optional[Any]: + """获取输入视图""" + return self._input_view + + def getCandidatesView(self) -> Optional[Any]: + """获取候选视图""" + return self._candidates_view + + def isFullscreenMode(self) -> bool: + """是否全屏模式""" + return self._is_fullscreen + + def onDisplayCompletions(self, completions: List["CompletionInfo"]): + """显示补全""" + pass + + def onUpdateSelection(self, old_sel_start: int, old_sel_end: int, + new_sel_start: int, new_sel_end: int, + candidates_start: int, candidates_end: int): + """更新选区""" + pass + + def onViewClicked(self, focus_changed: bool): + """视图点击""" + pass + + def onWindowShown(self): + """窗口显示""" + self._window_visible = True + + def onWindowHidden(self): + """窗口隐藏""" + self._window_visible = False + + def setKeyboard(self, keyboard: "Keyboard"): + """设置键盘""" + self._keyboard = keyboard + + def getKeyboard(self) -> Optional["Keyboard"]: + """获取键盘""" + return self._keyboard + + +# ═══════════════════════════════════════════════════════════════ +# Key +# ═══════════════════════════════════════════════════════════════ + +class Key(Stub): + """Key — 键盘按键""" + + def __init__(self, row: "KeyboardRow" = None): + super().__init__() + self.codes: List[int] = [] + self.label: Optional[str] = None + self.icon: Any = None + self.sticky: bool = False + self.modifier: bool = False + self.repeatable: bool = False + self.edgeFlags: int = 0 + self.popupCharacters: Optional[str] = None + self.popupResId: int = 0 + self._x: int = 0 + self._y: int = 0 + self._width: int = 0 + self._height: int = 0 + self._gap: int = 0 + self._row: Optional["KeyboardRow"] = row + self._pressed: bool = False + self._on: bool = False + + def getCode(self) -> int: + """获取按键码""" + return self.codes[0] if self.codes else 0 + + def isPressed(self) -> bool: + """是否按下""" + return self._pressed + + def setPressed(self, pressed: bool): + """设置按下状态""" + self._pressed = pressed + + def isOn(self) -> bool: + """是否开启""" + return self._on + + def setOn(self, on_state: bool): + """设置开启状态""" + self._on = on_state + + def squaredDistance(self, x: int, y: int) -> int: + """计算距离平方""" + center_x = self._x + self._width // 2 + center_y = self._y + self._height // 2 + return (x - center_x) ** 2 + (y - center_y) ** 2 + + def hitTest(self, x: int, y: int) -> bool: + """命中测试""" + return (self._x <= x <= self._x + self._width and + self._y <= y <= self._y + self._height) + + def getX(self) -> int: + return self._x + + def getY(self) -> int: + return self._y + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def toString(self) -> str: + return f"Key(code={self.getCode()}, label={self.label})" + + +# ═══════════════════════════════════════════════════════════════ +# KeyboardRow +# ═══════════════════════════════════════════════════════════════ + +class KeyboardRow(Stub): + """KeyboardRow — 键盘行""" + + def __init__(self, keyboard: "Keyboard" = None): + super().__init__() + self._keys: List[Key] = [] + self._keyboard: Optional["Keyboard"] = keyboard + self._default_width: int = 0 + self._default_height: int = 0 + self._row_edge_flags: int = 0 + self._mode: str = "" + + def getKeys(self) -> List[Key]: + return list(self._keys) + + def addKey(self, key: Key): + self._keys.append(key) + + def getKeyboard(self) -> Optional["Keyboard"]: + return self._keyboard + + def getDefaultWidth(self) -> int: + return self._default_width + + def getDefaultHeight(self) -> int: + return self._default_height + + def toString(self) -> str: + return f"KeyboardRow(keys={len(self._keys)})" + + +# ═══════════════════════════════════════════════════════════════ +# Keyboard +# ═══════════════════════════════════════════════════════════════ + +class Keyboard(Stub): + """Keyboard — 键盘布局""" + + def __init__(self, context: Any = None, xml_layout_res_id: int = 0): + super().__init__() + self._keys: List[Key] = [] + self._rows: List[KeyboardRow] = [] + self._modifier_keys: List[Key] = [] + self._height: int = 0 + self._min_width: int = 0 + self._key_width: int = 0 + self._key_height: int = 0 + self._horizontal_gap: int = 0 + self._vertical_gap: int = 0 + self._shift_keys: List[Key] = [] + self._alt_keys: List[Key] = [] + + def getKeys(self) -> List[Key]: + return list(self._keys) + + def getHeight(self) -> int: + return self._height + + def getMinWidth(self) -> int: + return self._min_width + + def getKeyWidth(self) -> int: + return self._key_width + + def getKeyHeight(self) -> int: + return self._key_height + + def getHorizontalGap(self) -> int: + return self._horizontal_gap + + def getVerticalGap(self) -> int: + return self._vertical_gap + + def getModifierKeys(self) -> List[Key]: + return list(self._modifier_keys) + + def getNearestKeys(self, x: int, y: int) -> List[Key]: + """获取最近的按键""" + nearest: List[Key] = [] + for key in self._keys: + if key.hitTest(x, y): + nearest.append(key) + return nearest + + def setKeys(self, keys: List[Key]): + """设置按键列表""" + self._keys = keys + + def getRows(self) -> List[KeyboardRow]: + return list(self._rows) + + def setShifted(self, shift_state: bool): + """设置Shift状态""" + pass + + def isShifted(self) -> bool: + """是否Shift状态""" + return False + + def getShiftKeyIndex(self) -> int: + """获取Shift按键索引""" + for i, key in enumerate(self._keys): + if key.modifier: + return i + return -1 + + def setVerticalGap(self, gap: int): + self._vertical_gap = gap + + def setHorizontalGap(self, gap: int): + self._horizontal_gap = gap + + def setKeyWidth(self, width: int): + self._key_width = width + + def setKeyHeight(self, height: int): + self._key_height = height + + def toString(self) -> str: + return f"Keyboard(keys={len(self._keys)}, rows={len(self._rows)})" + + +# ═══════════════════════════════════════════════════════════════ +# KeyboardView +# ═══════════════════════════════════════════════════════════════ + +class KeyboardView(Stub): + """KeyboardView — 键盘视图""" + + class OnKeyboardActionListener(Stub): + """OnKeyboardActionListener — 键盘动作监听器""" + + def onKey(self, primary_code: int): + pass + + def onPress(self, primary_code: int): + pass + + def onRelease(self, primary_code: int): + pass + + def onText(self, text: str): + pass + + def swipeLeft(self): + pass + + def swipeRight(self): + pass + + def swipeDown(self): + pass + + def swipeUp(self): + pass + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__() + self._keyboard: Optional[Keyboard] = None + self._on_keyboard_action_listener: Optional["KeyboardView.OnKeyboardActionListener"] = None + self._preview_enabled: bool = True + self._proximity_correction_enabled: bool = True + self._vertical_correction: int = 0 + self._key_text_color: int = 0xFF000000 + self._key_text_size: int = 20 + self._shadow_color: int = 0x00000000 + self._shadow_radius: float = 0.0 + self._background_color: int = 0xFF888888 + + def setKeyboard(self, keyboard: Keyboard): + self._keyboard = keyboard + + def getKeyboard(self) -> Optional[Keyboard]: + return self._keyboard + + def onDraw(self, canvas: Any): + """绘制键盘""" + pass + + def setOnKeyboardActionListener(self, listener: "KeyboardView.OnKeyboardActionListener"): + self._on_keyboard_action_listener = listener + + def getOnKeyboardActionListener(self) -> Optional["KeyboardView.OnKeyboardActionListener"]: + return self._on_keyboard_action_listener + + def setPreviewEnabled(self, preview_enabled: bool): + self._preview_enabled = preview_enabled + + def isPreviewEnabled(self) -> bool: + return self._preview_enabled + + def setProximityCorrectionEnabled(self, enabled: bool): + self._proximity_correction_enabled = enabled + + def isProximityCorrectionEnabled(self) -> bool: + return self._proximity_correction_enabled + + def setVerticalCorrection(self, vertical_correction: int): + self._vertical_correction = vertical_correction + + def getVerticalCorrection(self) -> int: + return self._vertical_correction + + def setKeyTextColor(self, color: int): + self._key_text_color = color + + def setKeyTextSize(self, size: int): + self._key_text_size = size + + def setShadowColor(self, color: int): + self._shadow_color = color + + def setShadowRadius(self, radius: float): + self._shadow_radius = radius + + def setBackgroundColor(self, color: int): + self._background_color = color + + def invalidateAllKeys(self): + """使所有按键失效""" + pass + + def invalidateKey(self, key_index: int): + """使指定按键失效""" + pass + + def onClick(self, key: Key): + """点击按键""" + if self._on_keyboard_action_listener and key.codes: + self._on_keyboard_action_listener.onKey(key.codes[0]) + + def onLongPress(self, key: Key) -> bool: + """长按按键""" + if key.popupCharacters: + return True + return False + + def closing(self): + """关闭""" + pass + + def handleBack(self) -> bool: + """处理返回键""" + return False + + def setKeys(self, keys: List[Key]): + """设置按键""" + if self._keyboard: + self._keyboard.setKeys(keys) + + +# ═══════════════════════════════════════════════════════════════ +# KeyEventCallback +# ═══════════════════════════════════════════════════════════════ + +class KeyEventCallback(Stub): + """KeyEventCallback — 按键事件回调""" + + def onKeyDown(self, key_code: int, event: Any) -> bool: + """按键按下""" + return False + + def onKeyLongPress(self, key_code: int, event: Any) -> bool: + """按键长按""" + return False + + def onKeyUp(self, key_code: int, event: Any) -> bool: + """按键释放""" + return False + + def onKeyMultiple(self, key_code: int, count: int, event: Any) -> bool: + """按键重复""" + return False + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "InputMethodService", + "InputMethodImpl", + "InputMethodSessionImpl", + "AbstractInputMethodService", + "InputMethod", + "InputMethodSession", + "Keyboard", + "KeyboardView", + "Key", + "KeyEventCallback", + "KeyboardRow", +] \ No newline at end of file diff --git a/android_pyapi/android/location.py b/android_pyapi/android/location.py new file mode 100644 index 0000000..5ad2073 --- /dev/null +++ b/android_pyapi/android/location.py @@ -0,0 +1,1055 @@ +""" +Android Location 组件 — Location, LocationManager, Geocoder 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import math +import threading +from typing import Any, Dict, List, Optional, Set, Tuple, Union +from datetime import datetime + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# Location +# ═══════════════════════════════════════════════════════════════ + +class Location(Stub): + """Location — 地理位置信息""" + + FORMAT_DEGREES = 0 + FORMAT_MINUTES = 1 + FORMAT_SECONDS = 2 + + def __init__(self, provider: str = ""): + super().__init__() + self._provider = provider + self._latitude: float = 0.0 + self._longitude: float = 0.0 + self._accuracy: float = 0.0 + self._time: int = 0 + self._elapsed_realtime_nanos: int = 0 + self._altitude: float = 0.0 + self._speed: float = 0.0 + self._bearing: float = 0.0 + self._extras: Optional[Dict[str, Any]] = None + + def setLatitude(self, latitude: float): + self._latitude = latitude + + def getLatitude(self) -> float: + return self._latitude + + def setLongitude(self, longitude: float): + self._longitude = longitude + + def getLongitude(self) -> float: + return self._longitude + + def setAccuracy(self, accuracy: float): + self._accuracy = accuracy + + def getAccuracy(self) -> float: + return self._accuracy + + def setTime(self, time: int): + self._time = time + + def getTime(self) -> int: + return self._time + + def setElapsedRealtimeNanos(self, nanos: int): + self._elapsed_realtime_nanos = nanos + + def getElapsedRealtimeNanos(self) -> int: + return self._elapsed_realtime_nanos + + def setAltitude(self, altitude: float): + self._altitude = altitude + + def getAltitude(self) -> float: + return self._altitude + + def setSpeed(self, speed: float): + self._speed = speed + + def getSpeed(self) -> float: + return self._speed + + def setBearing(self, bearing: float): + self._bearing = bearing + + def getBearing(self) -> float: + return self._bearing + + def setProvider(self, provider: str): + self._provider = provider + + def getProvider(self) -> str: + return self._provider + + def distanceTo(self, dest: "Location") -> float: + R = 6371000 + lat1 = math.radians(self._latitude) + lat2 = math.radians(dest._latitude) + dlat = math.radians(dest._latitude - self._latitude) + dlon = math.radians(dest._longitude - self._longitude) + a = (math.sin(dlat / 2) ** 2 + + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + return R * c + + def bearingTo(self, dest: "Location") -> float: + lat1 = math.radians(self._latitude) + lat2 = math.radians(dest._latitude) + dlon = math.radians(dest._longitude - self._longitude) + y = math.sin(dlon) * math.cos(lat2) + x = (math.cos(lat1) * math.sin(lat2) - + math.sin(lat1) * math.cos(lat2) * math.cos(dlon)) + return (math.degrees(math.atan2(y, x)) + 360) % 360 + + @staticmethod + def convert(coordinate: Union[str, float], output_type: int = FORMAT_DEGREES) -> Union[str, float]: + if isinstance(coordinate, str): + return Location._parse_coordinate(coordinate) + return Location._format_coordinate(coordinate, output_type) + + @staticmethod + def _parse_coordinate(coord_str: str) -> float: + coord_str = coord_str.strip() + try: + return float(coord_str) + except ValueError: + pass + parts = coord_str.replace(",", ".").split() + if len(parts) >= 3: + try: + d = float(parts[0]) + m = float(parts[1]) + s = float(parts[2].rstrip("NSEW")) + result = d + m / 60.0 + s / 3600.0 + if coord_str.rstrip()[-1:] in ("S", "W"): + result = -result + return result + except (ValueError, IndexError): + return 0.0 + return 0.0 + + @staticmethod + def _format_coordinate(coord: float, output_type: int) -> str: + if output_type == Location.FORMAT_DEGREES: + return f"{coord:.6f}" + elif output_type == Location.FORMAT_MINUTES: + deg = int(coord) + minutes = (coord - deg) * 60 + return f"{deg}:{minutes:.3f}" + else: + deg = int(coord) + minutes = (coord - deg) * 60 + int_min = int(minutes) + secs = (minutes - int_min) * 60 + return f"{deg}:{int_min}:{secs:.2f}" + + def getExtras(self) -> Optional[Dict[str, Any]]: + return self._extras + + def setExtras(self, extras: Dict[str, Any]): + self._extras = extras + + def toString(self) -> str: + return (f"Location[provider={self._provider}, " + f"{self._latitude:.6f},{self._longitude:.6f}, " + f"acc={self._accuracy:.1f}]") + + def __repr__(self): + return self.toString() + + +# ═══════════════════════════════════════════════════════════════ +# Criteria +# ═══════════════════════════════════════════════════════════════ + +class Criteria(Stub): + """Criteria — 位置提供者筛选条件""" + + ACCURACY_FINE = 1 + ACCURACY_COARSE = 2 + POWER_LOW = 1 + POWER_MEDIUM = 2 + POWER_HIGH = 3 + NO_REQUIREMENT = 0 + + def __init__(self): + super().__init__() + self._accuracy = self.NO_REQUIREMENT + self._power_requirement = self.NO_REQUIREMENT + self._altitude_required = False + self._bearing_required = False + self._speed_required = False + self._cost_allowed = False + + def setAccuracy(self, accuracy: int): + self._accuracy = accuracy + + def getAccuracy(self) -> int: + return self._accuracy + + def setPowerRequirement(self, power: int): + self._power_requirement = power + + def getPowerRequirement(self) -> int: + return self._power_requirement + + def setAltitudeRequired(self, required: bool): + self._altitude_required = required + + def isAltitudeRequired(self) -> bool: + return self._altitude_required + + def setBearingRequired(self, required: bool): + self._bearing_required = required + + def isBearingRequired(self) -> bool: + return self._bearing_required + + def setSpeedRequired(self, required: bool): + self._speed_required = required + + def isSpeedRequired(self) -> bool: + return self._speed_required + + def setCostAllowed(self, allowed: bool): + self._cost_allowed = allowed + + def isCostAllowed(self) -> bool: + return self._cost_allowed + + def toString(self) -> str: + return f"Criteria(acc={self._accuracy}, power={self._power_requirement})" + + +# ═══════════════════════════════════════════════════════════════ +# LocationProvider +# ═══════════════════════════════════════════════════════════════ + +class LocationProvider(Stub): + """LocationProvider — 位置提供者""" + + AVAILABLE = 2 + TEMPORARILY_UNAVAILABLE = 1 + OUT_OF_SERVICE = 0 + + def __init__(self, name: str = ""): + super().__init__() + self._name = name + self._accuracy = Criteria.NO_REQUIREMENT + self._power_requirement = Criteria.NO_REQUIREMENT + self._has_monetary_cost = False + self._requires_cell = False + self._requires_network = False + self._requires_satellite = False + self._supports_altitude = False + self._supports_bearing = False + self._supports_speed = False + + def getName(self) -> str: + return self._name + + def getAccuracy(self) -> int: + return self._accuracy + + def getPowerRequirement(self) -> int: + return self._power_requirement + + def hasMonetaryCost(self) -> bool: + return self._has_monetary_cost + + def requiresCell(self) -> bool: + return self._requires_cell + + def requiresNetwork(self) -> bool: + return self._requires_network + + def requiresSatellite(self) -> bool: + return self._requires_satellite + + def supportsAltitude(self) -> bool: + return self._supports_altitude + + def supportsBearing(self) -> bool: + return self._supports_bearing + + def supportsSpeed(self) -> bool: + return self._supports_speed + + def getRequirements(self) -> int: + req = 0 + if self._requires_cell: + req |= 1 + if self._requires_network: + req |= 2 + if self._requires_satellite: + req |= 4 + return req + + def toString(self) -> str: + return f"LocationProvider(name={self._name})" + + +# ═══════════════════════════════════════════════════════════════ +# LocationListener +# ═══════════════════════════════════════════════════════════════ + +class LocationListener(Stub): + """LocationListener — 位置变化监听器""" + + def onLocationChanged(self, location: Location): + pass + + def onStatusChanged(self, provider: str, status: int, extras: Any): + pass + + def onProviderEnabled(self, provider: str): + pass + + def onProviderDisabled(self, provider: str): + pass + + +# ═══════════════════════════════════════════════════════════════ +# GpsSatellite +# ═══════════════════════════════════════════════════════════════ + +class GpsSatellite(Stub): + """GpsSatellite — GPS 卫星信息""" + + def __init__(self): + super().__init__() + self._prn: int = 0 + self._snr: float = 0.0 + self._elevation: float = 0.0 + self._azimuth: float = 0.0 + self._has_almanac: bool = False + self._has_ephemeris: bool = False + self._used_in_fix: bool = False + + def getPrn(self) -> int: + return self._prn + + def getSnr(self) -> float: + return self._snr + + def getElevation(self) -> float: + return self._elevation + + def getAzimuth(self) -> float: + return self._azimuth + + def hasAlmanac(self) -> bool: + return self._has_almanac + + def hasEphemeris(self) -> bool: + return self._has_ephemeris + + def usedInFix(self) -> bool: + return self._used_in_fix + + def toString(self) -> str: + return f"GpsSatellite(prn={self._prn}, snr={self._snr})" + + +# ═══════════════════════════════════════════════════════════════ +# GpsStatus +# ═══════════════════════════════════════════════════════════════ + +class GpsStatus(Stub): + """GpsStatus — GPS 状态""" + + GPS_EVENT_STARTED = 1 + GPS_EVENT_STOPPED = 2 + GPS_EVENT_FIRST_FIX = 3 + GPS_EVENT_SATELLITE_STATUS = 4 + + def __init__(self): + super().__init__() + self._satellites: List[GpsSatellite] = [] + self._max_satellites: int = 32 + self._time_to_first_fix: int = 0 + + def getSatellites(self) -> List[GpsSatellite]: + return list(self._satellites) + + def getMaxSatellites(self) -> int: + return self._max_satellites + + def getTimeToFirstFix(self) -> int: + return self._time_to_first_fix + + def setTimeToFirstFix(self, ttf: int): + self._time_to_first_fix = ttf + + +class GpsStatusListener(Stub): + """GpsStatusListener — GPS 状态监听器""" + + def onGpsStatusChanged(self, event: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# GnssClock / GnssMeasurement / GnssMeasurementsEvent / GnssNavigationMessage +# ═══════════════════════════════════════════════════════════════ + +class GnssClock(Stub): + """GnssClock — GNSS 时钟""" + + def __init__(self): + super().__init__() + self._time_nanos: int = 0 + self._time_uncertainty_nanos: float = 0.0 + self._leap_second: int = 0 + self._full_bias_nanos: int = 0 + self._bias_nanos: float = 0.0 + self._bias_uncertainty_nanos: float = 0.0 + self._drift_nanos_per_sec: float = 0.0 + self._drift_uncertainty_nanos_per_sec: float = 0.0 + self._hardware_clock_discontinuity_count: int = 0 + self._has_leap_second: bool = False + self._has_full_bias_nanos: bool = False + self._has_bias_nanos: bool = False + self._has_bias_uncertainty_nanos: bool = False + self._has_drift_nanos_per_sec: bool = False + self._has_drift_uncertainty_nanos_per_sec: bool = False + + def getTimeNanos(self) -> int: + return self._time_nanos + + def getTimeUncertaintyNanos(self) -> float: + return self._time_uncertainty_nanos + + def getLeapSecond(self) -> int: + return self._leap_second + + def getFullBiasNanos(self) -> int: + return self._full_bias_nanos + + def getBiasNanos(self) -> float: + return self._bias_nanos + + def getBiasUncertaintyNanos(self) -> float: + return self._bias_uncertainty_nanos + + def getDriftNanosPerSec(self) -> float: + return self._drift_nanos_per_sec + + def getDriftUncertaintyNanosPerSec(self) -> float: + return self._drift_uncertainty_nanos_per_sec + + def getHardwareClockDiscontinuityCount(self) -> int: + return self._hardware_clock_discontinuity_count + + +class GnssMeasurement(Stub): + """GnssMeasurement — GNSS 测量值""" + + STATE_UNKNOWN = 0 + STATE_CODE_LOCK = 1 + STATE_BIT_SYNC = 2 + STATE_SUBFRAME_SYNC = 3 + STATE_TOW_DECODED = 4 + STATE_MSEC_AMBIGUOUS = 5 + + def __init__(self): + super().__init__() + self._svid: int = 0 + self._constellation_type: int = 0 + self._time_offset_nanos: float = 0.0 + self._state: int = 0 + self._received_gps_tow_nanos: int = 0 + self._received_gps_tow_uncertainty_nanos: int = 0 + self._carrier_frequency_hz: float = 0.0 + self._carrier_phase: float = 0.0 + self._carrier_phase_uncertainty: float = 0.0 + self._multipath_indicator: int = 0 + self._snr_in_db: float = 0.0 + self._constellation_type_val: int = 0 + + def getSvid(self) -> int: + return self._svid + + def getConstellationType(self) -> int: + return self._constellation_type + + def getTimeOffsetNanos(self) -> float: + return self._time_offset_nanos + + def getState(self) -> int: + return self._state + + def getReceivedGpsTowNanos(self) -> int: + return self._received_gps_tow_nanos + + def getReceivedGpsTowUncertaintyNanos(self) -> int: + return self._received_gps_tow_uncertainty_nanos + + def getCarrierFrequencyHz(self) -> float: + return self._carrier_frequency_hz + + def getCarrierPhase(self) -> float: + return self._carrier_phase + + def getCarrierPhaseUncertainty(self) -> float: + return self._carrier_phase_uncertainty + + def getMultipathIndicator(self) -> int: + return self._multipath_indicator + + def getSnrInDb(self) -> float: + return self._snr_in_db + + +class GnssMeasurementsEvent(Stub): + """GnssMeasurementsEvent — GNSS 测量事件""" + + def __init__(self, clock: Optional[GnssClock] = None, + measurements: Optional[List[GnssMeasurement]] = None): + super().__init__() + self._clock = clock or GnssClock() + self._measurements = measurements or [] + + def getClock(self) -> GnssClock: + return self._clock + + def getMeasurements(self) -> List[GnssMeasurement]: + return list(self._measurements) + + +class GnssNavigationMessage(Stub): + """GnssNavigationMessage — GNSS 导航消息""" + + STATUS_PARITY_PASSED = 1 + STATUS_PARITY_REBUILT = 2 + TYPE_GPS_L1CA = 256 + TYPE_GPS_L2CA = 512 + TYPE_GPS_L5 = 768 + TYPE_GLO_L1CA = 1024 + TYPE_GLO_L2CA = 1280 + TYPE_BDS_D1 = 1792 + TYPE_BDS_D2 = 2048 + TYPE_GAL_I = 2560 + TYPE_GAL_F = 2816 + TYPE_QZS_L1CA = 3072 + TYPE_QZS_L5 = 3328 + + def __init__(self): + super().__init__() + self._type: int = 0 + self._svid: int = 0 + self._message_id: int = 0 + self._submessage_id: int = 0 + self._data: bytes = b"" + self._status: int = 0 + + def getType(self) -> int: + return self._type + + def getSvid(self) -> int: + return self._svid + + def getMessageId(self) -> int: + return self._message_id + + def getSubmessageId(self) -> int: + return self._submessage_id + + def getData(self) -> bytes: + return self._data + + def getStatus(self) -> int: + return self._status + + +# ═══════════════════════════════════════════════════════════════ +# GnssStatus +# ═══════════════════════════════════════════════════════════════ + +class GnssStatus(Stub): + """GnssStatus — GNSS 卫星状态""" + + GNSS_CONSTELLATION_GPS = 1 + GNSS_CONSTELLATION_SBAS = 2 + GNSS_CONSTELLATION_GLONASS = 3 + CONSTELLATION_QZSS = 4 + CONSTELLATION_BEIDOU = 5 + CONSTELLATION_GALILEO = 6 + CONSTELLATION_IRNSS = 7 + + def __init__(self): + super().__init__() + self._satellites: List[Dict[str, Any]] = [] + + def getSatelliteCount(self) -> int: + return len(self._satellites) + + def getSvid(self, index: int) -> int: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("svid", 0) + return 0 + + def getConstellationType(self, index: int) -> int: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("constellation", 0) + return 0 + + def getCarrierFrequencyHz(self, index: int) -> float: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("carrier_freq", 0.0) + return 0.0 + + def getBasebandCn0DbHz(self, index: int) -> float: + return 0.0 + + def getCn0DbHz(self, index: int) -> float: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("cn0", 0.0) + return 0.0 + + def getAzimuthDegrees(self, index: int) -> float: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("azimuth", 0.0) + return 0.0 + + def getElevationDegrees(self, index: int) -> float: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("elevation", 0.0) + return 0.0 + + def hasAlmanac(self, index: int) -> bool: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("has_almanac", False) + return False + + def hasEphemeris(self, index: int) -> bool: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("has_ephemeris", False) + return False + + def usedInFix(self, index: int) -> bool: + if 0 <= index < len(self._satellites): + return self._satellites[index].get("used_in_fix", False) + return False + + +# ═══════════════════════════════════════════════════════════════ +# LocationRequest +# ═══════════════════════════════════════════════════════════════ + +class LocationRequest(Stub): + """LocationRequest — 位置请求""" + + QUALITY_HIGH_ACCURACY = 100 + QUALITY_BALANCED_POWER_ACCURACY = 102 + QUALITY_LOW_POWER = 104 + QUALITY_PASSIVE = 105 + + PRIORITY_HIGH_ACCURACY = 100 + PRIORITY_BALANCED_POWER_ACCURACY = 102 + PRIORITY_LOW_POWER = 104 + PRIORITY_NO_POWER = 105 + + def __init__(self): + super().__init__() + self._quality: int = self.QUALITY_BALANCED_POWER_ACCURACY + self._interval: int = 60000 + self._fastest_interval: int = 30000 + self._smallest_displacement: float = 0.0 + self._max_wait_time: int = 0 + self._duration_millis: int = 0 + self._max_updates: int = 0 # 0 means unlimited + self._priority: int = self.PRIORITY_BALANCED_POWER_ACCURACY + + def setQuality(self, quality: int): + self._quality = quality + + def getQuality(self) -> int: + return self._quality + + def setInterval(self, interval: int): + self._interval = interval + + def getInterval(self) -> int: + return self._interval + + def setFastestInterval(self, interval: int): + self._fastest_interval = interval + + def getFastestInterval(self) -> int: + return self._fastest_interval + + def setSmallestDisplacement(self, displacement: float): + self._smallest_displacement = displacement + + def getSmallestDisplacement(self) -> float: + return self._smallest_displacement + + def setMaxWaitTime(self, time: int): + self._max_wait_time = time + + def getMaxWaitTime(self) -> int: + return self._max_wait_time + + def setDurationMillis(self, duration: int): + self._duration_millis = duration + + def getDurationMillis(self) -> int: + return self._duration_millis + + def setMaxUpdates(self, updates: int): + self._max_updates = updates + + def getMaxUpdates(self) -> int: + return self._max_updates + + def setPriority(self, priority: int): + self._priority = priority + + def getPriority(self) -> int: + return self._priority + + def toString(self) -> str: + return f"LocationRequest(interval={self._interval}, priority={self._priority})" + + +# ═══════════════════════════════════════════════════════════════ +# LocationProviderProperties +# ═══════════════════════════════════════════════════════════════ + +class LocationProviderProperties(Stub): + """LocationProviderProperties — 提供者属性""" + + def __init__(self, requires_network: bool = False, + requires_satellite: bool = False, + requires_cell: bool = False, + has_monetary_cost: bool = False, + supports_altitude: bool = False, + supports_speed: bool = False, + supports_bearing: bool = False, + power_requirement: int = Criteria.NO_REQUIREMENT, + accuracy: int = Criteria.NO_REQUIREMENT): + super().__init__() + self.requiresNetwork = requires_network + self.requiresSatellite = requires_satellite + self.requiresCell = requires_cell + self.hasMonetaryCost = has_monetary_cost + self.supportsAltitude = supports_altitude + self.supportsSpeed = supports_speed + self.supportsBearing = supports_bearing + self.powerRequirement = power_requirement + self.accuracy = accuracy + + +# ═══════════════════════════════════════════════════════════════ +# SettingInjectorService +# ═══════════════════════════════════════════════════════════════ + +class SettingInjectorService(Stub): + """SettingInjectorService — 设置注入服务""" + + ACTION_INJECTED_SETTING = "android.location.InjectedSetting" + + def __init__(self, name: str = ""): + super().__init__() + self._name = name + + def onGetSummary(self) -> str: + return self._name + + def onGetEnabled(self) -> bool: + return True + + def toString(self) -> str: + return f"SettingInjectorService(name={self._name})" + + +# ═══════════════════════════════════════════════════════════════ +# Address +# ═══════════════════════════════════════════════════════════════ + +class Address(Stub): + """Address — 地理地址""" + + def __init__(self, locale: Any = None): + super().__init__() + self._address_lines: List[str] = [] + self._feature_name: str = "" + self._latitude: float = 0.0 + self._longitude: float = 0.0 + self._has_lat_lon: bool = False + self._admin_area: str = "" + self._sub_admin_area: str = "" + self._locality: str = "" + self._sub_locality: str = "" + self._premises: str = "" + self._thoroughfare: str = "" + self._sub_thoroughfare: str = "" + self._postal_code: str = "" + self._country_name: str = "" + self._country_code: str = "" + self._phone: str = "" + self._url: str = "" + self._extras: Optional[Dict[str, Any]] = None + + def getAddressLine(self, index: int) -> Optional[str]: + if 0 <= index < len(self._address_lines): + return self._address_lines[index] + return None + + def getMaxAddressLineIndex(self) -> int: + return max(0, len(self._address_lines) - 1) + + def getAdminArea(self) -> Optional[str]: + return self._admin_area or None + + def getSubAdminArea(self) -> Optional[str]: + return self._sub_admin_area or None + + def getLocality(self) -> Optional[str]: + return self._locality or None + + def getSubLocality(self) -> Optional[str]: + return self._sub_locality or None + + def getPremises(self) -> Optional[str]: + return self._premises or None + + def getThoroughfare(self) -> Optional[str]: + return self._thoroughfare or None + + def getSubThoroughfare(self) -> Optional[str]: + return self._sub_thoroughfare or None + + def getPostalCode(self) -> Optional[str]: + return self._postal_code or None + + def getCountryName(self) -> Optional[str]: + return self._country_name or None + + def getCountryCode(self) -> Optional[str]: + return self._country_code or None + + def getFeatureName(self) -> Optional[str]: + return self._feature_name or None + + def getLatitude(self) -> float: + return self._latitude + + def getLongitude(self) -> float: + return self._longitude + + def getPhone(self) -> Optional[str]: + return self._phone or None + + def getUrl(self) -> Optional[str]: + return self._url or None + + def setAddressLine(self, index: int, line: str): + while len(self._address_lines) <= index: + self._address_lines.append("") + self._address_lines[index] = line + + def setFeatureName(self, name: str): + self._feature_name = name + + def setLatitude(self, latitude: float): + self._latitude = latitude + self._has_lat_lon = True + + def setLongitude(self, longitude: float): + self._longitude = longitude + self._has_lat_lon = True + + def hasLatitude(self) -> bool: + return self._has_lat_lon + + def hasLongitude(self) -> bool: + return self._has_lat_lon + + def toString(self) -> str: + return f"Address(locality={self._locality}, country={self._country_name})" + + +# ═══════════════════════════════════════════════════════════════ +# Geocoder +# ═══════════════════════════════════════════════════════════════ + +class Geocoder(Stub): + """Geocoder — 地理编码/逆地理编码""" + + def __init__(self, context: Any = None, locale: Any = None): + super().__init__() + self._context = context + + def getFromLocation(self, latitude: float, longitude: float, + max_results: int = 1) -> List[Address]: + addr = Address() + addr.setLatitude(latitude) + addr.setLongitude(longitude) + addr.setFeatureName("Unknown Location") + addr.setLocality("Unknown") + addr.setCountryName("Unknown") + return [addr] if max_results > 0 else [] + + def getFromLocationName(self, location_name: str, max_results: int = 1, + lower_left_lat: Optional[float] = None, + lower_left_lon: Optional[float] = None, + upper_right_lat: Optional[float] = None, + upper_right_lon: Optional[float] = None) -> List[Address]: + addr = Address() + addr.setFeatureName(location_name) + addr.setLocality(location_name) + return [addr] if max_results > 0 else [] + + +# ═══════════════════════════════════════════════════════════════ +# LocationManager +# ═══════════════════════════════════════════════════════════════ + +class LocationManager(Stub): + """LocationManager — 位置管理器""" + + GPS_PROVIDER = "gps" + NETWORK_PROVIDER = "network" + PASSIVE_PROVIDER = "passive" + FUSED_PROVIDER = "fused" + + KEY_PROXIMITY_ENTERING = "entering" + KEY_PROXIMITY_DO_ENTER = "doEnter" + KEY_FLUSH_COMPLETE = "flushComplete" + KEY_LOCATION_CHANGED = "locationChanged" + KEY_PROXIMITY_ENTERING_DO_ENTER = "doEnter" + + PROVIDERS_CHANGED_ACTION = "android.location.PROVIDERS_CHANGED" + MODE_CHANGED_ACTION = "android.location.MODE_CHANGED" + + def __init__(self): + super().__init__() + self._listeners: Dict[str, List[LocationListener]] = {} + self._providers = { + self.GPS_PROVIDER: LocationProvider(self.GPS_PROVIDER), + self.NETWORK_PROVIDER: LocationProvider(self.NETWORK_PROVIDER), + self.PASSIVE_PROVIDER: LocationProvider(self.PASSIVE_PROVIDER), + self.FUSED_PROVIDER: LocationProvider(self.FUSED_PROVIDER), + } + self._last_known_locations: Dict[str, Location] = {} + self._enabled_providers = {self.GPS_PROVIDER, self.NETWORK_PROVIDER, + self.PASSIVE_PROVIDER, self.FUSED_PROVIDER} + + def getLastKnownLocation(self, provider: str) -> Optional[Location]: + if provider in self._last_known_locations: + return self._last_known_locations[provider] + loc = Location(provider) + loc.setLatitude(37.7749) + loc.setLongitude(-122.4194) + loc.setTime(int(datetime.now().timestamp() * 1000)) + loc.setAccuracy(20.0) + self._last_known_locations[provider] = loc + return loc + + def requestLocationUpdates(self, provider_or_request: Any, min_time: Any = None, + min_distance: float = 0.0, listener_or_callback: Any = None, + looper: Any = None) -> None: + if isinstance(provider_or_request, str): + provider = provider_or_request + else: + provider = self.GPS_PROVIDER + if listener_or_callback is not None: + if provider not in self._listeners: + self._listeners[provider] = [] + if isinstance(listener_or_callback, LocationListener): + self._listeners[provider].append(listener_or_callback) + + def removeUpdates(self, listener: Any): + for provider in list(self._listeners.keys()): + self._listeners[provider] = [ + l for l in self._listeners[provider] if l is not listener + ] + if not self._listeners[provider]: + del self._listeners[provider] + + def isProviderEnabled(self, provider: str) -> bool: + return provider in self._enabled_providers + + def getProviders(self, criteria: Optional[Criteria] = None, enabled_only: bool = False) -> List[str]: + providers = list(self._providers.keys()) + if enabled_only: + providers = [p for p in providers if p in self._enabled_providers] + return providers + + def getBestProvider(self, criteria: Criteria, enabled_only: bool = False) -> Optional[str]: + if criteria.getAccuracy() == Criteria.ACCURACY_FINE: + return self.GPS_PROVIDER + return self.NETWORK_PROVIDER + + def getAllProviders(self) -> List[str]: + return list(self._providers.keys()) + + def getProvider(self, name: str) -> Optional[LocationProvider]: + return self._providers.get(name) + + def addProximityAlert(self, latitude: float, longitude: float, + radius: float, expiration: int, intent: Any): + pass + + def sendExtraCommand(self, provider: str, command: str, extras: Any) -> bool: + return True + + def clearTestProviderLocation(self, provider: str): + pass + + def setTestProviderLocation(self, provider: str, location: Location): + pass + + def removeTestProvider(self, provider: str): + pass + + def addTestProvider(self, name: str, requires_network: bool, requires_satellite: bool, + requires_cell: bool, has_monetary_cost: bool, supports_altitude: bool, + supports_speed: bool, supports_bearing: bool, power_requirement: int, + accuracy: int): + self._providers[name] = LocationProvider(name) + + def toString(self) -> str: + return f"LocationManager(providers={list(self._providers.keys())})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Location", + "LocationManager", + "LocationListener", + "LocationProvider", + "Criteria", + "Geocoder", + "Address", + "GpsStatus", + "GpsSatellite", + "GpsStatusListener", + "LocationRequest", + "LocationProviderProperties", + "SettingInjectorService", + "GnssStatus", + "GnssClock", + "GnssMeasurement", + "GnssMeasurementsEvent", + "GnssNavigationMessage", +] \ No newline at end of file diff --git a/android_pyapi/android/media.py b/android_pyapi/android/media.py new file mode 100644 index 0000000..102982f --- /dev/null +++ b/android_pyapi/android/media.py @@ -0,0 +1,2049 @@ +""" +Android Media 组件 — MediaPlayer, MediaRecorder, SoundPool, AudioManager 等 +方法签名完全对齐 Android SDK +""" + +import io +import math +import os +import time +import struct +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, resolve_android_path +from .graphics import PointF + + +# ═══════════════════════════════════════════════════════════════ +# AudioAttributes +# ═══════════════════════════════════════════════════════════════ + +class AudioAttributes(Stub): + """AudioAttributes — 音频属性""" + + CONTENT_TYPE_UNKNOWN = 0 + CONTENT_TYPE_SPEECH = 1 + CONTENT_TYPE_MUSIC = 2 + CONTENT_TYPE_MOVIE = 3 + CONTENT_TYPE_SONIFICATION = 4 + + USAGE_UNKNOWN = 0 + USAGE_MEDIA = 1 + USAGE_VOICE_COMMUNICATION = 2 + USAGE_VOICE_COMMUNICATION_SIGNALLING = 3 + USAGE_ALARM = 4 + USAGE_NOTIFICATION = 5 + USAGE_NOTIFICATION_RINGTONE = 6 + USAGE_GAME = 14 + USAGE_ASSISTANCE_ACCESSIBILITY = 11 + USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = 12 + USAGE_ASSISTANCE_SONIFICATION = 13 + + FLAG_AUDIBILITY_ENFORCED = 1 + FLAG_SECURE = 2 + FLAG_SCO = 4 + FLAG_BEACON = 8 + FLAG_BYPASS_INTERRUPTION_POLICY = 16 + FLAG_BYPASS_MUTE = 32 + FLAG_LOW_LATENCY = 64 + FLAG_DEEP_BUFFER = 128 + + class Builder(Stub): + """AudioAttributes.Builder — 音频属性构建器""" + + def __init__(self): + super().__init__() + self._content_type = AudioAttributes.CONTENT_TYPE_UNKNOWN + self._usage = AudioAttributes.USAGE_UNKNOWN + self._flags = 0 + self._legacy_stream_type = -1 + + def setContentType(self, contentType: int) -> "AudioAttributes.Builder": + self._content_type = contentType + return self + + def setUsage(self, usage: int) -> "AudioAttributes.Builder": + self._usage = usage + return self + + def setFlags(self, flags: int) -> "AudioAttributes.Builder": + self._flags = flags + return self + + def setLegacyStreamType(self, streamType: int) -> "AudioAttributes.Builder": + self._legacy_stream_type = streamType + return self + + def build(self) -> "AudioAttributes": + attrs = AudioAttributes() + attrs._content_type = self._content_type + attrs._usage = self._usage + attrs._flags = self._flags + attrs._legacy_stream_type = self._legacy_stream_type + return attrs + + def __init__(self): + super().__init__() + self._content_type = AudioAttributes.CONTENT_TYPE_UNKNOWN + self._usage = AudioAttributes.USAGE_UNKNOWN + self._flags = 0 + self._legacy_stream_type = -1 + + def getContentType(self) -> int: + return self._content_type + + def getUsage(self) -> int: + return self._usage + + def getFlags(self) -> int: + return self._flags + + def getLegacyStreamType(self) -> int: + if self._legacy_stream_type >= 0: + return self._legacy_stream_type + usage_map = { + AudioAttributes.USAGE_MEDIA: 3, + AudioAttributes.USAGE_ALARM: 4, + AudioAttributes.USAGE_NOTIFICATION: 5, + AudioAttributes.USAGE_NOTIFICATION_RINGTONE: 2, + AudioAttributes.USAGE_VOICE_COMMUNICATION: 0, + AudioAttributes.USAGE_GAME: 3, + } + return usage_map.get(self._usage, 3) + + +# ═══════════════════════════════════════════════════════════════ +# AudioFormat +# ═══════════════════════════════════════════════════════════════ + +class AudioFormat(Stub): + """AudioFormat — 音频格式""" + + ENCODING_INVALID = 0 + ENCODING_DEFAULT = 1 + ENCODING_PCM_16BIT = 2 + ENCODING_PCM_8BIT = 3 + ENCODING_PCM_FLOAT = 4 + ENCODING_AC3 = 5 + ENCODING_E_AC3 = 6 + ENCODING_DTS = 7 + ENCODING_DTS_HD = 8 + ENCODING_MP3 = 9 + ENCODING_AAC = 10 + ENCODING_AAC_LC = 11 + ENCODING_AAC_HE_V1 = 12 + ENCODING_AAC_HE_V2 = 13 + ENCODING_AAC_ELD = 14 + ENCODING_AAC_XHE = 15 + ENCODING_AMRNB = 16 + ENCODING_AMRWB = 17 + ENCODING_OPUS = 18 + + CHANNEL_OUT_DEFAULT = 0x000 + CHANNEL_OUT_MONO = 0x004 + CHANNEL_OUT_STEREO = 0x00C + CHANNEL_OUT_QUAD = 0x01C + CHANNEL_OUT_SURROUND = 0x03C + CHANNEL_OUT_5POINT1 = 0x0FC + CHANNEL_OUT_7POINT1 = 0x3FC + + CHANNEL_IN_DEFAULT = 0x000 + CHANNEL_IN_MONO = 0x010 + CHANNEL_IN_STEREO = 0x020 + + def __init__(self): + super().__init__() + self._sample_rate = 44100 + self._encoding = AudioFormat.ENCODING_PCM_16BIT + self._channel_mask = AudioFormat.CHANNEL_OUT_STEREO + self._channel_count = 2 + + def getSampleRate(self) -> int: + return self._sample_rate + + def getEncoding(self) -> int: + return self._encoding + + def getChannelMask(self) -> int: + return self._channel_mask + + def getChannelCount(self) -> int: + return self._channel_count + + @staticmethod + def toChannelMask(channelCount: int) -> int: + return AudioFormat.CHANNEL_OUT_MONO if channelCount == 1 else AudioFormat.CHANNEL_OUT_STEREO + + +# ═══════════════════════════════════════════════════════════════ +# MediaPlayer +# ═══════════════════════════════════════════════════════════════ + +class MediaPlayer(Stub): + """MediaPlayer — 媒体播放器""" + + MEDIA_INFO_UNKNOWN = 1 + MEDIA_INFO_VIDEO_TRACK_LAGGING = 700 + MEDIA_INFO_VIDEO_RENDERING_START = 3 + MEDIA_INFO_BUFFERING_START = 701 + MEDIA_INFO_BUFFERING_END = 702 + MEDIA_INFO_BAD_INTERLEAVING = 800 + MEDIA_INFO_NOT_SEEKABLE = 801 + MEDIA_INFO_METADATA_UPDATE = 802 + MEDIA_INFO_AUDIO_NOT_PLAYING = 804 + MEDIA_INFO_VIDEO_NOT_PLAYING = 805 + MEDIA_INFO_SUBTITLE_TIMED_OUT = 902 + MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901 + + MEDIA_ERROR_UNKNOWN = 1 + MEDIA_ERROR_SERVER_DIED = 100 + MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200 + MEDIA_ERROR_TIMED_OUT = -110 + MEDIA_ERROR_IO = -1004 + MEDIA_ERROR_MALFORMED = -1007 + MEDIA_ERROR_UNSUPPORTED = -1010 + + # OnPreparedListener + class OnPreparedListener(Stub): + def onPrepared(self, mp: "MediaPlayer"): + pass + + # OnCompletionListener + class OnCompletionListener(Stub): + def onCompletion(self, mp: "MediaPlayer"): + pass + + # OnErrorListener + class OnErrorListener(Stub): + def onError(self, mp: "MediaPlayer", what: int, extra: int) -> bool: + return False + + # OnInfoListener + class OnInfoListener(Stub): + def onInfo(self, mp: "MediaPlayer", what: int, extra: int) -> bool: + return False + + # OnSeekCompleteListener + class OnSeekCompleteListener(Stub): + def onSeekComplete(self, mp: "MediaPlayer"): + pass + + # OnVideoSizeChangedListener + class OnVideoSizeChangedListener(Stub): + def onVideoSizeChanged(self, mp: "MediaPlayer", width: int, height: int): + pass + + def __init__(self): + super().__init__() + self._data_source = None + self._prepared = False + self._playing = False + self._looping = False + self._volume_left = 1.0 + self._volume_right = 1.0 + self._current_position = 0 + self._duration = 0 + self._video_width = 0 + self._video_height = 0 + self._screen_on = False + self._wake_mode = 0 + self._audio_attributes = None + self._display = None + self._surface = None + + # Listeners + self._on_prepared_listener = None + self._on_completion_listener = None + self._on_error_listener = None + self._on_info_listener = None + self._on_seek_complete_listener = None + self._on_video_size_changed_listener = None + + def setDataSource(self, path: str = None, uri=None, headers: Dict = None): + self._data_source = path or str(uri) + + def prepare(self): + self._prepared = True + + def prepareAsync(self): + self._prepared = True + if self._on_prepared_listener: + self._on_prepared_listener.onPrepared(self) + + def start(self): + self._playing = True + + def stop(self): + self._playing = False + + def pause(self): + self._playing = False + + def reset(self): + self._playing = False + self._prepared = False + self._current_position = 0 + + def release(self): + self._playing = False + self._prepared = False + + def seekTo(self, msec: int): + self._current_position = max(0, min(msec, self._duration)) + if self._on_seek_complete_listener: + self._on_seek_complete_listener.onSeekComplete(self) + + def getCurrentPosition(self) -> int: + return self._current_position + + def getDuration(self) -> int: + return self._duration + + def isPlaying(self) -> bool: + return self._playing + + def isLooping(self) -> bool: + return self._looping + + def setLooping(self, looping: bool): + self._looping = looping + + def setVolume(self, leftVolume: float, rightVolume: float): + self._volume_left = leftVolume + self._volume_right = rightVolume + + def getVolume(self) -> Tuple[float, float]: + return (self._volume_left, self._volume_right) + + def setScreenOnWhilePlaying(self, screenOn: bool): + self._screen_on = screenOn + + def setWakeMode(self, context, mode: int): + self._wake_mode = mode + + def setAudioAttributes(self, attributes: AudioAttributes): + self._audio_attributes = attributes + + def setDisplay(self, surface_holder): + self._display = surface_holder + + def getVideoWidth(self) -> int: + return self._video_width + + def getVideoHeight(self) -> int: + return self._video_height + + def setOnPreparedListener(self, listener: "MediaPlayer.OnPreparedListener"): + self._on_prepared_listener = listener + + def setOnCompletionListener(self, listener: "MediaPlayer.OnCompletionListener"): + self._on_completion_listener = listener + + def setOnErrorListener(self, listener: "MediaPlayer.OnErrorListener"): + self._on_error_listener = listener + + def setOnInfoListener(self, listener: "MediaPlayer.OnInfoListener"): + self._on_info_listener = listener + + def setOnSeekCompleteListener(self, listener: "MediaPlayer.OnSeekCompleteListener"): + self._on_seek_complete_listener = listener + + def setOnVideoSizeChangedListener(self, listener: "MediaPlayer.OnVideoSizeChangedListener"): + self._on_video_size_changed_listener = listener + + @staticmethod + def create(context, resid: int = 0) -> "MediaPlayer": + mp = MediaPlayer() + mp._prepared = True + return mp + + +# ═══════════════════════════════════════════════════════════════ +# MediaRecorder +# ═══════════════════════════════════════════════════════════════ + +class MediaRecorder(Stub): + """MediaRecorder — 媒体录制器""" + + class AudioSource: + DEFAULT = 0 + MIC = 1 + VOICE_UPLINK = 2 + VOICE_DOWNLINK = 3 + VOICE_CALL = 4 + CAMCORDER = 5 + VOICE_RECOGNITION = 6 + VOICE_COMMUNICATION = 7 + REMOTE_SUBMIX = 8 + UNPROCESSED = 9 + + class VideoSource: + DEFAULT = 0 + CAMERA = 1 + SURFACE = 2 + + class OutputFormat: + DEFAULT = 0 + THREE_GPP = 1 + MPEG_4 = 2 + AMR_NB = 3 + AMR_WB = 4 + AAC_ADIF = 5 + AAC_ADTS = 6 + OUTPUT_FORMAT_RTP_AVP = 7 + MPEG_2_TS = 8 + WEBM = 9 + HEIF = 10 + OGG = 11 + + class AudioEncoder: + DEFAULT = 0 + AMR_NB = 1 + AMR_WB = 2 + AAC = 3 + HE_AAC = 4 + AAC_ELD = 5 + VORBIS = 6 + OPUS = 7 + LPCM = 8 + + class VideoEncoder: + DEFAULT = 0 + H_263 = 1 + H_264 = 2 + MPEG_4_SP = 3 + VP8 = 4 + HEVC = 5 + VP9 = 6 + AV1 = 7 + + def __init__(self): + super().__init__() + self._audio_source = 0 + self._video_source = 0 + self._output_format = 0 + self._audio_encoder = 0 + self._video_encoder = 0 + self._output_file = None + self._preview_display = None + self._recording = False + + def setAudioSource(self, audio_source: int): + self._audio_source = audio_source + + def setVideoSource(self, video_source: int): + self._video_source = video_source + + def setOutputFormat(self, output_format: int): + self._output_format = output_format + + def setAudioEncoder(self, audio_encoder: int): + self._audio_encoder = audio_encoder + + def setVideoEncoder(self, video_encoder: int): + self._video_encoder = video_encoder + + def setOutputFile(self, path: str): + self._output_file = path + + def setPreviewDisplay(self, surface_holder): + self._preview_display = surface_holder + + def prepare(self): + pass + + def start(self): + self._recording = True + + def stop(self): + self._recording = False + + def reset(self): + self._recording = False + + def release(self): + self._recording = False + + def getMaxAmplitude(self) -> int: + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# SoundPool +# ═══════════════════════════════════════════════════════════════ + +class SoundPool(Stub): + """SoundPool — 音效池""" + + def __init__(self, maxStreams: int = 1, streamType: int = 3, srcQuality: int = 0): + super().__init__() + self._max_streams = maxStreams + self._stream_type = streamType + self._src_quality = srcQuality + self._sounds: Dict[int, str] = {} + self._playing: Dict[int, float] = {} + self._on_load_complete_listener = None + self._next_id = 1 + + def load(self, path: str, priority: int = 1) -> int: + sound_id = self._next_id + self._next_id += 1 + self._sounds[sound_id] = path + if self._on_load_complete_listener: + self._on_load_complete_listener.onLoadComplete(self, sound_id, 0) + return sound_id + + def load(self, context, resId: int, priority: int) -> int: + return self.load(str(resId), priority) + + def play(self, soundID: int, leftVolume: float, rightVolume: float, + priority: int, loop: int, rate: float) -> int: + stream_id = self._next_id + self._next_id += 1 + self._playing[stream_id] = 0.0 + return stream_id + + def pause(self, streamID: int): + pass + + def resume(self, streamID: int): + pass + + def autoPause(self): + pass + + def autoResume(self): + pass + + def release(self): + self._sounds.clear() + self._playing.clear() + + def setVolume(self, streamID: int, leftVolume: float, rightVolume: float): + pass + + def setRate(self, streamID: int, rate: float): + pass + + def setPriority(self, streamID: int, priority: int): + pass + + def setLoop(self, streamID: int, loop: int): + pass + + def setOnLoadCompleteListener(self, listener): + self._on_load_complete_listener = listener + + +# ═══════════════════════════════════════════════════════════════ +# AudioManager +# ═══════════════════════════════════════════════════════════════ + +class AudioManager(Stub): + """AudioManager — 音频管理器""" + + MODE_NORMAL = 0 + MODE_RINGTONE = 1 + MODE_IN_CALL = 2 + MODE_IN_COMMUNICATION = 3 + + STREAM_VOICE_CALL = 0 + STREAM_SYSTEM = 1 + STREAM_RING = 2 + STREAM_MUSIC = 3 + STREAM_ALARM = 4 + STREAM_NOTIFICATION = 5 + STREAM_BLUETOOTH_SCO = 6 + STREAM_DTMF = 8 + STREAM_TTS = 9 + STREAM_ACCESSIBILITY = 10 + + ADJUST_RAISE = 1 + ADJUST_LOWER = -1 + ADJUST_SAME = 0 + ADJUST_MUTE = -100 + ADJUST_UNMUTE = 100 + ADJUST_TOGGLE_MUTE = 101 + + FLAG_PLAY_SOUND = 1 + FLAG_SHOW_UI = 2 + FLAG_REMOVE_SOUND_AND_VIBRATE = 4 + FLAG_ALLOW_RINGER_MODES = 8 + FLAG_VIBRATE = 16 + FLAG_BYPASS_INTERRUPTION_POLICY = 32 + FLAG_BYPASS_MUTE = 64 + FLAG_LOW_LATENCY = 128 + + RINGER_MODE_NORMAL = 2 + RINGER_MODE_SILENT = 0 + RINGER_MODE_VIBRATE = 1 + + def __init__(self): + super().__init__() + self._mode = AudioManager.MODE_NORMAL + self._ringer_mode = AudioManager.RINGER_MODE_NORMAL + self._speakerphone_on = False + self._bluetooth_sco_on = False + self._wired_headset_on = False + self._stream_volumes = { + AudioManager.STREAM_VOICE_CALL: 4, + AudioManager.STREAM_SYSTEM: 7, + AudioManager.STREAM_RING: 5, + AudioManager.STREAM_MUSIC: 10, + AudioManager.STREAM_ALARM: 7, + AudioManager.STREAM_NOTIFICATION: 5, + } + self._parameters: Dict[str, str] = {} + + def getStreamVolume(self, streamType: int) -> int: + return self._stream_volumes.get(streamType, 0) + + def getStreamMaxVolume(self, streamType: int) -> int: + return 15 + + def setStreamVolume(self, streamType: int, index: int, flags: int = 0): + if streamType in self._stream_volumes: + self._stream_volumes[streamType] = max(0, min(15, index)) + + def adjustVolume(self, direction: int, flags: int = 0): + pass + + def adjustStreamVolume(self, streamType: int, direction: int, flags: int = 0): + current = self.getStreamVolume(streamType) + if direction == AudioManager.ADJUST_RAISE: + self.setStreamVolume(streamType, current + 1) + elif direction == AudioManager.ADJUST_LOWER: + self.setStreamVolume(streamType, current - 1) + + def getMode(self) -> int: + return self._mode + + def setMode(self, mode: int): + self._mode = mode + + def setSpeakerphoneOn(self, on: bool): + self._speakerphone_on = on + + def isSpeakerphoneOn(self) -> bool: + return self._speakerphone_on + + def isWiredHeadsetOn(self) -> bool: + return self._wired_headset_on + + def isBluetoothScoOn(self) -> bool: + return self._bluetooth_sco_on + + def setBluetoothScoOn(self, on: bool): + self._bluetooth_sco_on = on + + def setRingerMode(self, ringerMode: int): + self._ringer_mode = ringerMode + + def getRingerMode(self) -> int: + return self._ringer_mode + + def loadSoundEffects(self): + pass + + def unloadSoundEffects(self): + pass + + def getParameters(self, keys: str) -> str: + return ",".join(f"{k}={v}" for k, v in self._parameters.items() if k in keys) + + def setParameters(self, keyValuePairs: str): + for pair in keyValuePairs.split(";"): + if "=" in pair: + k, v = pair.split("=", 1) + self._parameters[k.strip()] = v.strip() + + +# ═══════════════════════════════════════════════════════════════ +# AudioTrack +# ═══════════════════════════════════════════════════════════════ + +class AudioTrack(Stub): + """AudioTrack — 音频轨道""" + + STATE_UNINITIALIZED = 0 + STATE_INITIALIZED = 1 + STATE_NO_STATIC_DATA = 2 + MODE_STATIC = 0 + MODE_STREAM = 1 + WRITE_BLOCKING = -1 + WRITE_NON_BLOCKING = 0 + SUCCESS = 0 + ERROR = -1 + ERROR_BAD_VALUE = -2 + ERROR_INVALID_OPERATION = -3 + ERROR_DEAD_OBJECT = -6 + ERROR_WAS_BLOCKED = -8 + + PERFORMANCE_MODE_NONE = 0 + PERFORMANCE_MODE_LOW_LATENCY = 1 + PERFORMANCE_MODE_POWER_SAVING = 2 + + def __init__(self, streamType: int, sampleRateInHz: int, channelConfig: int, + audioFormat: int, bufferSizeInBytes: int, mode: int): + super().__init__() + self._state = AudioTrack.STATE_INITIALIZED + self._playing = False + self._sample_rate = sampleRateInHz + self._channel_config = channelConfig + self._audio_format = audioFormat + self._buffer_size = bufferSizeInBytes + self._mode = mode + self._stream_type = streamType + self._volume = 1.0 + self._playback_rate = sampleRateInHz + self._buffer = bytearray(bufferSizeInBytes) + + def play(self): + self._playing = True + + def pause(self): + self._playing = False + + def stop(self): + self._playing = False + + def flush(self): + pass + + def release(self): + self._state = AudioTrack.STATE_UNINITIALIZED + self._playing = False + + def write(self, audioData, offsetInBytes: int = 0, sizeInBytes: int = -1, + writeMode: int = WRITE_BLOCKING) -> int: + if sizeInBytes < 0: + sizeInBytes = len(audioData) - offsetInBytes + if self._buffer and offsetInBytes + sizeInBytes <= len(self._buffer): + self._buffer[offsetInBytes:offsetInBytes + sizeInBytes] = audioData[offsetInBytes:offsetInBytes + sizeInBytes] + return sizeInBytes + + def getPlayState(self) -> int: + return 3 if self._playing else 1 + + def getState(self) -> int: + return self._state + + def getSampleRate(self) -> int: + return self._sample_rate + + def getPlaybackRate(self) -> int: + return self._playback_rate + + def setPlaybackRate(self, sampleRateInHz: int): + self._playback_rate = sampleRateInHz + + def setVolume(self, volume: float): + self._volume = max(0.0, min(1.0, volume)) + + def getVolume(self) -> float: + return self._volume + + def getAudioFormat(self) -> int: + return self._audio_format + + def getChannelConfiguration(self) -> int: + return self._channel_config + + def getChannelCount(self) -> int: + return 2 if self._channel_config == AudioFormat.CHANNEL_OUT_STEREO else 1 + + def getNotificationMarkerPosition(self) -> int: + return 0 + + def setNotificationMarkerPosition(self, marker: int): + pass + + def getPositionNotificationPeriod(self) -> int: + return 0 + + def setPositionNotificationPeriod(self, period: int): + pass + + @staticmethod + def getMinBufferSize(sampleRateInHz: int, channelConfig: int, audioFormat: int) -> int: + return sampleRateInHz * 2 * 2 # Conservative estimate + + @staticmethod + def getNativeOutputSampleRate(streamType: int) -> int: + return 44100 + + @staticmethod + def getMaxVolume() -> float: + return 1.0 + + @staticmethod + def getMinVolume() -> float: + return 0.0 + + +# ═══════════════════════════════════════════════════════════════ +# AudioRecord +# ═══════════════════════════════════════════════════════════════ + +class AudioRecord(Stub): + """AudioRecord — 音频录制器""" + + STATE_UNINITIALIZED = 0 + STATE_INITIALIZED = 1 + SUCCESS = 0 + ERROR = -1 + ERROR_BAD_VALUE = -2 + ERROR_INVALID_OPERATION = -3 + ERROR_DEAD_OBJECT = -6 + + def __init__(self, audioSource: int, sampleRateInHz: int, channelConfig: int, + audioFormat: int, bufferSizeInBytes: int): + super().__init__() + self._state = AudioRecord.STATE_INITIALIZED + self._recording = False + self._audio_source = audioSource + self._sample_rate = sampleRateInHz + self._channel_config = channelConfig + self._audio_format = audioFormat + self._buffer_size = bufferSizeInBytes + self._buffer = bytearray(bufferSizeInBytes) + + def startRecording(self): + self._recording = True + + def stop(self): + self._recording = False + + def release(self): + self._state = AudioRecord.STATE_UNINITIALIZED + self._recording = False + + def read(self, audioData, offsetInBytes: int, sizeInBytes: int) -> int: + return min(sizeInBytes, len(audioData) - offsetInBytes) + + def getState(self) -> int: + return self._state + + def getSampleRate(self) -> int: + return self._sample_rate + + def getRecordingState(self) -> int: + return 3 if self._recording else 1 + + def getAudioFormat(self) -> int: + return self._audio_format + + def getChannelConfiguration(self) -> int: + return self._channel_config + + def getChannelCount(self) -> int: + return 2 if self._channel_config == AudioFormat.CHANNEL_IN_STEREO else 1 + + @staticmethod + def getMinBufferSize(sampleRateInHz: int, channelConfig: int, audioFormat: int) -> int: + return sampleRateInHz * 2 * 2 + + def getAudioSource(self) -> int: + return self._audio_source + + +# ═══════════════════════════════════════════════════════════════ +# Ringtone +# ═══════════════════════════════════════════════════════════════ + +class Ringtone(Stub): + """Ringtone — 铃声""" + + def __init__(self, context=None): + super().__init__() + self._title = "" + self._stream_type = AudioManager.STREAM_RING + self._playing = False + self._volume = 1.0 + + def setStreamType(self, streamType: int): + self._stream_type = streamType + + def getStreamType(self) -> int: + return self._stream_type + + def setTitle(self, title: str): + self._title = title + + def getTitle(self, context=None) -> str: + return self._title + + def play(self): + self._playing = True + + def stop(self): + self._playing = False + + def isPlaying(self) -> bool: + return self._playing + + def setVolume(self, volume: float): + self._volume = max(0.0, min(1.0, volume)) + + def getVolume(self) -> float: + return self._volume + + def getAudioAttributes(self) -> AudioAttributes: + attrs = AudioAttributes() + if self._stream_type == AudioManager.STREAM_RING: + attrs._usage = AudioAttributes.USAGE_NOTIFICATION_RINGTONE + elif self._stream_type == AudioManager.STREAM_ALARM: + attrs._usage = AudioAttributes.USAGE_ALARM + else: + attrs._usage = AudioAttributes.USAGE_NOTIFICATION + return attrs + + +# ═══════════════════════════════════════════════════════════════ +# RingtoneManager +# ═══════════════════════════════════════════════════════════════ + +class RingtoneManager(Stub): + """RingtoneManager — 铃声管理器""" + + TYPE_RINGTONE = 1 + TYPE_NOTIFICATION = 2 + TYPE_ALARM = 4 + TYPE_ALL = 7 + + ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER" + EXTRA_RINGTONE_TYPE = "android.intent.extra.ringtone.TYPE" + EXTRA_RINGTONE_TITLE = "android.intent.extra.ringtone.TITLE" + EXTRA_RINGTONE_EXISTING_URI = "android.intent.extra.ringtone.EXISTING_URI" + EXTRA_RINGTONE_DEFAULT_URI = "android.intent.extra.ringtone.DEFAULT_URI" + EXTRA_RINGTONE_SHOW_DEFAULT = "android.intent.extra.ringtone.SHOW_DEFAULT" + EXTRA_RINGTONE_INCLUDE_DRM = "android.intent.extra.ringtone.INCLUDE_DRM" + EXTRA_RINGTONE_PICKED_URI = "android.intent.extra.ringtone.PICKED_URI" + + def __init__(self, context): + super().__init__() + self._context = context + self._type = RingtoneManager.TYPE_RINGTONE + self._cursor = [] + self._cursor_position = -1 + + def setType(self, type_: int): + self._type = type_ + + def getCursor(self): + return self._cursor + + def getRingtoneUri(self, position: int): + return None + + def getRingtonePosition(self, uri) -> int: + return 0 + + @staticmethod + def getRingtone(context, uri) -> Ringtone: + return Ringtone(context) + + @staticmethod + def getActualDefaultRingtoneUri(context, type_: int): + return None + + @staticmethod + def setActualDefaultRingtoneUri(context, type_: int, ringtoneUri): + pass + + @staticmethod + def getDefaultUri(type_: int): + return None + + @staticmethod + def getValidRingtoneUri(context): + return None + + @staticmethod + def inferStreamType(type_: int) -> int: + if type_ == RingtoneManager.TYPE_RINGTONE: + return AudioManager.STREAM_RING + elif type_ == RingtoneManager.TYPE_NOTIFICATION: + return AudioManager.STREAM_NOTIFICATION + elif type_ == RingtoneManager.TYPE_ALARM: + return AudioManager.STREAM_ALARM + return AudioManager.STREAM_RING + + +# ═══════════════════════════════════════════════════════════════ +# MediaMetadataRetriever +# ═══════════════════════════════════════════════════════════════ + +class MediaMetadataRetriever(Stub): + """MediaMetadataRetriever — 媒体元数据检索器""" + + METADATA_KEY_ALBUM = "album" + METADATA_KEY_ARTIST = "artist" + METADATA_KEY_TITLE = "title" + METADATA_KEY_DURATION = "duration" + METADATA_KEY_CD_TRACK_NUMBER = "cd_track_number" + METADATA_KEY_COMPOSER = "composer" + METADATA_KEY_DATE = "date" + METADATA_KEY_GENRE = "genre" + METADATA_KEY_MIMETYPE = "mimetype" + METADATA_KEY_NUM_TRACKS = "num_tracks" + METADATA_KEY_WRITER = "writer" + METADATA_KEY_YEAR = "year" + METADATA_KEY_VIDEO_WIDTH = "video_width" + METADATA_KEY_VIDEO_HEIGHT = "video_height" + METADATA_KEY_HAS_AUDIO = "has_audio" + METADATA_KEY_HAS_VIDEO = "has_video" + METADATA_KEY_BITRATE = "bitrate" + METADATA_KEY_LOCATION = "location" + + def __init__(self): + super().__init__() + self._metadata: Dict[str, str] = {} + + def setDataSource(self, path: str): + resolved = resolve_android_path(path) + if os.path.exists(resolved): + size = os.path.getsize(resolved) + self._metadata = { + MediaMetadataRetriever.METADATA_KEY_DURATION: "30000", + MediaMetadataRetriever.METADATA_KEY_TITLE: os.path.basename(resolved), + MediaMetadataRetriever.METADATA_KEY_MIMETYPE: "video/mp4", + MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH: "1920", + MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT: "1080", + MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO: "yes", + MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO: "yes", + MediaMetadataRetriever.METADATA_KEY_BITRATE: str(size * 8 // 30), + } + + def setDataSource(self, path: str, headers: Dict[str, str]): + self.setDataSource(path) + + def extractMetadata(self, keyCode: str) -> Optional[str]: + return self._metadata.get(keyCode) + + def getFrameAtTime(self, timeUs: int = -1, option: int = 0) -> Optional["Bitmap"]: + if HAS_PIL: + from PIL import Image + from .graphics import Bitmap + img = Image.new("RGB", (1920, 1080), (64, 64, 64)) + bm = Bitmap(1920, 1080, Bitmap.Config.ARGB_8888) + bm._image = img + return bm + return None + + def getScaledFrameAtTime(self, timeUs: int, option: int, width: int, height: int) -> Optional["Bitmap"]: + if HAS_PIL: + from PIL import Image + from .graphics import Bitmap + img = Image.new("RGB", (width, height), (64, 64, 64)) + bm = Bitmap(width, height, Bitmap.Config.ARGB_8888) + bm._image = img + return bm + return None + + def release(self): + self._metadata.clear() + + +# ═══════════════════════════════════════════════════════════════ +# MediaCodec +# ═══════════════════════════════════════════════════════════════ + +class MediaCodec(Stub): + """MediaCodec — 媒体编解码器""" + + BUFFER_FLAG_KEY_FRAME = 1 + BUFFER_FLAG_CODEC_CONFIG = 2 + BUFFER_FLAG_END_OF_STREAM = 4 + BUFFER_FLAG_PARTIAL_FRAME = 8 + BUFFER_FLAG_MUXER_DATA = 16 + + CONFIGURE_FLAG_ENCODE = 1 + CONFIGURE_FLAG_DECODE = 0 + + CRYPTO_MODE_UNENCRYPTED = 0 + CRYPTO_MODE_AES_CTR = 1 + CRYPTO_MODE_AES_WV = 2 + CRYPTO_MODE_AES_CBC = 3 + + INFO_TRY_AGAIN_LATER = -1 + INFO_OUTPUT_FORMAT_CHANGED = -2 + INFO_OUTPUT_BUFFERS_CHANGED = -3 + + MEDIA_CODEC_UNKNOWN = 0 + MEDIA_CODEC_DECODER = 1 + MEDIA_CODEC_ENCODER = 2 + + PARAMETER_KEY_SUSPEND = "suspend" + PARAMETER_KEY_VIDEO_BITRATE = "video-bitrate" + PARAMETER_KEY_REQUEST_SYNC_FRAME = "request-sync" + PARAMETER_KEY_DROP_BEFORE = "drop-before" + PARAMETER_KEY_DROP_AFTER = "drop-after" + PARAMETER_KEY_OFFSET_TIME = "offset-time" + PARAMETER_KEY_PRIORITY = "priority" + PARAMETER_KEY_OPERATING_RATE = "operating-rate" + PARAMETER_KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period" + PARAMETER_KEY_LOW_LATENCY = "low-latency" + + def __init__(self, name: str): + super().__init__() + self._name = name + self._configured = False + self._started = False + self._format = MediaFormat() + + @staticmethod + def createDecoderByType(mimeType: str) -> "MediaCodec": + return MediaCodec(f"decoder.{mimeType}") + + @staticmethod + def createEncoderByType(mimeType: str) -> "MediaCodec": + return MediaCodec(f"encoder.{mimeType}") + + def configure(self, format: "MediaFormat", surface, crypto, flags: int): + self._format = format + self._configured = True + + def start(self): + self._started = True + + def stop(self): + self._started = False + + def flush(self): + pass + + def release(self): + self._started = False + self._configured = False + + def getInputBuffers(self) -> List[Any]: + return [bytearray(4096)] + + def getOutputBuffers(self) -> List[Any]: + return [bytearray(4096)] + + def dequeueInputBuffer(self, timeoutUs: int) -> int: + return 0 + + def queueInputBuffer(self, index: int, offset: int, size: int, + presentationTimeUs: int, flags: int): + pass + + def dequeueOutputBuffer(self, info: "MediaCodec.BufferInfo", timeoutUs: int) -> int: + return MediaCodec.INFO_TRY_AGAIN_LATER + + def releaseOutputBuffer(self, index: int, render: bool): + pass + + def getOutputFormat(self) -> "MediaFormat": + return self._format + + def getInputImage(self, index: int): + return None + + def getOutputImage(self, index: int): + return None + + def getName(self) -> str: + return self._name + + def getCodecInfo(self): + return None + + +# ═══════════════════════════════════════════════════════════════ +# MediaCodecBufferInfo (nested class reference) +# ═══════════════════════════════════════════════════════════════ + +class MediaCodecBufferInfo(Stub): + """MediaCodec.BufferInfo — 编解码器缓冲信息""" + + def __init__(self): + super().__init__() + self.offset = 0 + self.size = 0 + self.presentationTimeUs = 0 + self.flags = 0 + + def set(self, offset: int, size: int, presentationTimeUs: int, flags: int): + self.offset = offset + self.size = size + self.presentationTimeUs = presentationTimeUs + self.flags = flags + + def duplicate(self) -> "MediaCodecBufferInfo": + info = MediaCodecBufferInfo() + info.set(self.offset, self.size, self.presentationTimeUs, self.flags) + return info + + +# ═══════════════════════════════════════════════════════════════ +# MediaFormat +# ═══════════════════════════════════════════════════════════════ + +class MediaFormat(Stub): + """MediaFormat — 媒体格式""" + + KEY_MIME = "mime" + KEY_WIDTH = "width" + KEY_HEIGHT = "height" + KEY_FRAME_RATE = "frame-rate" + KEY_BIT_RATE = "bitrate" + KEY_COLOR_FORMAT = "color-format" + KEY_I_FRAME_INTERVAL = "i-frame-interval" + KEY_CHANNEL_COUNT = "channel-count" + KEY_SAMPLE_RATE = "sample-rate" + KEY_DURATION = "durationUs" + KEY_MAX_INPUT_SIZE = "max-input-size" + KEY_AAC_PROFILE = "aac-profile" + KEY_LEVEL = "level" + KEY_PROFILE = "profile" + KEY_LANGUAGE = "language" + KEY_IS_AUTOSELECT = "is-autoselect" + KEY_IS_DEFAULT = "is-default" + KEY_IS_FORCED_SUBTITLE = "is-forced-subtitle" + KEY_PIXEL_ASPECT_RATIO_WIDTH = "pixel-aspect-ratio-width" + KEY_PIXEL_ASPECT_RATIO_HEIGHT = "pixel-aspect-ratio-height" + KEY_SLICE_HEIGHT = "slice-height" + KEY_STRIDE = "stride" + KEY_TEMPORAL_LAYERING = "temporal-layering" + KEY_REPEAT_PREVIOUS_FRAME_AFTER = "repeat-previous-frame-after" + KEY_PRIORITY = "priority" + KEY_OPERATING_RATE = "operating-rate" + KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period" + KEY_LATENCY = "latency" + KEY_MAX_B_FRAMES = "max-bframes" + KEY_HDR10_PLUS_INFO = "hdr10-plus-info" + + MIMETYPE_AUDIO_AAC = "audio/mp4a-latm" + MIMETYPE_AUDIO_AMR_NB = "audio/3gpp" + MIMETYPE_AUDIO_AMR_WB = "audio/amr-wb" + MIMETYPE_AUDIO_FLAC = "audio/flac" + MIMETYPE_AUDIO_MPEG = "audio/mpeg" + MIMETYPE_AUDIO_OPUS = "audio/opus" + MIMETYPE_AUDIO_RAW = "audio/raw" + MIMETYPE_AUDIO_VORBIS = "audio/vorbis" + MIMETYPE_VIDEO_AV1 = "video/av01" + MIMETYPE_VIDEO_AVC = "video/avc" + MIMETYPE_VIDEO_DOLBY_VISION = "video/dolby-vision" + MIMETYPE_VIDEO_H263 = "video/3gpp" + MIMETYPE_VIDEO_HEVC = "video/hevc" + MIMETYPE_VIDEO_MPEG2 = "video/mpeg2" + MIMETYPE_VIDEO_MPEG4 = "video/mp4v-es" + MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8" + MIMETYPE_VIDEO_VP9 = "video/x-vnd.on2.vp9" + MIMETYPE_VIDEO_RAW = "video/raw" + MIMETYPE_TEXT_CEA_608 = "text/cea-608" + MIMETYPE_TEXT_CEA_708 = "text/cea-708" + MIMETYPE_TEXT_VTT = "text/vtt" + MIMETYPE_IMAGE_ANDROID_HEIC = "image/vnd.android.heic" + + def __init__(self): + super().__init__() + self._properties: Dict[str, Any] = {} + + def setString(self, key: str, value: str): + self._properties[key] = value + + def setInteger(self, key: str, value: int): + self._properties[key] = value + + def setFloat(self, key: str, value: float): + self._properties[key] = value + + def setByteBuffer(self, key: str, buf): + self._properties[key] = buf + + def setFeatureEnabled(self, feature: str, enabled: bool): + self._properties[feature] = enabled + + def getString(self, key: str) -> Optional[str]: + v = self._properties.get(key) + return str(v) if v is not None else None + + def getInteger(self, key: str, defaultValue: int = 0) -> int: + v = self._properties.get(key) + return int(v) if v is not None else defaultValue + + def getFloat(self, key: str, defaultValue: float = 0.0) -> float: + v = self._properties.get(key) + return float(v) if v is not None else defaultValue + + def getByteBuffer(self, key: str): + return self._properties.get(key) + + def containsKey(self, key: str) -> bool: + return key in self._properties + + def keys(self) -> List[str]: + return list(self._properties.keys()) + + @staticmethod + def createVideoFormat(mime: str, width: int, height: int) -> "MediaFormat": + fmt = MediaFormat() + fmt.setString(MediaFormat.KEY_MIME, mime) + fmt.setInteger(MediaFormat.KEY_WIDTH, width) + fmt.setInteger(MediaFormat.KEY_HEIGHT, height) + return fmt + + @staticmethod + def createAudioFormat(mime: str, sampleRate: int, channelCount: int) -> "MediaFormat": + fmt = MediaFormat() + fmt.setString(MediaFormat.KEY_MIME, mime) + fmt.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate) + fmt.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelCount) + return fmt + + @staticmethod + def createSubtitleFormat(mime: str, language: str) -> "MediaFormat": + fmt = MediaFormat() + fmt.setString(MediaFormat.KEY_MIME, mime) + fmt.setString(MediaFormat.KEY_LANGUAGE, language) + return fmt + + +# ═══════════════════════════════════════════════════════════════ +# MediaMuxer +# ═══════════════════════════════════════════════════════════════ + +class MediaMuxer(Stub): + """MediaMuxer — 媒体混合器""" + + OUTPUT_FORMAT_MPEG_4 = 0 + OUTPUT_FORMAT_WEBM = 1 + OUTPUT_FORMAT_THREE_GPP = 2 + OUTPUT_FORMAT_HEIF = 3 + OUTPUT_FORMAT_OGG = 4 + + def __init__(self, path: str, format: int): + super().__init__() + self._path = path + self._format = format + self._started = False + self._tracks: Dict[int, MediaFormat] = {} + self._next_track = 0 + + def addTrack(self, format: MediaFormat) -> int: + idx = self._next_track + self._next_track += 1 + self._tracks[idx] = format + return idx + + def start(self): + self._started = True + + def stop(self): + self._started = False + + def release(self): + self._started = False + self._tracks.clear() + + def writeSampleData(self, trackIndex: int, byteBuf, bufferInfo: MediaCodecBufferInfo): + pass + + def setOrientationHint(self, degrees: int): + pass + + def setLocation(self, latitude: float, longitude: float): + pass + + def getTrackFormat(self, index: int) -> Optional[MediaFormat]: + return self._tracks.get(index) + + +# ═══════════════════════════════════════════════════════════════ +# MediaExtractor +# ═══════════════════════════════════════════════════════════════ + +class MediaExtractor(Stub): + """MediaExtractor — 媒体提取器""" + + SEEK_TO_NEXT_SYNC = 0 + SEEK_TO_PREVIOUS_SYNC = 1 + SEEK_TO_CLOSEST_SYNC = 2 + SEEK_TO_CLOSEST_SYNC = 2 + SAMPLE_FLAG_SYNC = 1 + SAMPLE_FLAG_ENCRYPTED = 2 + + def __init__(self): + super().__init__() + self._tracks: List[MediaFormat] = [] + self._selected_tracks: Set[int] = set() + self._current_track = 0 + + def setDataSource(self, path: str): + pass + + def setDataSource(self, context, uri, headers: Dict[str, str] = None): + pass + + def getTrackCount(self) -> int: + return len(self._tracks) + + def getTrackFormat(self, index: int) -> Optional[MediaFormat]: + if 0 <= index < len(self._tracks): + return self._tracks[index] + return None + + def selectTrack(self, index: int): + self._selected_tracks.add(index) + + def unselectTrack(self, index: int): + self._selected_tracks.discard(index) + + def readSampleData(self, byteBuf, offset: int) -> int: + return 0 + + def getSampleTrackIndex(self) -> int: + return 0 + + def getSampleTime(self) -> int: + return 0 + + def advance(self) -> bool: + return False + + def release(self): + self._tracks.clear() + self._selected_tracks.clear() + + def seekTo(self, timeUs: int, mode: int): + pass + + def getSampleFlags(self) -> int: + return 0 + + def getCachedDuration(self) -> int: + return 0 + + def hasCachedReachedEndOfStream(self) -> bool: + return False + + def getSampleSize(self) -> int: + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# FaceDetector / FaceDetectorFace +# ═══════════════════════════════════════════════════════════════ + +class FaceDetectorFace(Stub): + """FaceDetector.Face — 检测到的人脸""" + + CONFIDENCE_THRESHOLD = 0.4 + EULER_X = 0 + EULER_Y = 1 + EULER_Z = 2 + + def __init__(self): + super().__init__() + self._confidence = 0.0 + self._mid_point_x = 0.0 + self._mid_point_y = 0.0 + self._eyes_distance = 0.0 + self._pose_euler_x = 0.0 + self._pose_euler_y = 0.0 + self._pose_euler_z = 0.0 + + def confidence(self) -> float: + return self._confidence + + def getMidPoint(self, point: PointF): + point.set(self._mid_point_x, self._mid_point_y) + + def eyesDistance(self) -> float: + return self._eyes_distance + + def pose(self, euler: int) -> float: + if euler == FaceDetectorFace.EULER_X: + return self._pose_euler_x + elif euler == FaceDetectorFace.EULER_Y: + return self._pose_euler_y + elif euler == FaceDetectorFace.EULER_Z: + return self._pose_euler_z + return 0.0 + + +class FaceDetector(Stub): + """FaceDetector — 人脸检测器""" + + def __init__(self, width: int, height: int, maxFaces: int): + super().__init__() + self._width = width + self._height = height + self._max_faces = maxFaces + + def findFaces(self, bitmap: "Bitmap", faces: List[FaceDetectorFace]) -> int: + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# ThumbnailUtils +# ═══════════════════════════════════════════════════════════════ + +class ThumbnailUtils(Stub): + """ThumbnailUtils — 缩略图工具""" + + OPTIONS_RECYCLE_INPUT = 256 + + @staticmethod + def createVideoThumbnail(filePath: str, kind: int) -> Optional["Bitmap"]: + if HAS_PIL: + from PIL import Image + from .graphics import Bitmap + img = Image.new("RGB", (320, 240), (32, 32, 32)) + bm = Bitmap(320, 240, Bitmap.Config.ARGB_8888) + bm._image = img + return bm + return None + + @staticmethod + def createImageThumbnail(filePath: str, kind: int) -> Optional["Bitmap"]: + resolved = resolve_android_path(filePath) + if not os.path.exists(resolved): + return None + if HAS_PIL: + from PIL import Image + from .graphics import Bitmap + img = Image.open(resolved) + img.thumbnail((320, 240), Image.LANCZOS) + bm = Bitmap(img.width, img.height, Bitmap.Config.ARGB_8888) + if img.mode != "RGBA": + img = img.convert("RGBA") + bm._image = img + return bm + return None + + @staticmethod + def extractThumbnail(source: "Bitmap", width: int, height: int, + options: int = 0) -> Optional["Bitmap"]: + if HAS_PIL and hasattr(source, "_image") and source._image: + from PIL import Image + img = source._image.copy() + img.thumbnail((width, height), Image.LANCZOS) + bm = Bitmap(img.width, img.height, source._config) + bm._image = img + if options & ThumbnailUtils.OPTIONS_RECYCLE_INPUT: + source.recycle() + return bm + return Bitmap.createScaledBitmap(source, width, height) + + +# ═══════════════════════════════════════════════════════════════ +# MediaScannerConnection / MediaScannerConnectionClient +# ═══════════════════════════════════════════════════════════════ + +class MediaScannerConnectionClient(Stub): + """MediaScannerConnection.MediaScannerConnectionClient — 扫描客户端接口""" + + def onMediaScannerConnected(self): + pass + + def onScanCompleted(self, path: str, uri): + pass + + +class MediaScannerConnection(Stub): + """MediaScannerConnection — 媒体扫描连接""" + + def __init__(self, context, client: MediaScannerConnectionClient = None): + super().__init__() + self._client = client + self._connected = False + + def connect(self): + self._connected = True + if self._client: + self._client.onMediaScannerConnected() + + def disconnect(self): + self._connected = False + + def isConnected(self) -> bool: + return self._connected + + def scanFile(self, path: str, mimeType: str): + if self._client: + self._client.onScanCompleted(path, None) + + @staticmethod + def scanFile(context, paths: List[str], mimeTypes: List[str], + callback: MediaScannerConnectionClient): + for path in paths: + if callback: + callback.onScanCompleted(path, None) + + +# ═══════════════════════════════════════════════════════════════ +# MediaActionSound +# ═══════════════════════════════════════════════════════════════ + +class MediaActionSound(Stub): + """MediaActionSound — 媒体操作音效""" + + SHUTTER_CLICK = 0 + FOCUS_COMPLETE = 1 + START_VIDEO_RECORDING = 2 + STOP_VIDEO_RECORDING = 3 + + def __init__(self): + super().__init__() + + def play(self, soundName: int): + pass + + def load(self, soundName: int): + pass + + def release(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# ToneGenerator +# ═══════════════════════════════════════════════════════════════ + +class ToneGenerator(Stub): + """ToneGenerator — 音调发生器""" + + TONE_DTMF_0 = 0 + TONE_DTMF_1 = 1 + TONE_DTMF_2 = 2 + TONE_DTMF_3 = 3 + TONE_DTMF_4 = 4 + TONE_DTMF_5 = 5 + TONE_DTMF_6 = 6 + TONE_DTMF_7 = 7 + TONE_DTMF_8 = 8 + TONE_DTMF_9 = 9 + TONE_DTMF_S = 10 + TONE_DTMF_P = 11 + TONE_DTMF_A = 12 + TONE_DTMF_B = 13 + TONE_DTMF_C = 14 + TONE_DTMF_D = 15 + TONE_SUP_CALL_WAITING = 16 + TONE_SUP_RINGTONE = 17 + TONE_PROP_BEEP = 24 + TONE_PROP_ACK = 25 + TONE_PROP_NACK = 26 + TONE_PROP_PROMPT = 27 + TONE_PROP_BEEP2 = 28 + TONE_SUP_INTERCEPT = 29 + TONE_SUP_INTERCEPT_ABBREV = 30 + TONE_SUP_CONGESTION = 31 + TONE_SUP_CONGESTION_ABBREV = 32 + TONE_SUP_CONFIRM = 33 + TONE_SUP_RADIO_ACK = 34 + TONE_SUP_RADIO_NOTAVAIL = 35 + TONE_SUP_ERROR = 36 + TONE_SUP_CALL_WAITING = 37 + TONE_SUP_RINGTONE = 38 + TONE_PROP_BEEP = 39 + TONE_PROP_ACK = 40 + TONE_PROP_NACK = 41 + TONE_PROP_PROMPT = 42 + TONE_PROP_BEEP2 = 43 + TONE_SUP_INTERCEPT = 44 + TONE_PROP_END_EMERGENCY = 45 + TONE_CDMA_PRESSHOLDKEY_LITE = 46 + TONE_CDMA_ALERT_SIGNAL_RELEASE = 47 + TONE_CDMA_NETWORK_USA_RINGBACK = 48 + TONE_CDMA_INTERCEPT = 49 + TONE_CDMA_ABBR_INTERCEPT = 50 + TONE_CDMA_REORDER = 51 + TONE_CDMA_ABBR_REORDER = 52 + TONE_CDMA_NETWORK_BUSY = 53 + TONE_CDMA_CONFIRM = 54 + TONE_CDMA_ANSWER = 55 + TONE_CDMA_NETWORK_CALLWAITING = 56 + TONE_CDMA_PRESSHOLDKEY_LITE = 57 + TONE_CDMA_ONE_MIN_BEEP = 58 + TONE_CDMA_SIGNAL_OFF = 59 + TONE_CDMA_SIGNAL_ON = 60 + TONE_CDMA_HIGH_LITE = 61 + TONE_CDMA_MED_LITE = 62 + TONE_CDMA_LOW_LITE = 63 + TONE_CDMA_LOW_PBX_LITE = 64 + TONE_CDMA_HIGH_PBX_LITE = 65 + TONE_CDMA_MED_PBX_LITE = 66 + TONE_CDMA_PBX_OFF = 67 + TONE_CDMA_PBX_ON = 68 + TONE_CDMA_ALERT_CALL_GUARD = 69 + TONE_CDMA_SOFT_ERROR_LITE = 70 + TONE_CDMA_ABBR_ALERT = 71 + TONE_CDMA_NETWORK_BUSY_ONE_SHOT = 72 + TONE_CDMA_NETWORK_BUSY = 73 + TONE_CDMA_NETWORK_BUSY_ONE_SHOT = 74 + TONE_CDMA_ABBR_ALERT = 75 + TONE_CDMA_SOFT_ERROR_LITE = 76 + TONE_CDMA_ALERT_CALL_GUARD = 77 + TONE_CDMA_LOW_PBX_LITE = 78 + TONE_CDMA_MED_PBX_LITE = 79 + TONE_CDMA_HIGH_PBX_LITE = 80 + TONE_CDMA_PBX_OFF = 81 + TONE_CDMA_PBX_ON = 82 + TONE_CDMA_PRESSHOLDKEY_LITE = 83 + TONE_CDMA_ONE_MIN_BEEP = 84 + TONE_CDMA_SIGNAL_OFF = 85 + TONE_CDMA_SIGNAL_ON = 86 + TONE_CDMA_HIGH_LITE = 87 + TONE_CDMA_MED_LITE = 88 + TONE_CDMA_LOW_LITE = 89 + TONE_CDMA_ALERT_AUTO_REDIAL = 90 + TONE_CDMA_ALERT_CALL_GUARD = 91 + TONE_PROP_BEEP = 92 + TONE_PROP_ACK = 93 + TONE_PROP_NACK = 94 + TONE_PROP_PROMPT = 95 + TONE_PROP_BEEP2 = 96 + TONE_SUP_INTERCEPT = 97 + TONE_SUP_INTERCEPT_ABBREV = 98 + TONE_SUP_CONGESTION = 99 + TONE_SUP_CONGESTION_ABBREV = 100 + TONE_SUP_CONFIRM = 101 + TONE_SUP_RADIO_ACK = 102 + TONE_SUP_RADIO_NOTAVAIL = 103 + TONE_SUP_ERROR = 104 + TONE_SUP_CALL_WAITING = 105 + TONE_SUP_RINGTONE = 106 + + MIN_VOLUME = 0 + MAX_VOLUME = 100 + + def __init__(self, streamType: int, volume: int): + super().__init__() + self._stream_type = streamType + self._volume = max(0, min(100, volume)) + + def startTone(self, toneType: int, durationMs: int = -1): + pass + + def stopTone(self): + pass + + def release(self): + pass + + def getAudioSessionId(self) -> int: + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# ExifInterface +# ═══════════════════════════════════════════════════════════════ + +class ExifInterface(Stub): + """ExifInterface — EXIF 接口""" + + TAG_APERTURE = "FNumber" + TAG_DATETIME = "DateTime" + TAG_EXPOSURE_TIME = "ExposureTime" + TAG_FLASH = "Flash" + TAG_FOCAL_LENGTH = "FocalLength" + TAG_GPS_ALTITUDE = "GPSAltitude" + TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef" + TAG_GPS_DATESTAMP = "GPSDateStamp" + TAG_GPS_LATITUDE = "GPSLatitude" + TAG_GPS_LATITUDE_REF = "GPSLatitudeRef" + TAG_GPS_LONGITUDE = "GPSLongitude" + TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef" + TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod" + TAG_GPS_TIMESTAMP = "GPSTimeStamp" + TAG_IMAGE_LENGTH = "ImageLength" + TAG_IMAGE_WIDTH = "ImageWidth" + TAG_ISO = "ISOSpeedRatings" + TAG_MAKE = "Make" + TAG_MODEL = "Model" + TAG_ORIENTATION = "Orientation" + TAG_WHITE_BALANCE = "WhiteBalance" + + ORIENTATION_NORMAL = 1 + ORIENTATION_ROTATE_90 = 6 + ORIENTATION_ROTATE_180 = 3 + ORIENTATION_ROTATE_270 = 8 + ORIENTATION_FLIP_HORIZONTAL = 2 + ORIENTATION_FLIP_VERTICAL = 4 + ORIENTATION_TRANSPOSE = 5 + ORIENTATION_TRANSVERSE = 7 + ORIENTATION_UNDEFINED = 0 + + WHITEBALANCE_AUTO = 0 + WHITEBALANCE_MANUAL = 1 + + FLAG_NO_FLASH = 0 + FLAG_FIRED = 1 + FLAG_STROBE_RETURN_LIGHT_DETECTED = 2 + FLAG_STROBE_RETURN_LIGHT_NOT_DETECTED = 4 + FLAG_COMPULSORY_FIRE = 8 + FLAG_COMPULSORY_SUPPRESSION = 16 + FLAG_AUTO_MODE = 24 + FLAG_AUTO_MODE_RED_EYE = 25 + FLAG_NO_STROBE_FUNCTION = 32 + FLAG_RED_EYE_SUPPORT = 64 + + def __init__(self, filename: str): + super().__init__() + self._filename = resolve_android_path(filename) + self._attributes: Dict[str, str] = {} + self._load_exif() + + def _load_exif(self): + """尝试从文件中读取 EXIF 数据""" + if not self._filename or not os.path.exists(self._filename): + return + if HAS_PIL: + from PIL import Image + from PIL import ExifTags + try: + img = Image.open(self._filename) + exif_data = img._getexif() + if exif_data: + for tag_id, value in exif_data.items(): + tag_name = ExifTags.TAGS.get(tag_id, str(tag_id)) + self._attributes[str(tag_name)] = str(value) + except Exception: + self._attributes[self.TAG_MAKE] = "Unknown" + self._attributes[self.TAG_MODEL] = "Android Device" + self._attributes[self.TAG_ORIENTATION] = str(self.ORIENTATION_NORMAL) + self._attributes[self.TAG_IMAGE_WIDTH] = "1920" + self._attributes[self.TAG_IMAGE_LENGTH] = "1080" + else: + self._attributes[self.TAG_MAKE] = "Unknown" + self._attributes[self.TAG_MODEL] = "Android Device" + self._attributes[self.TAG_ORIENTATION] = str(self.ORIENTATION_NORMAL) + + def getAttribute(self, tag: str) -> Optional[str]: + return self._attributes.get(tag) + + def getAttributeInt(self, tag: str, defaultValue: int) -> int: + v = self._attributes.get(tag) + if v is not None: + try: + return int(v) + except (ValueError, TypeError): + return defaultValue + return defaultValue + + def getAttributeDouble(self, tag: str, defaultValue: float) -> float: + v = self._attributes.get(tag) + if v is not None: + try: + # Handle rational strings like "1/100" + if "/" in str(v): + num, den = str(v).split("/") + return float(num) / float(den) + return float(v) + except (ValueError, TypeError, ZeroDivisionError): + return defaultValue + return defaultValue + + def setAttribute(self, tag: str, value: str): + self._attributes[tag] = value + + def saveAttributes(self): + if not self._filename or not HAS_PIL: + return + from PIL import Image + try: + img = Image.open(self._filename) + exif_bytes = img.info.get("exif", b"") + img.save(self._filename, exif=exif_bytes) + except Exception: + pass + + def hasThumbnail(self) -> bool: + return False + + def getThumbnail(self) -> Optional[bytes]: + return None + + def getThumbnailRange(self) -> Optional[Tuple[int, int]]: + return None + + def getAttributeRange(self, tag: str) -> Optional[Tuple[int, int]]: + return None + + def setIsUri(self, isUri: bool): + pass + + def flipHorizontally(self): + pass + + def flipVertically(self): + pass + + def rotate(self, degree: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# CamcorderProfile +# ═══════════════════════════════════════════════════════════════ + +class CamcorderProfile(Stub): + """CamcorderProfile — 摄像机配置""" + + QUALITY_LOW = 0 + QUALITY_HIGH = 1 + QUALITY_QCIF = 2 + QUALITY_CIF = 3 + QUALITY_480P = 4 + QUALITY_720P = 5 + QUALITY_1080P = 6 + QUALITY_QVGA = 7 + QUALITY_2160P = 8 + QUALITY_VGA = 9 + QUALITY_4KDCI = 10 + QUALITY_QHD = 11 + QUALITY_2K = 12 + + def __init__(self): + super().__init__() + self.quality = CamcorderProfile.QUALITY_HIGH + self.fileFormat = MediaRecorder.OutputFormat.MPEG_4 + self.duration = 30 + self.videoCodec = MediaRecorder.VideoEncoder.H_264 + self.videoBitRate = 20000000 + self.videoFrameRate = 30 + self.videoFrameWidth = 1920 + self.videoFrameHeight = 1080 + self.audioCodec = MediaRecorder.AudioEncoder.AAC + self.audioBitRate = 192000 + self.audioSampleRate = 44100 + self.audioChannels = 2 + + @staticmethod + def get(cameraId: int, quality: int) -> Optional["CamcorderProfile"]: + if quality in (CamcorderProfile.QUALITY_LOW, CamcorderProfile.QUALITY_HIGH): + profile = CamcorderProfile() + profile.quality = quality + if quality == CamcorderProfile.QUALITY_LOW: + profile.videoFrameWidth = 640 + profile.videoFrameHeight = 480 + profile.videoBitRate = 500000 + profile.audioBitRate = 96000 + return profile + return None + + @staticmethod + def hasProfile(cameraId: int, quality: int) -> bool: + return True + + @staticmethod + def getAll(cameraId: int) -> List[int]: + return [ + CamcorderProfile.QUALITY_LOW, + CamcorderProfile.QUALITY_HIGH, + CamcorderProfile.QUALITY_480P, + CamcorderProfile.QUALITY_720P, + CamcorderProfile.QUALITY_1080P, + ] + + +# ═══════════════════════════════════════════════════════════════ +# EncoderProfiles +# ═══════════════════════════════════════════════════════════════ + +class EncoderProfiles(Stub): + """EncoderProfiles — 编码器配置 (API 31+)""" + + class VideoProfile(Stub): + def __init__(self): + super().__init__() + self.codec = "" + self.bitrate = 0 + self.width = 0 + self.height = 0 + self.frameRate = 0 + self.profile = 0 + self.bitDepth = 8 + self.chromaSubsampling = 420 + self.hdrFormat = 0 + + class AudioProfile(Stub): + def __init__(self): + super().__init__() + self.codec = "" + self.bitrate = 0 + self.sampleRate = 0 + self.channels = 0 + self.profile = 0 + + def __init__(self): + super().__init__() + self._default_duration = 30 + self._recommended_file_format = MediaRecorder.OutputFormat.MPEG_4 + self._video_profiles: List[EncoderProfiles.VideoProfile] = [] + self._audio_profiles: List[EncoderProfiles.AudioProfile] = [] + + def getDefaultDuration(self) -> int: + return self._default_duration + + def getRecommendedFileFormat(self) -> int: + return self._recommended_file_format + + def getVideoProfiles(self) -> List["VideoProfile"]: + return list(self._video_profiles) + + def getAudioProfiles(self) -> List["AudioProfile"]: + return list(self._audio_profiles) + + @staticmethod + def get(cameraId: int, quality: int) -> Optional["EncoderProfiles"]: + profiles = EncoderProfiles() + + vp = EncoderProfiles.VideoProfile() + vp.codec = MediaFormat.MIMETYPE_VIDEO_AVC + vp.bitrate = 20000000 + vp.width = 1920 + vp.height = 1080 + vp.frameRate = 30 + vp.profile = 8 # High Profile + profiles._video_profiles.append(vp) + + ap = EncoderProfiles.AudioProfile() + ap.codec = MediaFormat.MIMETYPE_AUDIO_AAC + ap.bitrate = 192000 + ap.sampleRate = 44100 + ap.channels = 2 + ap.profile = 2 # AAC LC + profiles._audio_profiles.append(ap) + + return profiles + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "MediaPlayer", + "MediaRecorder", + "SoundPool", + "AudioManager", + "AudioTrack", + "AudioRecord", + "Ringtone", + "RingtoneManager", + "MediaMetadataRetriever", + "MediaCodec", + "MediaCodecBufferInfo", + "MediaFormat", + "MediaMuxer", + "MediaExtractor", + "FaceDetector", + "FaceDetectorFace", + "ThumbnailUtils", + "MediaScannerConnection", + "MediaScannerConnectionClient", + "AudioAttributes", + "AudioFormat", + "MediaActionSound", + "ToneGenerator", + "ExifInterface", + "CamcorderProfile", + "EncoderProfiles", +] \ No newline at end of file diff --git a/android_pyapi/android/net.py b/android_pyapi/android/net.py new file mode 100644 index 0000000..3a6d6dc --- /dev/null +++ b/android_pyapi/android/net.py @@ -0,0 +1,1397 @@ +""" +Android Network 组件 — ConnectivityManager, NetworkInfo, Uri_Builder 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import socket +import struct +import threading +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union +from urllib.parse import urlparse, urlunparse, quote, unquote + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# Uri (re-export from content) +# ═══════════════════════════════════════════════════════════════ + +# Uri is defined in android.content; re-export the reference here. +from .content import Uri + +Uri_Builder = Uri.Builder + + +# ═══════════════════════════════════════════════════════════════ +# LocalSocket / LocalSocketAddress +# ═══════════════════════════════════════════════════════════════ + +class LocalSocketAddress(Stub): + """Android LocalSocketAddress""" + + class Namespace(Stub): + ABSTRACT = 0 + RESERVED = 1 + FILESYSTEM = 2 + + def __init__(self, name: str, namespace: int = Namespace.ABSTRACT): + super().__init__() + self._name = name + self._namespace = namespace + + def getName(self) -> str: + return self._name + + def getNamespace(self) -> int: + return self._namespace + + +class LocalSocket(Stub): + """Android LocalSocket — 基于 Unix domain socket 实现""" + + def __init__(self, sock_type: int = 1): # SOCK_STREAM + super().__init__() + self._sock: Optional[socket.socket] = None + self._sock_type = sock_type + self._input_stream: Optional[bytes] = b"" + self._output_stream: Optional[bytes] = b"" + self._closed = False + self._so_timeout: float = 0.0 + self._address: Optional[LocalSocketAddress] = None + + def connect(self, address: LocalSocketAddress, timeout: int = 0): + self._address = address + ns = address.getNamespace() + if ns == LocalSocketAddress.Namespace.ABSTRACT: + sock_path = "\0" + address.getName() + elif ns == LocalSocketAddress.Namespace.FILESYSTEM: + sock_path = address.getName() + else: + sock_path = os.path.join("/dev/socket", address.getName()) + try: + self._sock = socket.socket(socket.AF_UNIX, self._sock_type) + if timeout > 0: + self._sock.settimeout(timeout / 1000.0) + self._sock.connect(sock_path) + except (OSError, socket.error) as e: + raise AndroidOnlyError(f"LocalSocket.connect failed: {e}") + + def bind(self, address: LocalSocketAddress): + self._address = address + ns = address.getNamespace() + if ns == LocalSocketAddress.Namespace.ABSTRACT: + sock_path = "\0" + address.getName() + elif ns == LocalSocketAddress.Namespace.FILESYSTEM: + sock_path = address.getName() + else: + sock_path = os.path.join("/dev/socket", address.getName()) + try: + self._sock = socket.socket(socket.AF_UNIX, self._sock_type) + self._sock.bind(sock_path) + except (OSError, socket.error) as e: + raise AndroidOnlyError(f"LocalSocket.bind failed: {e}") + + def getInputStream(self): + return self._input_stream + + def getOutputStream(self): + return self._output_stream + + def close(self): + self._closed = True + if self._sock: + try: + self._sock.close() + except Exception: + pass + self._sock = None + + def setSoTimeout(self, timeout: int): + self._so_timeout = timeout / 1000.0 + if self._sock: + self._sock.settimeout(self._so_timeout) + + def getSoTimeout(self) -> int: + return int(self._so_timeout * 1000) + + def shutdownInput(self): + if self._sock: + try: + self._sock.shutdown(socket.SHUT_RD) + except Exception: + pass + + def shutdownOutput(self): + if self._sock: + try: + self._sock.shutdown(socket.SHUT_WR) + except Exception: + pass + + def isClosed(self) -> bool: + return self._closed + + def isConnected(self) -> bool: + return self._sock is not None + + def getRemoteSocketAddress(self) -> Optional[LocalSocketAddress]: + return self._address + + def toString(self) -> str: + return f"LocalSocket(address={self._address})" + + +# ═══════════════════════════════════════════════════════════════ +# NetworkUtils +# ═══════════════════════════════════════════════════════════════ + +class NetworkUtils(Stub): + """NetworkUtils — 网络工具""" + + @staticmethod + def getNetworkPartOfAddress(addr: str, prefix_len: int) -> str: + return addr + + @staticmethod + def intToInetAddress(host_address: int) -> str: + try: + return socket.inet_ntoa(struct.pack("!I", host_address & 0xFFFFFFFF)) + except Exception: + return "0.0.0.0" + + @staticmethod + def inetAddressToInt(addr: str) -> int: + try: + return struct.unpack("!I", socket.inet_aton(addr))[0] + except Exception: + return 0 + + @staticmethod + def isIPv6ULA(addr: str) -> bool: + return addr.startswith("fc") or addr.startswith("fd") + + @staticmethod + def isIPv6GlobalMulticast(addr: str) -> bool: + return addr.startswith("ff") + + @staticmethod + def prefixLengthToSubnetMask(prefix_len: int) -> int: + return (0xFFFFFFFF << (32 - prefix_len)) & 0xFFFFFFFF if prefix_len <= 32 else 0 + + +# ═══════════════════════════════════════════════════════════════ +# InetAddresses +# ═══════════════════════════════════════════════════════════════ + +class InetAddresses(Stub): + """InetAddresses — 地址解析工具""" + + @staticmethod + def isNumeric(address: str) -> bool: + try: + socket.inet_aton(address) + return True + except OSError: + return False + + @staticmethod + def parseNumericAddress(address: str) -> str: + return address + + @staticmethod + def getAddressFamily(address: str) -> int: + return 2 # AF_INET + + +# ═══════════════════════════════════════════════════════════════ +# NetworkSpecifier / MatchAllNetworkSpecifier +# ═══════════════════════════════════════════════════════════════ + +class NetworkSpecifier(Stub): + """NetworkSpecifier — 网络说明符基类""" + + def __init__(self): + super().__init__() + + def satisfiedBy(self, other: "NetworkSpecifier") -> bool: + return self is other + + def equals(self, other) -> bool: + return self is other + + def hashCode(self) -> int: + return hash(self.__class__.__name__) & 0x7FFFFFFF + + +class MatchAllNetworkSpecifier(NetworkSpecifier): + """匹配所有网络的说明符""" + + def __init__(self): + super().__init__() + + def satisfiedBy(self, other: NetworkSpecifier) -> bool: + return True + + def toString(self) -> str: + return "MatchAllNetworkSpecifier" + + +class WifiAwareNetworkSpecifier(NetworkSpecifier): + """WiFi Aware 网络说明符""" + + def __init__(self, role: int, client_id: int, peer_id: int, peer_mac: Optional[str] = None): + super().__init__() + self._role = role + self._client_id = client_id + self._peer_id = peer_id + self._peer_mac = peer_mac + + def getRole(self) -> int: + return self._role + + def getClientId(self) -> int: + return self._client_id + + def toString(self) -> str: + return f"WifiAwareNetworkSpecifier(role={self._role}, client={self._client_id})" + + +class EthernetNetworkSpecifier(NetworkSpecifier): + """以太网网络说明符""" + + def __init__(self, data: Optional[str] = None): + super().__init__() + self._data = data + + def getData(self) -> Optional[str]: + return self._data + + def toString(self) -> str: + return f"EthernetNetworkSpecifier(data={self._data})" + + +class TelephonyNetworkSpecifier(NetworkSpecifier): + """电话网络说明符""" + + def __init__(self, subscription_id: int = 0): + super().__init__() + self._subscription_id = subscription_id + + def getSubscriptionId(self) -> int: + return self._subscription_id + + def toString(self) -> str: + return f"TelephonyNetworkSpecifier(subId={self._subscription_id})" + + +# ═══════════════════════════════════════════════════════════════ +# NetworkState +# ═══════════════════════════════════════════════════════════════ + +class NetworkState(Stub): + """NetworkState — 网络状态""" + + def __init__(self, network: Optional["Network"] = None, + network_info: Optional["NetworkInfo"] = None, + link_properties: Optional["LinkProperties"] = None, + network_capabilities: Optional["NetworkCapabilities"] = None, + subscriber_id: Optional[str] = None): + super().__init__() + self.network = network + self.network_info = network_info + self.link_properties = link_properties + self.network_capabilities = network_capabilities + self.subscriber_id = subscriber_id + + def getNetwork(self) -> Optional["Network"]: + return self.network + + def getNetworkInfo(self) -> Optional["NetworkInfo"]: + return self.network_info + + def getLinkProperties(self) -> Optional["LinkProperties"]: + return self.link_properties + + def getNetworkCapabilities(self) -> Optional["NetworkCapabilities"]: + return self.network_capabilities + + def toString(self) -> str: + return f"NetworkState(network={self.network})" + + +# ═══════════════════════════════════════════════════════════════ +# NetworkInfo +# ═══════════════════════════════════════════════════════════════ + +class NetworkInfo(Stub): + """NetworkInfo — 网络连接信息""" + + class DetailedState(Stub): + CONNECTED = "CONNECTED" + CONNECTING = "CONNECTING" + DISCONNECTED = "DISCONNECTED" + SUSPENDED = "SUSPENDED" + IDLE = "IDLE" + FAILED = "FAILED" + BLOCKED = "BLOCKED" + VERIFYING_POOR_CONNECTION = "VERIFYING_POOR_CONNECTION" + CAPTIVE_PORTAL_CHECK = "CAPTIVE_PORTAL_CHECK" + OBTAINING_IPADDR = "OBTAINING_IPADDR" + + class State(Stub): + CONNECTED = "CONNECTED" + CONNECTING = "CONNECTING" + DISCONNECTED = "DISCONNECTED" + SUSPENDED = "SUSPENDED" + UNKNOWN = "UNKNOWN" + + def __init__(self, type_: int = 0, subtype: int = 0, + state: str = "DISCONNECTED", detailed_state: str = "DISCONNECTED", + type_name: str = "", subtype_name: str = "", + extra_info: Optional[str] = None, reason: Optional[str] = None, + failover: bool = False, roaming: bool = False, + is_available: bool = True): + super().__init__() + self._type = type_ + self._subtype = subtype + self._state = state + self._detailed_state = detailed_state + self._type_name = type_name or self._default_type_name(type_) + self._subtype_name = subtype_name + self._extra_info = extra_info + self._reason = reason + self._failover = failover + self._roaming = roaming + self._is_available = is_available + self._is_connected = (state == "CONNECTED") + self._is_connecting = (state == "CONNECTING") + + def _default_type_name(self, type_: int) -> str: + names = { + 0: "MOBILE", 1: "WIFI", 6: "WIMAX", 7: "BLUETOOTH", + 9: "ETHERNET", 17: "VPN", + } + return names.get(type_, "UNKNOWN") + + def getType(self) -> int: + return self._type + + def getSubtype(self) -> int: + return self._subtype + + def isConnected(self) -> bool: + return self._state == "CONNECTED" + + def isConnectedOrConnecting(self) -> bool: + return self._state in ("CONNECTED", "CONNECTING") + + def getState(self) -> str: + return self._state + + def getDetailedState(self) -> str: + return self._detailed_state + + def getTypeName(self) -> str: + return self._type_name + + def getSubtypeName(self) -> str: + return self._subtype_name + + def getExtraInfo(self) -> Optional[str]: + return self._extra_info + + def getReason(self) -> Optional[str]: + return self._reason + + def getFailover(self) -> bool: + return self._failover + + def getRoaming(self) -> bool: + return self._roaming + + def isAvailable(self) -> bool: + return self._is_available + + def isFailover(self) -> bool: + return self._failover + + def isRoaming(self) -> bool: + return self._roaming + + def toString(self) -> str: + return (f"NetworkInfo(type={self._type_name}[{self._type}], " + f"state={self._state}/{self._detailed_state}, " + f"roaming={self._roaming}, available={self._is_available})") + + def hashCode(self) -> int: + return self._type + + def equals(self, other) -> bool: + if isinstance(other, NetworkInfo): + return self._type == other._type and self._state == other._state + return False + + +# ═══════════════════════════════════════════════════════════════ +# Network +# ═══════════════════════════════════════════════════════════════ + +class Network(Stub): + """Network — 网络句柄""" + + def __init__(self, net_id: int = 0): + super().__init__() + self._net_id = net_id + + def getNetworkHandle(self) -> int: + return self._net_id + + def getNetworkCapabilities(self) -> Optional["NetworkCapabilities"]: + return None + + def getLinkProperties(self) -> Optional["LinkProperties"]: + return None + + def bindSocket(self, socket_obj: Any): + pass + + def bindSocketToNetwork(self, socket_obj: Any) -> bool: + return True + + def equals(self, other) -> bool: + if isinstance(other, Network): + return self._net_id == other._net_id + return False + + def hashCode(self) -> int: + return self._net_id + + def toString(self) -> str: + return f"Network({self._net_id})" + + +# ═══════════════════════════════════════════════════════════════ +# NetworkCapabilities +# ═══════════════════════════════════════════════════════════════ + +class NetworkCapabilities(Stub): + """NetworkCapabilities — 网络能力""" + + NET_CAPABILITY_INTERNET = 12 + NET_CAPABILITY_NOT_METERED = 11 + NET_CAPABILITY_NOT_ROAMING = 18 + NET_CAPABILITY_VALIDATED = 16 + NET_CAPABILITY_CAPTIVE_PORTAL = 17 + NET_CAPABILITY_NOT_VPN = 15 + NET_CAPABILITY_MMS = 0 + NET_CAPABILITY_SUPL = 1 + NET_CAPABILITY_DUN = 2 + NET_CAPABILITY_FOTA = 3 + NET_CAPABILITY_IMS = 4 + NET_CAPABILITY_CBS = 5 + NET_CAPABILITY_WIFI_P2P = 6 + NET_CAPABILITY_IA = 7 + NET_CAPABILITY_RCS = 8 + NET_CAPABILITY_XCAP = 9 + NET_CAPABILITY_EIMS = 10 + NET_CAPABILITY_NOT_CONGESTED = 13 + NET_CAPABILITY_NOT_RESTRICTED = 14 + NET_CAPABILITY_TRUSTED = 19 + NET_CAPABILITY_NOT_VCN_MANAGED = 20 + NET_CAPABILITY_PRIORITIZE_LATENCY = 21 + NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 22 + + TRANSPORT_WIFI = 1 + TRANSPORT_CELLULAR = 0 + TRANSPORT_ETHERNET = 3 + TRANSPORT_BLUETOOTH = 2 + TRANSPORT_VPN = 4 + TRANSPORT_WIFI_AWARE = 5 + TRANSPORT_LOWPAN = 6 + TRANSPORT_TEST = 7 + + SIGNAL_STRENGTH_UNSPECIFIED = -2147483648 + + def __init__(self): + super().__init__() + self._capabilities: Set[int] = set() + self._transports: Set[int] = set() + self._link_upstream_bw = 0 + self._link_downstream_bw = 0 + self._signal_strength = self.SIGNAL_STRENGTH_UNSPECIFIED + self._network_specifier: Optional[NetworkSpecifier] = None + self._owner_uid = -1 + self._ssid: Optional[str] = None + self._administrator_uids: List[int] = [] + self._enterprise_ids: List[int] = [] + + def hasCapability(self, capability: int) -> bool: + return capability in self._capabilities + + def hasTransport(self, transport: int) -> bool: + return transport in self._transports + + def addCapability(self, capability: int) -> "NetworkCapabilities": + self._capabilities.add(capability) + return self + + def removeCapability(self, capability: int) -> "NetworkCapabilities": + self._capabilities.discard(capability) + return self + + def getCapabilities(self) -> List[int]: + return sorted(self._capabilities) + + def getLinkUpstreamBandwidthKbps(self) -> int: + return self._link_upstream_bw + + def getLinkDownstreamBandwidthKbps(self) -> int: + return self._link_downstream_bw + + def setLinkUpstreamBandwidthKbps(self, bw: int): + self._link_upstream_bw = bw + + def setLinkDownstreamBandwidthKbps(self, bw: int): + self._link_downstream_bw = bw + + def addTransportType(self, transport: int) -> "NetworkCapabilities": + self._transports.add(transport) + return self + + def removeTransportType(self, transport: int) -> "NetworkCapabilities": + self._transports.discard(transport) + return self + + def getTransportInfo(self) -> Optional[Any]: + return None + + def setNetworkSpecifier(self, specifier: NetworkSpecifier): + self._network_specifier = specifier + + def getNetworkSpecifier(self) -> Optional[NetworkSpecifier]: + return self._network_specifier + + def getSignalStrength(self) -> int: + return self._signal_strength + + def setSignalStrength(self, strength: int): + self._signal_strength = strength + + def getOwnerUid(self) -> int: + return self._owner_uid + + def setOwnerUid(self, uid: int): + self._owner_uid = uid + + def getSsid(self) -> Optional[str]: + return self._ssid + + def setSsid(self, ssid: str): + self._ssid = ssid + + def getAdministratorUids(self) -> List[int]: + return list(self._administrator_uids) + + def setAdministratorUids(self, uids: List[int]): + self._administrator_uids = list(uids) + + def equals(self, other) -> bool: + if isinstance(other, NetworkCapabilities): + return (self._capabilities == other._capabilities + and self._transports == other._transports) + return False + + def hashCode(self) -> int: + return hash(frozenset(self._capabilities)) ^ hash(frozenset(self._transports)) + + def toString(self) -> str: + caps = ",".join(str(c) for c in sorted(self._capabilities)) + trans = ",".join(str(t) for t in sorted(self._transports)) + return f"NetworkCapabilities(caps=[{caps}], transports=[{trans}])" + + +# ═══════════════════════════════════════════════════════════════ +# NetworkRequest +# ═══════════════════════════════════════════════════════════════ + +class NetworkRequest(Stub): + """NetworkRequest — 网络请求""" + + class Builder(Stub): + """Builder 模式""" + + def __init__(self): + super().__init__() + self._capabilities: Set[int] = set() + self._transports: Set[int] = set() + self._network_specifier: Optional[NetworkSpecifier] = None + self._clear_capabilities: Set[int] = set() + + def addCapability(self, capability: int) -> "NetworkRequest.Builder": + self._capabilities.add(capability) + return self + + def removeCapability(self, capability: int) -> "NetworkRequest.Builder": + self._capabilities.discard(capability) + self._clear_capabilities.add(capability) + return self + + def addTransportType(self, transport: int) -> "NetworkRequest.Builder": + self._transports.add(transport) + return self + + def removeTransportType(self, transport: int) -> "NetworkRequest.Builder": + self._transports.discard(transport) + return self + + def setNetworkSpecifier(self, specifier: NetworkSpecifier) -> "NetworkRequest.Builder": + self._network_specifier = specifier + return self + + def build(self) -> "NetworkRequest": + request = NetworkRequest() + request._capabilities = set(self._capabilities) + request._transports = set(self._transports) + request._network_specifier = self._network_specifier + request._request_id = id(request) + return request + + def __init__(self): + super().__init__() + self._capabilities: Set[int] = set() + self._transports: Set[int] = set() + self._network_specifier: Optional[NetworkSpecifier] = None + self._request_id = 0 + + def hasCapability(self, capability: int) -> bool: + return capability in self._capabilities + + def hasTransport(self, transport: int) -> bool: + return transport in self._transports + + def getCapabilities(self) -> Set[int]: + return set(self._capabilities) + + def getTransports(self) -> Set[int]: + return set(self._transports) + + def getNetworkSpecifier(self) -> Optional[NetworkSpecifier]: + return self._network_specifier + + def getRequestId(self) -> int: + return self._request_id + + def toString(self) -> str: + return f"NetworkRequest(id={self._request_id})" + + +# ═══════════════════════════════════════════════════════════════ +# NetworkCallback +# ═══════════════════════════════════════════════════════════════ + +class NetworkCallback(Stub): + """NetworkCallback — 网络回调""" + + def __init__(self, flags: int = 0): + super().__init__() + self._flags = flags + + def onAvailable(self, network: Network): + pass + + def onLost(self, network: Network): + pass + + def onCapabilitiesChanged(self, network: Network, capabilities: NetworkCapabilities): + pass + + def onLinkPropertiesChanged(self, network: Network, link_properties: "LinkProperties"): + pass + + def onNetworkSuspended(self, network: Network): + pass + + def onResumed(self, network: Network): + pass + + def onBlockedStatusChanged(self, network: Network, blocked: bool): + pass + + def onLosing(self, network: Network, max_ms_to_live: int): + pass + + def onPreCheck(self, network: Network) -> bool: + return True + + def onUnavailable(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# LinkAddress +# ═══════════════════════════════════════════════════════════════ + +class LinkAddress(Stub): + """LinkAddress — 链接地址""" + + def __init__(self, address: str = "0.0.0.0", + prefix_length: int = 24, + flags: int = 0, + scope: int = 0): + super().__init__() + self._address = address + self._prefix_length = prefix_length + self._flags = flags + self._scope = scope + self._is_ipv6 = ":" in address + + def getAddress(self) -> str: + return self._address + + def getPrefixLength(self) -> int: + return self._prefix_length + + def getFlags(self) -> int: + return self._flags + + def getScope(self) -> int: + return self._scope + + def isIpv6(self) -> bool: + return self._is_ipv6 + + def toString(self) -> str: + return f"{self._address}/{self._prefix_length}" + + def equals(self, other) -> bool: + if isinstance(other, LinkAddress): + return self._address == other._address + return False + + def hashCode(self) -> int: + return hash(self._address) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# RouteInfo +# ═══════════════════════════════════════════════════════════════ + +class RouteInfo(Stub): + """RouteInfo — 路由信息""" + + RTN_UNICAST = 1 + RTN_UNREACHABLE = 2 + RTN_BROADCAST = 3 + RTN_THROW = 4 + RTN_OTHER = 5 + + def __init__(self, destination: Optional[LinkAddress] = None, + gateway: str = "", + interface_name: str = "", + is_default: bool = False, + type_: int = RTN_UNICAST): + super().__init__() + self._destination = destination + self._gateway = gateway + self._interface_name = interface_name + self._is_default = is_default + self._type = type_ + + def getDestination(self) -> Optional[LinkAddress]: + return self._destination + + def getGateway(self) -> str: + return self._gateway + + def getInterface(self) -> str: + return self._interface_name + + def getType(self) -> int: + return self._type + + def isDefaultRoute(self) -> bool: + return self._is_default + + def isIPv4Default(self) -> bool: + return self._is_default and self._destination is not None and not self._destination.isIpv6() + + def isIPv6Default(self) -> bool: + return self._is_default and self._destination is not None and self._destination.isIpv6() + + def toString(self) -> str: + dest = str(self._destination) if self._destination else "0.0.0.0/0" + gw = self._gateway or "0.0.0.0" + return f"RouteInfo({dest} -> {gw} via {self._interface_name})" + + def equals(self, other) -> bool: + if isinstance(other, RouteInfo): + return (self._gateway == other._gateway + and self._interface_name == other._interface_name) + return False + + def hashCode(self) -> int: + return hash(self._gateway) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# LinkProperties +# ═══════════════════════════════════════════════════════════════ + +class LinkProperties(Stub): + """LinkProperties — 链接属性""" + + def __init__(self): + super().__init__() + self._link_addresses: List[LinkAddress] = [] + self._routes: List[RouteInfo] = [] + self._dns_servers: List[str] = [] + self._interface_name: str = "" + self._mtu: int = 1500 + self._http_proxy: Optional[ProxyInfo] = None + self._private_dns_server_name: str = "" + self._private_dns_active: bool = False + self._domains: str = "" + self._pcscf: List[str] = [] + + def getLinkAddresses(self) -> List[LinkAddress]: + return list(self._link_addresses) + + def getRoutes(self) -> List[RouteInfo]: + return list(self._routes) + + def getDnsServers(self) -> List[str]: + return list(self._dns_servers) + + def getInterfaceName(self) -> str: + return self._interface_name + + def setInterfaceName(self, name: str): + self._interface_name = name + + def addLinkAddress(self, address: LinkAddress): + if address not in self._link_addresses: + self._link_addresses.append(address) + + def removeLinkAddress(self, address: LinkAddress): + self._link_addresses = [a for a in self._link_addresses if a != address] + + def addRoute(self, route: RouteInfo): + if route not in self._routes: + self._routes.append(route) + + def removeRoute(self, route: RouteInfo): + self._routes = [r for r in self._routes if r != route] + + def addDnsServer(self, dns: str): + if dns not in self._dns_servers: + self._dns_servers.append(dns) + + def removeDnsServer(self, dns: str): + self._dns_servers = [d for d in self._dns_servers if d != dns] + + def getMtu(self) -> int: + return self._mtu + + def setMtu(self, mtu: int): + self._mtu = mtu + + def getHttpProxy(self) -> Optional["ProxyInfo"]: + return self._http_proxy + + def setHttpProxy(self, proxy: "ProxyInfo"): + self._http_proxy = proxy + + def getPrivateDnsServerName(self) -> str: + return self._private_dns_server_name + + def setPrivateDnsServerName(self, name: str): + self._private_dns_server_name = name + self._private_dns_active = bool(name) + + def isPrivateDnsActive(self) -> bool: + return self._private_dns_active + + def setLinkAddresses(self, addresses: List[LinkAddress]): + self._link_addresses = list(addresses) + + def setDnsServers(self, dns_servers: List[str]): + self._dns_servers = list(dns_servers) + + def getDomains(self) -> str: + return self._domains + + def setDomains(self, domains: str): + self._domains = domains + + def getPcscf(self) -> List[str]: + return list(self._pcscf) + + def addPcscf(self, addr: str): + if addr not in self._pcscf: + self._pcscf.append(addr) + + def setMtuV4(int_mtu: int): + pass + + def setMtuV6(int_mtu: int): + pass + + def clearLinkAddresses(self): + self._link_addresses.clear() + + def clearRoutes(self): + self._routes.clear() + + def clearDnsServers(self): + self._dns_servers.clear() + + def toString(self) -> str: + iface = self._interface_name or "none" + return f"LinkProperties(iface={iface}, mtu={self._mtu}, dns={self._dns_servers})" + + def equals(self, other) -> bool: + if isinstance(other, LinkProperties): + return (self._interface_name == other._interface_name + and self._mtu == other._mtu) + return False + + def hashCode(self) -> int: + return hash(self._interface_name) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# DnsResolver +# ═══════════════════════════════════════════════════════════════ + +class DnsResolver(Stub): + """DnsResolver — DNS 解析器""" + + CLASS_IN = 1 + TYPE_A = 1 + TYPE_AAAA = 28 + + @staticmethod + def getInstance() -> "DnsResolver": + return DnsResolver() + + def query(self, hostname: str, callback: Any, flags: int = 0): + try: + result = socket.getaddrinfo(hostname, None) + callback.onAnswer(None, 0, result) + except socket.gaierror: + callback.onAnswer(None, -1, None) + + +# ═══════════════════════════════════════════════════════════════ +# DhcpInfo +# ═══════════════════════════════════════════════════════════════ + +class DhcpInfo(Stub): + """DhcpInfo — DHCP 信息""" + + def __init__(self): + super().__init__() + self.ipAddress = 0 + self.gateway = 0 + self.netmask = 0 + self.dns1 = 0 + self.dns2 = 0 + self.serverAddress = 0 + self.leaseDuration = 0 + + def toString(self) -> str: + return (f"DhcpInfo(ip={self.ipAddress}, gw={self.gateway}, " + f"mask={self.netmask}, dns1={self.dns1}, dns2={self.dns2})") + + +# ═══════════════════════════════════════════════════════════════ +# SSLSessionCache +# ═══════════════════════════════════════════════════════════════ + +class SSLSessionCache(Stub): + """SSLSessionCache — SSL 会话缓存""" + + def __init__(self, context_or_dir: Any): + super().__init__() + self._dir: Optional[str] = None + if isinstance(context_or_dir, str): + self._dir = context_or_dir + else: + self._dir = os.path.join(_app_data_dir, "ssl_cache") + + def getDir(self) -> Optional[str]: + return self._dir + + +# ═══════════════════════════════════════════════════════════════ +# TrafficStats +# ═══════════════════════════════════════════════════════════════ + +class TrafficStats(Stub): + """TrafficStats — 流量统计""" + + UID_TETHERING = "UNSUPPORTED" + UNSUPPORTED = -1 + TAG_SYSTEM_UPDATE = 0 + + @staticmethod + def getMobileTxBytes() -> int: + return 0 + + @staticmethod + def getMobileRxBytes() -> int: + return 0 + + @staticmethod + def getTotalTxBytes() -> int: + return 0 + + @staticmethod + def getTotalRxBytes() -> int: + return 0 + + @staticmethod + def getMobileTxPackets() -> int: + return 0 + + @staticmethod + def getMobileRxPackets() -> int: + return 0 + + @staticmethod + def getTotalTxPackets() -> int: + return 0 + + @staticmethod + def getTotalRxPackets() -> int: + return 0 + + @staticmethod + def getUidTxBytes(uid: int) -> int: + return 0 + + @staticmethod + def getUidRxBytes(uid: int) -> int: + return 0 + + @staticmethod + def getUidTxPackets(uid: int) -> int: + return 0 + + @staticmethod + def getUidRxPackets(uid: int) -> int: + return 0 + + @staticmethod + def getUidTcpTxBytes(uid: int) -> int: + return 0 + + @staticmethod + def getUidTcpRxBytes(uid: int) -> int: + return 0 + + @staticmethod + def getUidUdpTxBytes(uid: int) -> int: + return 0 + + @staticmethod + def getUidUdpRxBytes(uid: int) -> int: + return 0 + + @staticmethod + def setThreadStatsTag(tag: int): + pass + + @staticmethod + def clearThreadStatsTag(): + pass + + @staticmethod + def getThreadStatsTag() -> int: + return 0 + + @staticmethod + def setThreadStatsUid(uid: int): + pass + + @staticmethod + def clearThreadStatsUid(): + pass + + @staticmethod + def incrementOperationCount(uid: int, tag: int, count: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# Proxy / ProxyInfo +# ═══════════════════════════════════════════════════════════════ + +class Proxy(Stub): + """Proxy — 系统代理信息""" + + @staticmethod + def getDefaultHost() -> Optional[str]: + return None + + @staticmethod + def getDefaultPort() -> int: + return -1 + + +class ProxyInfo(Stub): + """ProxyInfo — 代理详细信息""" + + def __init__(self, host: str = "", port: int = 0, + pac_file_url: Optional[str] = None, + username: str = "", password: str = "", + exclusion_list: Optional[List[str]] = None): + super().__init__() + self._host = host + self._port = port + self._pac_file_url = pac_file_url + self._username = username + self._password = password + self._exclusion_list = list(exclusion_list) if exclusion_list else [] + self._valid = bool(host) or bool(pac_file_url) + + def getHost(self) -> str: + return self._host + + def getPort(self) -> int: + return self._port + + def getPacFileUrl(self) -> Optional[str]: + return self._pac_file_url + + def getUsername(self) -> str: + return self._username + + def getPassword(self) -> str: + return self._password + + def getExclusionList(self) -> List[str]: + return list(self._exclusion_list) + + def isValid(self) -> bool: + return self._valid + + def makeProxy(self, url: str): + return self + + def equals(self, other) -> bool: + if isinstance(other, ProxyInfo): + return self._host == other._host and self._port == other._port + return False + + def hashCode(self) -> int: + return hash(self._host) ^ self._port + + def toString(self) -> str: + pac = f" pac={self._pac_file_url}" if self._pac_file_url else "" + return f"ProxyInfo(host={self._host}:{self._port}{pac})" + + +# ═══════════════════════════════════════════════════════════════ +# ConnectivityManager +# ═══════════════════════════════════════════════════════════════ + +class ConnectivityManager(Stub): + """ConnectivityManager — 连接管理器""" + + TYPE_WIFI = 1 + TYPE_MOBILE = 0 + TYPE_BLUETOOTH = 7 + TYPE_ETHERNET = 9 + TYPE_VPN = 17 + TYPE_WIMAX = 6 + TYPE_DUMMY = 8 + TYPE_PROXY = 16 + + CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE" + + DEFAULT_NETWORK_PREFERENCE = 1 + + def __init__(self): + super().__init__() + self._networks: Dict[int, "Network"] = {} + self._callbacks: List[Tuple[NetworkCallback, Optional[NetworkRequest]]] = [] + self._active_network_info = NetworkInfo( + type_=self.TYPE_WIFI, state="CONNECTED", detailed_state="CONNECTED", + type_name="WIFI", is_available=True, + ) + self._net_id_counter = 0 + self._network_caps: Dict[int, NetworkCapabilities] = {} + self._link_props: Dict[int, LinkProperties] = {} + + def getActiveNetworkInfo(self) -> Optional[NetworkInfo]: + return self._active_network_info + + def getNetworkInfo(self, network_type: int) -> Optional[NetworkInfo]: + network = self._networks.get(network_type) + if network: + return NetworkInfo(type_=network_type, state="CONNECTED", is_available=True) + return self._active_network_info + + def getNetworkInfoForUid(self, network: Network, uid: int) -> Optional[NetworkInfo]: + return self._active_network_info + + def getAllNetworks(self) -> List[Network]: + return list(self._networks.values()) + + def getActiveNetwork(self) -> Optional[Network]: + if self._networks: + nid = next(iter(self._networks)) + return self._networks[nid] + net = Network(1) + self._networks[1] = net + return net + + def getActiveNetworkForUid(self, uid: int) -> Optional[Network]: + return self.getActiveNetwork() + + def getNetworkCapabilities(self, network: Network) -> Optional[NetworkCapabilities]: + caps = self._network_caps.get(network.getNetworkHandle()) + if caps is None: + caps = NetworkCapabilities() + caps.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + caps.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + caps.addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + caps.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + self._network_caps[network.getNetworkHandle()] = caps + return caps + + def getLinkProperties(self, network: Network) -> Optional[LinkProperties]: + lp = self._link_props.get(network.getNetworkHandle()) + if lp is None: + lp = LinkProperties() + self._link_props[network.getNetworkHandle()] = lp + return lp + + def requestNetwork(self, request: NetworkRequest, callback: NetworkCallback, timeout_ms: int = 0, + handler: Any = None, network_specifier: int = 0) -> None: + self._callbacks.append((callback, request)) + net = self.getActiveNetwork() + if net: + try: + callback.onAvailable(net) + except Exception: + pass + + def registerDefaultNetworkCallback(self, callback: NetworkCallback, handler: Any = None) -> None: + self._callbacks.append((callback, None)) + net = self.getActiveNetwork() + if net: + try: + callback.onAvailable(net) + except Exception: + pass + + def registerNetworkCallback(self, request: NetworkRequest, callback: NetworkCallback, + handler: Any = None) -> None: + self._callbacks.append((callback, request)) + net = self.getActiveNetwork() + if net: + try: + callback.onAvailable(net) + except Exception: + pass + + def unregisterNetworkCallback(self, callback_or_request: Any): + if isinstance(callback_or_request, NetworkCallback): + self._callbacks = [ + (cb, req) for cb, req in self._callbacks + if cb is not callback_or_request + ] + elif isinstance(callback_or_request, NetworkRequest): + self._callbacks = [ + (cb, req) for cb, req in self._callbacks + if req is not callback_or_request + ] + + def bindProcessToNetwork(self, network: Optional[Network]) -> bool: + return True + + def getProcessDefaultNetwork(self) -> Optional[Network]: + return self.getActiveNetwork() + + @staticmethod + def setProcessDefaultNetwork(network: Optional[Network]) -> bool: + return True + + def getAllNetworkInfo(self) -> List[NetworkInfo]: + return [self._active_network_info] + + def getNetworkInfoForProcess(self) -> Optional[NetworkInfo]: + return self._active_network_info + + def isDefaultNetworkActive(self) -> bool: + return True + + def addDefaultNetworkActiveListener(self, listener: Any): + pass + + def removeDefaultNetworkActiveListener(self, listener: Any): + pass + + def reportBadNetwork(self, network: Network): + pass + + def setNetworkPreference(self, preference: int): + pass + + def getNetworkPreference(self) -> int: + return self.DEFAULT_NETWORK_PREFERENCE + + def startCaptivePortalApp(self, network: Network): + pass + + def setProcessDefaultNetworkForUid(self, uid: int) -> bool: + return True + + def registerNetworkAgent(self, agent: Any) -> int: + return 0 + + def toString(self) -> str: + return f"ConnectivityManager(networks={len(self._networks)})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Uri", + "Uri_Builder", + "ConnectivityManager", + "NetworkInfo", + "Network", + "NetworkCapabilities", + "NetworkRequest", + "NetworkCallback", + "LinkProperties", + "LinkAddress", + "RouteInfo", + "DnsResolver", + "DhcpInfo", + "InetAddresses", + "SSLSessionCache", + "TrafficStats", + "Proxy", + "ProxyInfo", + "NetworkSpecifier", + "MatchAllNetworkSpecifier", + "WifiAwareNetworkSpecifier", + "EthernetNetworkSpecifier", + "TelephonyNetworkSpecifier", + "NetworkState", + "LocalSocket", + "LocalSocketAddress", + "NetworkUtils", +] \ No newline at end of file diff --git a/android_pyapi/android/net_wifi.py b/android_pyapi/android/net_wifi.py new file mode 100644 index 0000000..9371da6 --- /dev/null +++ b/android_pyapi/android/net_wifi.py @@ -0,0 +1,1020 @@ +""" +Android WiFi 组件 — WifiManager, WifiInfo, ScanResult 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import json +import random +from typing import Any, Dict, List, Optional, Set, Tuple, Union +from datetime import datetime + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# SupplicantState +# ═══════════════════════════════════════════════════════════════ + +class SupplicantState(Stub): + """SupplicantState — wpa_supplicant 状态枚举""" + + ASSOCIATED = "ASSOCIATED" + ASSOCIATING = "ASSOCIATING" + COMPLETED = "COMPLETED" + DISCONNECTED = "DISCONNECTED" + DORMANT = "DORMANT" + FOUR_WAY_HANDSHAKE = "FOUR_WAY_HANDSHAKE" + GROUP_HANDSHAKE = "GROUP_HANDSHAKE" + INACTIVE = "INACTIVE" + INTERFACE_DISABLED = "INTERFACE_DISABLED" + INVALID = "INVALID" + SCANNING = "SCANNING" + UNINITIALIZED = "UNINITIALIZED" + + _VALID_STATES = [ + "ASSOCIATED", "ASSOCIATING", "COMPLETED", "DISCONNECTED", + "DORMANT", "FOUR_WAY_HANDSHAKE", "GROUP_HANDSHAKE", "INACTIVE", + "INTERFACE_DISABLED", "INVALID", "SCANNING", "UNINITIALIZED", + ] + + def __init__(self, state: str = "DISCONNECTED"): + super().__init__() + self._state = state if state in self._VALID_STATES else "INVALID" + + def getState(self) -> str: + return self._state + + def toString(self) -> str: + return self._state + + @staticmethod + def isValid(state_str: str) -> bool: + return state_str in SupplicantState._VALID_STATES + + def compareTo(self, other: "SupplicantState") -> int: + if self._state == other._state: + return 0 + return -1 if (self._VALID_STATES.index(self._state) + < self._VALID_STATES.index(other._state)) else 1 + + def equals(self, other) -> bool: + if isinstance(other, SupplicantState): + return self._state == other._state + return False + + def hashCode(self) -> int: + return hash(self._state) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# WifiSsid +# ═══════════════════════════════════════════════════════════════ + +class WifiSsid(Stub): + """WifiSsid — WiFi SSID 封装""" + + def __init__(self, ssid: str = ""): + super().__init__() + self._ssid = ssid + + @staticmethod + def createFromAsciiBytes(ssid_bytes: bytes) -> "WifiSsid": + try: + return WifiSsid(ssid_bytes.decode("utf-8")) + except UnicodeDecodeError: + return WifiSsid(repr(ssid_bytes)) + + @staticmethod + def createFromHex(hex_str: str) -> "WifiSsid": + try: + decoded = bytes.fromhex(hex_str).decode("utf-8", errors="replace") + return WifiSsid(decoded) + except Exception: + return WifiSsid(hex_str) + + def toString(self) -> str: + return self._ssid + + +# ═══════════════════════════════════════════════════════════════ +# WifiConfiguration +# ═══════════════════════════════════════════════════════════════ + +class WifiConfiguration(Stub): + """WifiConfiguration — WiFi 网络配置""" + + SECURITY_OPEN = 0 + SECURITY_WEP = 1 + SECURITY_PSK = 2 + SECURITY_EAP = 3 + SECURITY_SAE = 4 + SECURITY_OWE = 5 + + class KeyMgmt(Stub): + NONE = 0 + WPA_PSK = 1 + WPA_EAP = 2 + IEEE8021X = 3 + WPA2_PSK = 4 + OSEN = 5 + FT_PSK = 6 + FT_EAP = 7 + SAE = 8 + OWE = 9 + SUITE_B_192 = 10 + WPA3_SAE = 8 + WPA3_OWE = 9 + + class GroupCipher(Stub): + NONE = 0 + TKIP = 1 + CCMP = 2 + TKIP_CCMP = 3 + AES = 4 + SMS4 = 5 + GCMP_128 = 6 + GCMP_256 = 7 + BIP_GMAC_128 = 8 + BIP_GMAC_256 = 9 + BIP_CMAC_256 = 10 + + class PairwiseCipher(Stub): + NONE = 0 + TKIP = 1 + CCMP = 2 + NONE_ = 3 + GCMP_128 = 4 + GCMP_256 = 5 + SMS4 = 6 + + class AuthAlgorithm(Stub): + OPEN = 0 + SHARED = 1 + LEAP = 2 + + class Protocol(Stub): + RSN = 0 + WPA = 1 + OSEN = 2 + + class Status(Stub): + CURRENT = 0 + DISABLED = 1 + ENABLED = 2 + + def __init__(self): + super().__init__() + self.SSID: Optional[str] = None + self.BSSID: Optional[str] = None + self.preSharedKey: Optional[str] = None + self.status: int = WifiConfiguration.Status.DISABLED + self.networkId: int = -1 + self.allowedKeyManagement: Set[int] = set() + self.allowedGroupCiphers: Set[int] = set() + self.allowedPairwiseCiphers: Set[int] = set() + self.allowedAuthAlgorithms: Set[int] = set() + self.allowedProtocols: Set[int] = set() + self.hiddenSSID: bool = False + self.priority: int = 0 + self.wepKeys: List[Optional[str]] = [None, None, None, None] + self.wepTxKeyIndex: int = 0 + self.isEnterprise: bool = False + self._enterprise_config: Optional["WifiEnterpriseConfig"] = None + + def getNetworkId(self) -> int: + return self.networkId + + def toString(self) -> str: + ssid = self.SSID or "(none)" + return f"WifiConfiguration(SSID={ssid}, id={self.networkId})" + + def hashCode(self) -> int: + return hash(self.SSID) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# WifiEnterpriseConfig +# ═══════════════════════════════════════════════════════════════ + +class WifiEnterpriseConfig(Stub): + """WifiEnterpriseConfig — 企业级 WiFi 配置""" + + def __init__(self): + super().__init__() + self._identity: str = "" + self._password: str = "" + self._ca_cert: Optional[str] = None + self._client_cert: Optional[str] = None + self._subject_match: str = "" + self._alt_subject_match: str = "" + self._domain_suffix_match: str = "" + self._eap_method: int = -1 + self._phase2_method: int = -1 + + def setIdentity(self, identity: str): + self._identity = identity + + def getIdentity(self) -> str: + return self._identity + + def setPassword(self, password: str): + self._password = password + + def getPassword(self) -> str: + return self._password + + def setCaCertificate(self, cert: Any): + pass + + def getCaCertificate(self) -> Optional[Any]: + return None + + def setClientCertificate(self, cert: Any): + pass + + def getClientCertificate(self) -> Optional[Any]: + return None + + def setSubjectMatch(self, match: str): + self._subject_match = match + + def getSubjectMatch(self) -> str: + return self._subject_match + + def setDomainSuffixMatch(self, match: str): + self._domain_suffix_match = match + + def getDomainSuffixMatch(self) -> str: + return self._domain_suffix_match + + def toString(self) -> str: + return f"WifiEnterpriseConfig(identity={self._identity})" + + +# ═══════════════════════════════════════════════════════════════ +# WifiInfo +# ═══════════════════════════════════════════════════════════════ + +class WifiInfo(Stub): + """WifiInfo — WiFi 连接信息""" + + LINK_SPEED_UNITS = "Mbps" + STANDARD_LEGACY = 1 + STANDARD_11N = 4 + STANDARD_11AC = 5 + STANDARD_11AX = 6 + STANDARD_11BE = 7 + SECURITY_TYPE_OPEN = 0 + SECURITY_TYPE_WEP = 1 + SECURITY_TYPE_PSK = 2 + SECURITY_TYPE_EAP = 3 + SECURITY_TYPE_SAE = 4 + SECURITY_TYPE_OWE = 5 + SECURITY_TYPE_WAPI_PSK = 6 + SECURITY_TYPE_PASSPOINT_R1_R2 = 7 + SECURITY_TYPE_PASSPOINT_R3 = 8 + + INVALID_RSSI = -127 + DEFAULT_RSSI = -60 + + def __init__(self): + super().__init__() + self._ssid: str = "" + self._bssid: str = "00:00:00:00:00:00" + self._rssi: int = self.INVALID_RSSI + self._link_speed: int = 0 + self._frequency: int = 0 + self._ip_address: int = 0 + self._mac_address: str = "02:00:00:00:00:00" + self._network_id: int = -1 + self._supplicant_state: SupplicantState = SupplicantState(SupplicantState.DISCONNECTED) + self._wifi_standard: int = self.STANDARD_LEGACY + self._is_hidden_ssid: bool = False + self._tx_link_speed: int = 0 + self._rx_link_speed: int = 0 + self._max_rx_link_speed: int = 0 + self._max_tx_link_speed: int = 0 + self._security_type: int = self.SECURITY_TYPE_OPEN + self._rssi_timestamp: int = 0 + + def getSSID(self) -> str: + return self._ssid + + def getBSSID(self) -> str: + return self._bssid + + def getRssi(self) -> int: + return self._rssi + + def getLinkSpeed(self) -> int: + return self._link_speed + + def getFrequency(self) -> int: + return self._frequency + + def getIpAddress(self) -> int: + return self._ip_address + + def getMacAddress(self) -> str: + return self._mac_address + + def getNetworkId(self) -> int: + return self._network_id + + def getSupplicantState(self) -> SupplicantState: + return self._supplicant_state + + def getWifiStandard(self) -> int: + return self._wifi_standard + + def isHiddenSSID(self) -> bool: + return self._is_hidden_ssid + + def getTxLinkSpeedMbps(self) -> int: + return self._tx_link_speed + + def getRxLinkSpeedMbps(self) -> int: + return self._rx_link_speed + + def getMaxSupportedRxLinkSpeedMbps(self) -> int: + return self._max_rx_link_speed + + def getMaxSupportedTxLinkSpeedMbps(self) -> int: + return self._max_tx_link_speed + + def getSecurityType(self) -> int: + return self._security_type + + def getRssiTimestamp(self) -> int: + return self._rssi_timestamp + + def toString(self) -> str: + return (f"WifiInfo(SSID={self._ssid}, BSSID={self._bssid}, " + f"rssi={self._rssi}, speed={self._link_speed} {self.LINK_SPEED_UNITS})") + + def hashCode(self) -> int: + return hash(self._bssid) & 0x7FFFFFFF + + +# ═══════════════════════════════════════════════════════════════ +# ScanResult +# ═══════════════════════════════════════════════════════════════ + +class ScanResult(Stub): + """ScanResult — WiFi 扫描结果""" + + WIFI_BAND_24_GHZ = 0 + WIFI_BAND_5_GHZ = 1 + WIFI_BAND_6_GHZ = 2 + CHANNEL_WIDTH_20MHZ = 0 + CHANNEL_WIDTH_40MHZ = 1 + CHANNEL_WIDTH_80MHZ = 2 + CHANNEL_WIDTH_160MHZ = 3 + CHANNEL_WIDTH_80MHZ_PLUS_MHZ = 4 + + UNSPECIFIED = -1 + + def __init__(self): + super().__init__() + self.SSID: str = "" + self.BSSID: str = "" + self.capabilities: str = "" + self.level: int = 0 + self.frequency: int = 0 + self.timestamp: int = 0 + self.channelWidth: int = self.CHANNEL_WIDTH_20MHZ + self.centerFreq0: int = 0 + self.centerFreq1: int = 0 + self.operatorFriendlyName: str = "" + self.venueName: str = "" + self.is80211mcResponder: bool = False + self.isPasspointNetwork: bool = False + self._wifi_standard: int = 0 + self._seen: int = 0 + self._distance: Optional[float] = None + self._distance_sd: Optional[float] = None + + def getSSID(self) -> str: + return self.SSID + + def getBSSID(self) -> str: + return self.BSSID + + def getCapabilities(self) -> str: + return self.capabilities + + def getLevel(self) -> int: + return self.level + + def getFrequency(self) -> int: + return self.frequency + + def getTimestamp(self) -> int: + return self.timestamp + + def getChannelWidth(self) -> int: + return self.channelWidth + + def getCenterFreq0(self) -> int: + return self.centerFreq0 + + def getCenterFreq1(self) -> int: + return self.centerFreq1 + + def getOperatorFriendlyName(self) -> str: + return self.operatorFriendlyName + + def getVenueName(self) -> str: + return self.venueName + + def is80211mcResponder(self) -> bool: + return self.is80211mcResponder + + def isPasspointNetwork(self) -> bool: + return self.isPasspointNetwork + + def getWifiStandard(self) -> int: + return self._wifi_standard + + def getDistance(self) -> Optional[float]: + return self._distance + + def getDistanceSd(self) -> Optional[float]: + return self._distance_sd + + def toString(self) -> str: + return f"ScanResult(SSID={self.SSID}, BSSID={self.BSSID}, level={self.level})" + + +# ═══════════════════════════════════════════════════════════════ +# WifiNetworkSpecifier +# ═══════════════════════════════════════════════════════════════ + +class WifiNetworkSpecifier(Stub): + """WifiNetworkSpecifier — WiFi 网络说明符""" + + def __init__(self, ssid: Optional[str] = None, bssid: Optional[str] = None, + band: int = 0, security_type: int = 0): + super().__init__() + self._ssid = ssid + self._bssid = bssid + self._band = band + self._security_type = security_type + + def getSsid(self) -> Optional[str]: + return self._ssid + + def getBssid(self) -> Optional[str]: + return self._bssid + + def getBand(self) -> int: + return self._band + + def toString(self) -> str: + return f"WifiNetworkSpecifier(SSID={self._ssid})" + + +# ═══════════════════════════════════════════════════════════════ +# WifiNetworkSuggestion +# ═══════════════════════════════════════════════════════════════ + +class WifiNetworkSuggestion(Stub): + """WifiNetworkSuggestion — WiFi 网络建议""" + + def __init__(self, ssid: str = "", bssid: Optional[str] = None, + is_hidden: bool = False, priority: int = 0, + wpa2_passphrase: Optional[str] = None, + wpa3_passphrase: Optional[str] = None, + is_owe: bool = False): + super().__init__() + self._ssid = ssid + self._bssid = bssid + self._is_hidden = is_hidden + self._priority = priority + self._wpa2_passphrase = wpa2_passphrase + self._wpa3_passphrase = wpa3_passphrase + self._is_owe = is_owe + + def getSsid(self) -> str: + return self._ssid + + def getBssid(self) -> Optional[str]: + return self._bssid + + def isHidden(self) -> bool: + return self._is_hidden + + def getPriority(self) -> int: + return self._priority + + def toString(self) -> str: + return f"WifiNetworkSuggestion(SSID={self._ssid})" + + +# ═══════════════════════════════════════════════════════════════ +# WifiManager helpers +# ═══════════════════════════════════════════════════════════════ + +class WifiManagerMulticastLock(Stub): + """WifiManager.MulticastLock""" + + def __init__(self, tag: str): + super().__init__() + self._tag = tag + self._held = False + self._ref_count = 0 + + def acquire(self): + self._held = True + self._ref_count += 1 + + def release(self): + if self._ref_count > 0: + self._ref_count -= 1 + if self._ref_count == 0: + self._held = False + + def isHeld(self) -> bool: + return self._held + + def toString(self) -> str: + return f"MulticastLock(tag={self._tag}, held={self._held})" + + +class WifiManagerLocalOnlyHotspotCallback(Stub): + """WifiManager.LocalOnlyHotspotCallback""" + + ERROR_NO_CHANNEL = 1 + ERROR_GENERIC = 2 + ERROR_INCOMPATIBLE_MODE = 3 + ERROR_TETHERING_DISALLOWED = 4 + + def onStarted(self, hotspot: Any): + pass + + def onStopped(self): + pass + + def onFailed(self, reason: int): + pass + + +class WifiManagerWpsCallback(Stub): + """WifiManager.WpsCallback""" + + def onStarted(self, pin: str): + pass + + def onSucceeded(self): + pass + + def onFailed(self, reason: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# WifiManager +# ═══════════════════════════════════════════════════════════════ + +class WifiManager(Stub): + """WifiManager — WiFi 管理器""" + + WIFI_STATE_DISABLING = 0 + WIFI_STATE_DISABLED = 1 + WIFI_STATE_ENABLING = 2 + WIFI_STATE_ENABLED = 3 + WIFI_STATE_UNKNOWN = 4 + + WIFI_MODE_FULL = 1 + WIFI_MODE_FULL_HIGH_PERF = 3 + WIFI_MODE_SCAN_ONLY = 2 + + ERROR_AUTHENTICATING = 1 + SAP_START_FAILURE_GENERAL = 0 + + ACTION_PICK_WIFI_NETWORK = "android.net.wifi.PICK_WIFI_NETWORK" + SUPPLICANT_CONNECTION_CHANGE_ACTION = "android.net.wifi.supplicant.CONNECTION_CHANGE" + SUPPLICANT_STATE_CHANGED_ACTION = "android.net.wifi.SUPPLICANT_STATE_CHANGED" + NETWORK_STATE_CHANGED_ACTION = "android.net.wifi.STATE_CHANGE" + SCAN_RESULTS_AVAILABLE_ACTION = "android.net.wifi.SCAN_RESULTS" + WIFI_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_STATE_CHANGED" + RSSI_CHANGED_ACTION = "android.net.wifi.RSSI_CHANGED" + NETWORK_IDS_CHANGED_ACTION = "android.net.wifi.NETWORK_IDS_CHANGED" + + def __init__(self): + super().__init__() + self._wifi_state = self.WIFI_STATE_ENABLED + self._wifi_enabled = True + self._connection_info: Optional[WifiInfo] = None + self._scan_results: List[ScanResult] = [] + self._configured_networks: List[WifiConfiguration] = [] + self._network_suggestions: List[WifiNetworkSuggestion] = [] + self._listeners: Dict[str, List[Any]] = {} + self._next_network_id = 0 + self._p2p_channels: List[int] = [] + + def getConnectionInfo(self) -> Optional[WifiInfo]: + if self._connection_info is None: + info = WifiInfo() + info._ssid = '"mock_wifi"' + info._bssid = "00:1A:2B:3C:4D:5E" + info._rssi = -55 + info._link_speed = 1300 + info._frequency = 5180 + info._network_id = 0 + info._supplicant_state = SupplicantState(SupplicantState.COMPLETED) + self._connection_info = info + return self._connection_info + + def getScanResults(self) -> List[ScanResult]: + return list(self._scan_results) + + def startScan(self) -> bool: + return True + + def setWifiEnabled(self, enabled: bool) -> bool: + self._wifi_enabled = enabled + self._wifi_state = self.WIFI_STATE_ENABLED if enabled else self.WIFI_STATE_DISABLED + return True + + def isWifiEnabled(self) -> bool: + return self._wifi_enabled + + def getWifiState(self) -> int: + return self._wifi_state + + def getConfiguredNetworks(self) -> List[WifiConfiguration]: + return list(self._configured_networks) + + def addNetwork(self, config: WifiConfiguration) -> int: + config.networkId = self._next_network_id + self._next_network_id += 1 + self._configured_networks.append(config) + return config.networkId + + def updateNetwork(self, config: WifiConfiguration) -> int: + for i, c in enumerate(self._configured_networks): + if c.networkId == config.networkId: + self._configured_networks[i] = config + return config.networkId + return -1 + + def removeNetwork(self, net_id: int) -> bool: + self._configured_networks = [ + c for c in self._configured_networks if c.networkId != net_id + ] + return True + + def enableNetwork(self, net_id: int, disable_others: bool = False) -> bool: + for c in self._configured_networks: + if c.networkId == net_id: + c.status = WifiConfiguration.Status.ENABLED + elif disable_others: + c.status = WifiConfiguration.Status.DISABLED + return True + + def disableNetwork(self, net_id: int) -> bool: + for c in self._configured_networks: + if c.networkId == net_id: + c.status = WifiConfiguration.Status.DISABLED + return True + + def disconnect(self) -> bool: + return True + + def reconnect(self) -> bool: + return True + + def reassociate(self) -> bool: + return True + + def is5GHzBandSupported(self) -> bool: + return True + + def is6GHzBandSupported(self) -> bool: + import sys + return sys.version_info >= (3, 10) + + def isEasyConnectSupported(self) -> bool: + return False + + def getP2pChannelList(self) -> List[int]: + return list(self._p2p_channels) + + def startLocalOnlyHotspot(self, callback: WifiManagerLocalOnlyHotspotCallback, handler: Any = None): + try: + callback.onStarted("MockHotspot") + except Exception: + pass + + def addNetworkSuggestions(self, suggestions: List[WifiNetworkSuggestion]) -> int: + self._network_suggestions.extend(suggestions) + return 0 # STATUS_SUGGESTION_SUCCESS + + def removeNetworkSuggestions(self, suggestions: List[WifiNetworkSuggestion]) -> int: + for s in suggestions: + self._network_suggestions = [x for x in self._network_suggestions if x is not s] + return 0 + + def createMulticastLock(self, tag: str) -> WifiManagerMulticastLock: + return WifiManagerMulticastLock(tag) + + def getDhcpInfo(self): + from .net import DhcpInfo + return DhcpInfo() + + def pingSupplicant(self) -> bool: + return True + + def startScanActive(self) -> bool: + return True + + def setScanAlwaysAvailable(self, enabled: bool): + pass + + def isScanAlwaysAvailable(self) -> bool: + return True + + def saveConfiguration(self) -> bool: + return True + + def toString(self) -> str: + return f"WifiManager(state={self._wifi_state})" + + +# ═══════════════════════════════════════════════════════════════ +# WifiP2pManager +# ═══════════════════════════════════════════════════════════════ + +class WifiP2pDevice(Stub): + """WifiP2pDevice — P2P 设备""" + + CONNECTED = 0 + INVITED = 1 + FAILED = 2 + AVAILABLE = 3 + UNAVAILABLE = 4 + + def __init__(self): + super().__init__() + self.deviceAddress: str = "" + self.deviceName: str = "" + self.primaryDeviceType: str = "" + self.secondaryDeviceType: str = "" + self.wpsConfigMethods: int = 0 + self.deviceCapability: int = 0 + self.groupCapability: int = 0 + self.status: int = self.UNAVAILABLE + self.GO_PORT: int = 0 + + def getDeviceAddress(self) -> str: + return self.deviceAddress + + def getDeviceName(self) -> str: + return self.deviceName + + def isServiceDiscoveryCapable(self) -> bool: + return True + + def isGroupOwner(self) -> bool: + return self.groupCapability > 0 + + def toString(self) -> str: + return f"WifiP2pDevice(name={self.deviceName}, addr={self.deviceAddress})" + + +class WifiP2pConfig(Stub): + """WifiP2pConfig — P2P 配置""" + + class Builder(Stub): + def __init__(self): + super().__init__() + self._device_address = "" + self._network_name = "" + self._passphrase = "" + self._group_owner_band = 0 + + def setDeviceAddress(self, addr: str) -> "WifiP2pConfig.Builder": + self._device_address = addr + return self + + def setNetworkName(self, name: str) -> "WifiP2pConfig.Builder": + self._network_name = name + return self + + def setPassphrase(self, passphrase: str) -> "WifiP2pConfig.Builder": + self._passphrase = passphrase + return self + + def setGroupOwnerBand(self, band: int) -> "WifiP2pConfig.Builder": + self._group_owner_band = band + return self + + def build(self) -> "WifiP2pConfig": + config = WifiP2pConfig() + config.deviceAddress = self._device_address + config.networkName = self._network_name + config.passphrase = self._passphrase + return config + + def __init__(self): + super().__init__() + self.deviceAddress: str = "" + self.networkName: str = "" + self.passphrase: str = "" + + +class WifiP2pGroup(Stub): + """WifiP2pGroup — P2P 群组""" + + def __init__(self): + super().__init__() + self._network_name: str = "" + self._is_group_owner: bool = False + self._owner: Optional[WifiP2pDevice] = None + self._clients: List[WifiP2pDevice] = [] + self._passphrase: str = "" + self._interface: str = "" + + def getNetworkName(self) -> str: + return self._network_name + + def isGroupOwner(self) -> bool: + return self._is_group_owner + + def getOwner(self) -> Optional[WifiP2pDevice]: + return self._owner + + def getClientList(self) -> List[WifiP2pDevice]: + return list(self._clients) + + def getPassphrase(self) -> str: + return self._passphrase + + def getInterface(self) -> str: + return self._interface + + def toString(self) -> str: + return f"WifiP2pGroup(name={self._network_name})" + + +class WifiP2pManager(Stub): + """WifiP2pManager — P2P 管理器""" + + WIFI_P2P_STATE_ENABLED_ACTION = "android.net.wifi.p2p.STATE_CHANGED" + WIFI_P2P_STATE_CHANGED_ACTION = "android.net.wifi.p2p.STATE_CHANGED" + WIFI_P2P_PEERS_CHANGED_ACTION = "android.net.wifi.p2p.PEERS_CHANGED" + WIFI_P2P_CONNECTION_CHANGED_ACTION = "android.net.wifi.p2p.CONNECTION_CHANGED" + WIFI_P2P_THIS_DEVICE_CHANGED_ACTION = "android.net.wifi.p2p.THIS_DEVICE_CHANGED" + WIFI_P2P_DISCOVERY_CHANGED_ACTION = "android.net.wifi.p2p.DISCOVERY_CHANGED" + + class Channel(Stub): + """P2P 通道""" + + def __init__(self): + super().__init__() + + def __init__(self): + super().__init__() + self._channel: Optional[WifiP2pManager.Channel] = None + self._peers: List[WifiP2pDevice] = [] + + def initialize(self, context: Any, looper: Any, listener: Any) -> "WifiP2pManager.Channel": + self._channel = WifiP2pManager.Channel() + if listener: + try: + listener.onSuccess() + except Exception: + pass + return self._channel + + def discoverPeers(self, channel: "WifiP2pManager.Channel", listener: Any): + if listener: + try: + listener.onSuccess() + except Exception: + pass + + def discoverServices(self, channel: "WifiP2pManager.Channel", listener: Any): + if listener: + try: + listener.onSuccess() + except Exception: + pass + + def requestPeers(self, channel: "WifiP2pManager.Channel", listener: Any): + if listener: + try: + listener.onPeersAvailable(self._peers) + except Exception: + pass + + def requestGroupInfo(self, channel: "WifiP2pManager.Channel", listener: Any): + if listener: + try: + listener.onGroupInfoAvailable(WifiP2pGroup()) + except Exception: + pass + + def createGroup(self, channel: "WifiP2pManager.Channel", listener: Any): + if listener: + try: + listener.onSuccess() + except Exception: + pass + + def removeGroup(self, channel: "WifiP2pManager.Channel", listener: Any): + if listener: + try: + listener.onSuccess() + except Exception: + pass + + def connect(self, channel: "WifiP2pManager.Channel", config: WifiP2pConfig, listener: Any): + if listener: + try: + listener.onSuccess() + except Exception: + pass + + def cancelConnect(self, channel: "WifiP2pManager.Channel", listener: Any): + if listener: + try: + listener.onSuccess() + except Exception: + pass + + def stopPeerDiscovery(self, channel: "WifiP2pManager.Channel", listener: Any): + if listener: + try: + listener.onSuccess() + except Exception: + pass + + def requestDeviceInfo(self, channel: "WifiP2pManager.Channel", device_address: str, listener: Any): + if listener: + try: + listener.onDeviceInfoAvailable(WifiP2pDevice()) + except Exception: + pass + + def setDeviceName(self, channel: "WifiP2pManager.Channel", device_name: str, listener: Any): + if listener: + try: + listener.onSuccess() + except Exception: + pass + + def setWfdEnabled(self, channel: "WifiP2pManager.Channel", enabled: bool, listener: Any): + pass + + def toString(self) -> str: + return f"WifiP2pManager(peers={len(self._peers)})" + + +# ═══════════════════════════════════════════════════════════════ +# WifiConfigurationKeyMgmt / WifiConfiguration... aliases +# ═══════════════════════════════════════════════════════════════ + +WifiConfigurationKeyMgmt = WifiConfiguration.KeyMgmt +WifiConfigurationGroupCipher = WifiConfiguration.GroupCipher +WifiConfigurationPairwiseCipher = WifiConfiguration.PairwiseCipher +WifiConfigurationAuthAlgorithm = WifiConfiguration.AuthAlgorithm +WifiConfigurationProtocol = WifiConfiguration.Protocol +WifiConfigurationStatus = WifiConfiguration.Status + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "WifiManager", + "WifiInfo", + "WifiConfiguration", + "WifiConfigurationKeyMgmt", + "WifiConfigurationGroupCipher", + "WifiConfigurationPairwiseCipher", + "WifiConfigurationAuthAlgorithm", + "WifiConfigurationProtocol", + "WifiConfigurationStatus", + "ScanResult", + "WifiSsid", + "SupplicantState", + "WifiEnterpriseConfig", + "WifiNetworkSpecifier", + "WifiNetworkSuggestion", + "WifiManagerLocalOnlyHotspotCallback", + "WifiManagerWpsCallback", + "WifiManagerMulticastLock", + "WifiP2pManager", + "WifiP2pDevice", + "WifiP2pConfig", + "WifiP2pGroup", +] \ No newline at end of file diff --git a/android_pyapi/android/nfc.py b/android_pyapi/android/nfc.py new file mode 100644 index 0000000..a747fe9 --- /dev/null +++ b/android_pyapi/android/nfc.py @@ -0,0 +1,390 @@ +""" +Android NFC 组件 — NfcAdapter, NfcManager, Tag, NdefMessage 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# NdefRecord +# ═══════════════════════════════════════════════════════════════ + +class NdefRecord(Stub): + """NdefRecord — NDEF 记录""" + + TNF_ABSOLUTE_URI = 3 + TNF_EMPTY = 0 + TNF_EXTERNAL_TYPE = 4 + TNF_MIME_MEDIA = 2 + TNF_UNCHANGED = 6 + TNF_UNKNOWN = 5 + TNF_WELL_KNOWN = 1 + + RTD_URI = "U" + RTD_TEXT = "T" + RTD_SMART_POSTER = "Sp" + RTD_ALTERNATIVE_CARRIER = "ac" + RTD_HANDOVER_CARRIER = "Hc" + RTD_HANDOVER_REQUEST = "Hr" + RTD_HANDOVER_SELECT = "Hs" + + def __init__(self, tnf: int = TNF_EMPTY, type_: bytes = b"", + id_: bytes = b"", payload: bytes = b""): + super().__init__() + self._tnf = tnf + self._type = type_ + self._id = id_ + self._payload = payload + + @staticmethod + def createUri(uri_spec: str) -> "NdefRecord": + return NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, + type_=uri_spec.encode("utf-8"), + payload=uri_spec.encode("utf-8")) + + @staticmethod + def createMime(mime_type: str, data: bytes) -> "NdefRecord": + return NdefRecord(NdefRecord.TNF_MIME_MEDIA, + type_=mime_type.encode("utf-8"), + payload=data) + + @staticmethod + def createExternal(domain: str, type_: str, data: bytes) -> "NdefRecord": + full_type = f"{domain}:{type_}" + return NdefRecord(NdefRecord.TNF_EXTERNAL_TYPE, + type_=full_type.encode("utf-8"), + payload=data) + + @staticmethod + def createApplicationRecord(package_name: str) -> "NdefRecord": + return NdefRecord(NdefRecord.TNF_EXTERNAL_TYPE, + type_=package_name.encode("utf-8"), + payload=package_name.encode("utf-8")) + + def getTnf(self) -> int: + return self._tnf + + def getType(self) -> bytes: + return self._type + + def getId(self) -> bytes: + return self._id + + def getPayload(self) -> bytes: + return self._payload + + def toByteArray(self) -> bytes: + import struct + tnf_byte = struct.pack("B", self._tnf & 0x07) + type_len = len(self._type) + id_len = len(self._id) + payload_len = len(self._payload) + header = struct.pack("BB", type_len, id_len) + return tnf_byte + header + struct.pack(">I", payload_len) + \ + self._type + self._id + self._payload + + def toString(self) -> str: + return f"NdefRecord(tnf={self._tnf}, type={self._type})" + + +# ═══════════════════════════════════════════════════════════════ +# NdefMessage +# ═══════════════════════════════════════════════════════════════ + +class NdefMessage(Stub): + """NdefMessage — NDEF 消息""" + + def __init__(self, records: Union[List[NdefRecord], bytes, Any]): + super().__init__() + if isinstance(records, list): + self._records = list(records) + elif isinstance(records, bytes): + self._records = [] + else: + self._records = [] + + def getRecords(self) -> List[NdefRecord]: + return list(self._records) + + def getByteArrayLength(self) -> int: + return sum(len(r.toByteArray()) for r in self._records) + + def toByteArray(self) -> bytes: + data = b"" + for r in self._records: + data += r.toByteArray() + return data + + +# ═══════════════════════════════════════════════════════════════ +# Tag +# ═══════════════════════════════════════════════════════════════ + +class Tag(Stub): + """Tag — NFC 标签""" + + def __init__(self, id_: bytes = b"", tech_list: Optional[List[str]] = None): + super().__init__() + self._id = id_ + self._tech_list = tech_list or [] + + def getId(self) -> bytes: + return self._id + + def getTechList(self) -> List[str]: + return list(self._tech_list) + + def getTechListParcelable(self) -> Any: + return TechListParcelable(self._tech_list) + + def toString(self) -> str: + return f"Tag(id={self._id.hex()}, techs={self._tech_list})" + + +class TechListParcelable(Stub): + """TechListParcelable — 技术列表包裹""" + + def __init__(self, tech_list: List[str]): + super().__init__() + self._tech_list = list(tech_list) + + def getTechList(self) -> List[str]: + return list(self._tech_list) + + def toString(self) -> str: + return f"TechListParcelable(techs={self._tech_list})" + + +# ═══════════════════════════════════════════════════════════════ +# NfcAdapter callback classes +# ═══════════════════════════════════════════════════════════════ + +class NfcAdapterCreateBeamUrisCallback(Stub): + def createBeamUris(self, event: "NfcEvent") -> List[Any]: + return [] + + +class NfcAdapterOnNdefPushCompleteCallback(Stub): + def onNdefPushComplete(self, event: "NfcEvent"): + pass + + +class NdefMessageCallback(Stub): + def createNdefMessage(self, event: "NfcEvent") -> Optional[NdefMessage]: + return None + + +# ═══════════════════════════════════════════════════════════════ +# NfcEvent +# ═══════════════════════════════════════════════════════════════ + +class NfcEvent(Stub): + """NfcEvent — NFC 事件""" + + def __init__(self, nfc_adapter: Any = None): + super().__init__() + self.nfcAdapter = nfc_adapter + + def getNfcAdapter(self) -> Any: + return self.nfcAdapter + + +# ═══════════════════════════════════════════════════════════════ +# NfcAntennaInfo / ApduServiceInfo / CardEmulation / NfcFCardEmulation +# ═══════════════════════════════════════════════════════════════ + +class NfcAntennaInfo(Stub): + """NfcAntennaInfo — NFC 天线信息""" + + def __init__(self, x: float = 0.0, y: float = 0.0): + super().__init__() + self._x = x + self._y = y + + def getX(self) -> float: + return self._x + + def getY(self) -> float: + return self._y + + +class ApduServiceInfo(Stub): + """ApduServiceInfo — APDU 服务信息""" + + def __init__(self, description: str = ""): + super().__init__() + self._description = description + + def getDescription(self) -> str: + return self._description + + +class CardEmulation(Stub): + """CardEmulation — 卡模拟""" + + CATEGORY_PAYMENT = "payment" + CATEGORY_OTHER = "other" + + SELECTION_TYPE_ASK_IF_CONFLICT = 1 + SELECTION_TYPE_PREFER_DEFAULT = 2 + SELECTION_TYPE_USER_PROMPTED = 0 + + +class NfcFCardEmulation(Stub): + """NfcFCardEmulation — NFC-F 卡模拟""" + + EXTRA_NFC_F_CARD_EMULATION_PARAMS = "android.nfc.extra.EXTRA_NFC_F_CARD_EMULATION_PARAMS" + + @staticmethod + def getInstance(nfc_adapter: Any) -> "NfcFCardEmulation": + return NfcFCardEmulation() + + def enableService(self, activity: Any, service: Any) -> bool: + return True + + def disableService(self, activity: Any, service: Any) -> bool: + return True + + def setSystemCodeForService(self, activity: Any, service: Any, system_code: str) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# NfcManager +# ═══════════════════════════════════════════════════════════════ + +class NfcManager(Stub): + """NfcManager — NFC 管理器""" + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + self._default_adapter: Optional["NfcAdapter"] = None + + def getDefaultAdapter(self) -> Optional["NfcAdapter"]: + if self._default_adapter is None: + self._default_adapter = NfcAdapter() + return self._default_adapter + + +# ═══════════════════════════════════════════════════════════════ +# NfcAdapter +# ═══════════════════════════════════════════════════════════════ + +class NfcAdapter(Stub): + """NfcAdapter — NFC 适配器""" + + ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED" + ACTION_TECH_DISCOVERED = "android.nfc.action.TECH_DISCOVERED" + ACTION_TAG_DISCOVERED = "android.nfc.action.TAG_DISCOVERED" + + FLAG_READER_NFC_A = 1 + FLAG_READER_NFC_B = 2 + FLAG_READER_NFC_F = 4 + FLAG_READER_NFC_V = 8 + FLAG_READER_NFC_BARCODE = 16 + FLAG_READER_SKIP_NDEF_CHECK = 128 + FLAG_READER_NO_PLATFORM_SOUNDS = 256 + + FLAG_NDEF_PUSH_NO_CONFIRM = 1 + + _default_adapter: Optional["NfcAdapter"] = None + + def __init__(self): + super().__init__() + self._enabled = True + self._ndef_push_message: Optional[NdefMessage] = None + self._ndef_push_callback: Optional[NdefMessageCallback] = None + self._push_complete_callback: Optional[NfcAdapterOnNdefPushCompleteCallback] = None + self._beam_uri_callback: Optional[NfcAdapterCreateBeamUrisCallback] = None + + @staticmethod + def getDefaultAdapter(context: Any = None) -> Optional["NfcAdapter"]: + if NfcAdapter._default_adapter is None: + NfcAdapter._default_adapter = NfcAdapter() + return NfcAdapter._default_adapter + + def isEnabled(self) -> bool: + return self._enabled + + def enable(self) -> bool: + self._enabled = True + return True + + def disable(self) -> bool: + self._enabled = False + return True + + def setNdefPushMessage(self, message: NdefMessage, activity: Any, + vararg_activities: Any = None): + self._ndef_push_message = message + + def setNdefPushMessageCallback(self, callback: NdefMessageCallback, activity: Any, + vararg_activities: Any = None): + self._ndef_push_callback = callback + + def setOnNdefPushCompleteCallback(self, callback: NfcAdapterOnNdefPushCompleteCallback, + activity: Any, vararg_activities: Any = None): + self._push_complete_callback = callback + + def setBeamUrisCallback(self, callback: NfcAdapterCreateBeamUrisCallback, activity: Any): + self._beam_uri_callback = callback + + def enableForegroundDispatch(self, activity: Any, intent: Any, + filters: Any, tech_lists: Any): + pass + + def disableForegroundDispatch(self, activity: Any): + pass + + def enableReaderMode(self, activity: Any, callback: Any, flags: int, extras: Any = None): + pass + + def disableReaderMode(self, activity: Any): + pass + + def invokeBeam(self, activity: Any) -> bool: + return True + + def isSecureNfcSupported(self) -> bool: + return False + + def isSecureNfcEnabled(self) -> bool: + return False + + def enableSecureNfc(self, enable: bool) -> bool: + return True + + def toString(self) -> str: + return f"NfcAdapter(enabled={self._enabled})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "NfcAdapter", + "NfcManager", + "Tag", + "NdefMessage", + "NdefRecord", + "NfcEvent", + "NfcAdapterCreateBeamUrisCallback", + "NfcAdapterOnNdefPushCompleteCallback", + "NdefMessageCallback", + "TechListParcelable", + "NfcAntennaInfo", + "ApduServiceInfo", + "CardEmulation", + "NfcFCardEmulation", +] \ No newline at end of file diff --git a/android_pyapi/android/os.py b/android_pyapi/android/os.py new file mode 100644 index 0000000..0eb3e02 --- /dev/null +++ b/android_pyapi/android/os.py @@ -0,0 +1,1660 @@ +""" +Android OS 组件 — Bundle, Handler, Looper, Message, Build 等 +方法签名完全对齐 Android SDK +""" + +import asyncio +import functools +import heapq +import os as _os +import sys +import threading +import time as _time +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, + HAS_PSUTIL, +) + + +# ═══════════════════════════════════════════════════════════════ +# Build +# ═══════════════════════════════════════════════════════════════ + +class Build(Stub): + """Build — 设备信息""" + + class VERSION(Stub): + """版本信息""" + SDK_INT = 34 # Android 14 + RELEASE = "14" + INCREMENTAL = "20240301" + CODENAME = "REL" + SECURITY_PATCH = "2024-03-05" + BASE_OS = "" + PREVIEW_SDK_INT = 0 + RESOURCES_SDK_INT = 34 + MEDIA_PERFORMANCE_CLASS = 0 + + class VERSION_CODES(Stub): + """版本代码常量""" + BASE = 1 + BASE_1_1 = 2 + CUPCAKE = 3 + DONUT = 4 + ECLAIR = 5 + ECLAIR_0_1 = 6 + ECLAIR_MR1 = 7 + FROYO = 8 + GINGERBREAD = 9 + GINGERBREAD_MR1 = 10 + HONEYCOMB = 11 + HONEYCOMB_MR1 = 12 + HONEYCOMB_MR2 = 13 + ICE_CREAM_SANDWICH = 14 + ICE_CREAM_SANDWICH_MR1 = 15 + JELLY_BEAN = 16 + JELLY_BEAN_MR1 = 17 + JELLY_BEAN_MR2 = 18 + KITKAT = 19 + KITKAT_WATCH = 20 + LOLLIPOP = 21 + LOLLIPOP_MR1 = 22 + M = 23 + N = 24 + N_MR1 = 25 + O = 26 + O_MR1 = 27 + P = 28 + Q = 29 + R = 30 + S = 31 + S_V2 = 32 + TIRAMISU = 33 + UPSIDE_DOWN_CAKE = 34 + + VERSION = VERSION() + BOARD = "redfin" + BOOTLOADER = "r7.0-12345" + BRAND = "google" + CPU_ABI = "arm64-v8a" + CPU_ABI2 = "" + DEVICE = "redfin" + DISPLAY = "AP1A.240305.001" + FINGERPRINT = "google/redfin/redfin:14/AP1A.240305.001/12345678:user/release-keys" + HARDWARE = "redfin" + HOST = "build-test-01" + ID = "AP1A.240305.001" + MANUFACTURER = "Google" + MODEL = "Pixel 5" + ODM_SKU = None + PRODUCT = "redfin" + RADIO = "" + SERIAL = "unknown" + SKU = None + SOC_MANUFACTURER = "Qualcomm" + SOC_MODEL = "Snapdragon 765G" + SUPPORTED_32_BIT_ABIS = ["armeabi-v7a", "armeabi"] + SUPPORTED_64_BIT_ABIS = ["arm64-v8a"] + SUPPORTED_ABIS = ["arm64-v8a", "armeabi-v7a", "armeabi"] + TAGS = "release-keys" + TIME = int(_time.time() * 1000) + TYPE = "user" + USER = "build-user" + + @staticmethod + def getRadioVersion() -> str: + return "" + + @staticmethod + def getSerial() -> str: + return "unknown" + + @staticmethod + def getFingerprint() -> str: + return Build.FINGERPRINT + + +# ═══════════════════════════════════════════════════════════════ +# SystemClock +# ═══════════════════════════════════════════════════════════════ + +class SystemClock(Stub): + """SystemClock — 系统时钟""" + + @staticmethod + def elapsedRealtime() -> int: + """返回自启动以来的毫秒数""" + return int(_time.monotonic() * 1000) + + @staticmethod + def elapsedRealtimeNanos() -> int: + return int(_time.monotonic_ns()) + + @staticmethod + def uptimeMillis() -> int: + """返回自启动以来(不含深度睡眠)的毫秒数""" + return int(_time.monotonic() * 1000) + + @staticmethod + def currentThreadTimeMillis() -> int: + return int(_time.clock_gettime(_time.CLOCK_THREAD_CPUTIME_ID) * 1000) + + @staticmethod + def sleep(ms: int): + _time.sleep(ms / 1000.0) + + @staticmethod + def setCurrentTimeMillis(millis: int) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# Environment +# ═══════════════════════════════════════════════════════════════ + +class Environment(Stub): + """Environment — 环境目录""" + + MEDIA_BAD_REMOVAL = "bad_removal" + MEDIA_CHECKING = "checking" + MEDIA_EJECTING = "ejecting" + MEDIA_MOUNTED = "mounted" + MEDIA_MOUNTED_READ_ONLY = "mounted_ro" + MEDIA_NOFS = "nofs" + MEDIA_REMOVED = "removed" + MEDIA_SHARED = "shared" + MEDIA_UNKNOWN = "unknown" + MEDIA_UNMOUNTABLE = "unmountable" + MEDIA_UNMOUNTED = "unmounted" + + DIRECTORY_ALARMS = "Alarms" + DIRECTORY_DCIM = "DCIM" + DIRECTORY_DOCUMENTS = "Documents" + DIRECTORY_DOWNLOADS = "Download" + DIRECTORY_MOVIES = "Movies" + DIRECTORY_MUSIC = "Music" + DIRECTORY_NOTIFICATIONS = "Notifications" + DIRECTORY_PICTURES = "Pictures" + DIRECTORY_PODCASTS = "Podcasts" + DIRECTORY_RINGTONES = "Ringtones" + DIRECTORY_AUDIOBOOKS = "Audiobooks" + DIRECTORY_SCREENSHOTS = "Screenshots" + + @staticmethod + def getExternalStorageDirectory() -> str: + return _sdcard_root + + @staticmethod + def getExternalStoragePublicDirectory(type_: str) -> str: + path = _os.path.join(_sdcard_root, type_) + _os.makedirs(path, exist_ok=True) + return path + + @staticmethod + def getDataDirectory() -> str: + return _os.path.join(_app_data_dir, "data") + + @staticmethod + def getDownloadCacheDirectory() -> str: + return _os.path.join(_app_data_dir, "cache", "download") + + @staticmethod + def getRootDirectory() -> str: + return _os.path.abspath(_os.sep) + + @staticmethod + def getStorageDirectory(user_handle: Any = None) -> str: + return _sdcard_root + + @staticmethod + def getExternalStorageState(path: Optional[str] = None) -> str: + return Environment.MEDIA_MOUNTED + + @staticmethod + def isExternalStorageRemovable(path: Optional[str] = None) -> bool: + return False + + @staticmethod + def isExternalStorageEmulated(path: Optional[str] = None) -> bool: + return True + + @staticmethod + def isExternalStorageLegacy(path: Optional[str] = None) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# Process +# ═══════════════════════════════════════════════════════════════ + +class Process(Stub): + """Process — 进程信息""" + + # 进程优先级 + THREAD_PRIORITY_DEFAULT = 0 + THREAD_PRIORITY_LOWEST = -19 + THREAD_PRIORITY_BACKGROUND = 10 + THREAD_PRIORITY_FOREGROUND = -2 + THREAD_PRIORITY_DISPLAY = -4 + THREAD_PRIORITY_URGENT_DISPLAY = -8 + THREAD_PRIORITY_AUDIO = -16 + THREAD_PRIORITY_URGENT_AUDIO = -19 + THREAD_PRIORITY_MORE_FAVORABLE = -1 + THREAD_PRIORITY_LESS_FAVORABLE = 1 + + SIGNAL_KILL = 9 + SIGNAL_QUIT = 3 + SIGNAL_USR1 = 10 + SIGNAL_USR2 = 12 + + @staticmethod + def myTid() -> int: + return threading.get_ident() + + @staticmethod + def myUid() -> int: + return 10001 + + @staticmethod + def myPid() -> int: + return _os.getpid() + + @staticmethod + def killProcess(pid: int): + pass + + @staticmethod + def sendSignal(pid: int, signal: int): + pass + + @staticmethod + def setThreadPriority(priority: int): + pass + + @staticmethod + def setThreadPriority(tid: int, priority: int): + pass + + @staticmethod + def getElapsedCpuTime() -> int: + return int(_time.clock_gettime(_time.CLOCK_PROCESS_CPUTIME_ID) * 1000) + + @staticmethod + def supportsProcesses() -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# BaseBundle / PersistableBundle +# ═══════════════════════════════════════════════════════════════ + +class BaseBundle(Stub): + """BaseBundle — Bundle 基类""" + + def __init__(self): + super().__init__() + self._map: Dict[str, Any] = {} + self._class_loader = None + + def getString(self, key: str, default: Optional[str] = None) -> Optional[str]: + v = self._map.get(key, default) + return str(v) if v is not None else default + + def getInt(self, key: str, default: int = 0) -> int: + v = self._map.get(key, default) + try: + return int(v) if v is not None else default + except (ValueError, TypeError): + return default + + def getLong(self, key: str, default: int = 0) -> int: + return self.getInt(key, default) + + def getFloat(self, key: str, default: float = 0.0) -> float: + v = self._map.get(key, default) + try: + return float(v) if v is not None else default + except (ValueError, TypeError): + return default + + def getDouble(self, key: str, default: float = 0.0) -> float: + return self.getFloat(key, default) + + def getBoolean(self, key: str, default: bool = False) -> bool: + v = self._map.get(key, default) + if isinstance(v, bool): + return v + if isinstance(v, str): + return v.lower() in ("true", "1", "yes") + return bool(v) if v is not None else default + + def getByte(self, key: str, default: int = 0) -> int: + return self.getInt(key, default) + + def getChar(self, key: str, default: str = "") -> str: + return str(self._map.get(key, default)) + + def getShort(self, key: str, default: int = 0) -> int: + return self.getInt(key, default) + + def getCharSequence(self, key: str, default: Optional[str] = None) -> Optional[str]: + return self.getString(key, default) + + def getSerializable(self, key: str, default: Any = None) -> Any: + return self._map.get(key, default) + + def getStringArrayList(self, key: str) -> Optional[List[str]]: + v = self._map.get(key) + if isinstance(v, list): + return [str(x) for x in v] + return None + + def getIntegerArrayList(self, key: str) -> Optional[List[int]]: + v = self._map.get(key) + if isinstance(v, list): + return [int(x) for x in v if isinstance(x, (int, float))] + return None + + def containsKey(self, key: str) -> bool: + return key in self._map + + def isEmpty(self) -> bool: + return len(self._map) == 0 + + def size(self) -> int: + return len(self._map) + + def clear(self): + self._map.clear() + + def keySet(self) -> Set[str]: + return set(self._map.keys()) + + def hasKey(self, key: str) -> bool: + return key in self._map + + def toString(self) -> str: + return f"Bundle({self._map})" + + def __len__(self) -> int: + return len(self._map) + + def __contains__(self, key: str) -> bool: + return key in self._map + + def __getitem__(self, key: str): + return self._map[key] + + def __setitem__(self, key: str, value: Any): + self._map[key] = value + + def __delitem__(self, key: str): + self._map.pop(key, None) + + +class PersistableBundle(BaseBundle): + """PersistableBundle — 可持久化的 Bundle""" + + def __init__(self, capacity: int = 0): + super().__init__() + + def putPersistableBundle(self, key: str, value: "PersistableBundle"): + self._map[key] = value + + def getPersistableBundle(self, key: str) -> Optional["PersistableBundle"]: + v = self._map.get(key) + return v if isinstance(v, PersistableBundle) else None + + +# ═══════════════════════════════════════════════════════════════ +# Bundle +# ═══════════════════════════════════════════════════════════════ + +class Bundle(BaseBundle): + """Bundle — 键值对数据容器""" + + EMPTY: "Bundle" = None # 在类加载后初始化 + + def __init__(self, capacity: int = 0): + super().__init__() + self._capacity = capacity + + def putString(self, key: str, value: Optional[str]): + self._map[key] = value + + def putInt(self, key: str, value: int): + self._map[key] = value + + def putLong(self, key: str, value: int): + self._map[key] = value + + def putFloat(self, key: str, value: float): + self._map[key] = value + + def putDouble(self, key: str, value: float): + self._map[key] = value + + def putBoolean(self, key: str, value: bool): + self._map[key] = value + + def putByte(self, key: str, value: int): + self._map[key] = value + + def putChar(self, key: str, value: str): + self._map[key] = value + + def putShort(self, key: str, value: int): + self._map[key] = value + + def putCharSequence(self, key: str, value: Optional[str]): + self._map[key] = value + + def putSerializable(self, key: str, value: Any): + self._map[key] = value + + def putParcelable(self, key: str, value: Any): + self._map[key] = value + + def putStringArrayList(self, key: str, value: Optional[List[str]]): + self._map[key] = value + + def putIntegerArrayList(self, key: str, value: Optional[List[int]]): + self._map[key] = value + + def putAll(self, bundle: "Bundle"): + if bundle: + self._map.update(bundle._map) + + def getParcelable(self, key: str, default: Any = None) -> Any: + return self._map.get(key, default) + + def getBundle(self, key: str) -> Optional["Bundle"]: + v = self._map.get(key) + return v if isinstance(v, Bundle) else None + + def putBundle(self, key: str, value: Optional["Bundle"]): + self._map[key] = value + + def describeContents(self) -> int: + return 0 + + def clone(self) -> "Bundle": + b = Bundle() + b._map = dict(self._map) + return b + + def deepCopy(self) -> "Bundle": + import copy + b = Bundle() + b._map = copy.deepcopy(self._map) + return b + + def filterValues(self): + pass + + def getPairValue(self) -> Optional[str]: + return None + + def setClassLoader(self, loader: Any): + self._class_loader = loader + + def getClassLoader(self) -> Any: + return self._class_loader + + @staticmethod + def forPair(key: str, value: str) -> "Bundle": + b = Bundle() + b.putString(key, value) + return b + + +Bundle.EMPTY = Bundle() + + +# ═══════════════════════════════════════════════════════════════ +# Parcel / Parcelable +# ═══════════════════════════════════════════════════════════════ + +class Parcelable(Stub): + """Parcelable 接口""" + + CONTENTS_FILE_DESCRIPTOR = "CONTENTS_FILE_DESCRIPTOR" + PARCELABLE_WRITE_RETURN_VALUE = 1 + PARCELABLE_ELIDE_DUPLICATES = 2 + + def describeContents(self) -> int: + return 0 + + def writeToParcel(self, dest: "Parcel", flags: int): + pass + + class Creator: + def createFromParcel(self, source: "Parcel"): + return None + def newArray(self, size: int): + return [None] * size + + class ClassLoaderCreator: + def createFromParcel(self, source: "Parcel", loader: Any): + return None + def newArray(self, size: int): + return [None] * size + + +class Parcel(Stub): + """Parcel — 序列化数据容器""" + + STRING_CREATOR = "STRING_CREATOR" + STRING_LIST_CREATOR = "STRING_LIST_CREATOR" + INT_CREATOR = "INT_CREATOR" + + def __init__(self): + super().__init__() + self._data: Dict[str, Any] = {} + self._position = 0 + self._size = 0 + + @staticmethod + def obtain() -> "Parcel": + return Parcel() + + def recycle(self): + self._data.clear() + self._position = 0 + self._size = 0 + + def writeInt(self, value: int): + self._data.setdefault("_ints", []).append(value) + self._size += 4 + + def writeLong(self, value: int): + self._data.setdefault("_longs", []).append(value) + self._size += 8 + + def writeFloat(self, value: float): + self._data.setdefault("_floats", []).append(value) + self._size += 4 + + def writeDouble(self, value: float): + self._data.setdefault("_doubles", []).append(value) + self._size += 8 + + def writeString(self, value: Optional[str]): + strs = self._data.setdefault("_strings", []) + strs.append(value) + self._size += len(value) + 4 if value else 4 + + def writeBoolean(self, value: bool): + self.writeInt(1 if value else 0) + + def writeByte(self, value: int): + self.writeInt(value & 0xFF) + + def writeParcelable(self, p: Parcelable, flags: int): + pass + + def writeStringArray(self, values: Optional[List[str]]): + self._data["_string_array"] = values + + def writeIntArray(self, values: Optional[List[int]]): + self._data["_int_array"] = values + + def writeLongArray(self, values: Optional[List[int]]): + self._data["_long_array"] = values + + def readInt(self) -> int: + ints = self._data.get("_ints", []) + if ints: + return ints.pop(0) + return 0 + + def readLong(self) -> int: + longs = self._data.get("_longs", []) + if longs: + return longs.pop(0) + return 0 + + def readFloat(self) -> float: + floats = self._data.get("_floats", []) + if floats: + return floats.pop(0) + return 0.0 + + def readDouble(self) -> float: + doubles = self._data.get("_doubles", []) + if doubles: + return doubles.pop(0) + return 0.0 + + def readString(self) -> Optional[str]: + strs = self._data.get("_strings", []) + if strs: + return strs.pop(0) + return None + + def readBoolean(self) -> bool: + return self.readInt() != 0 + + def readByte(self) -> int: + return self.readInt() & 0xFF + + def dataAvail(self) -> int: + return self._size - self._position + + def dataPosition(self) -> int: + return self._position + + def setDataPosition(self, pos: int): + self._position = pos + + def dataSize(self) -> int: + return self._size + + def dataCapacity(self) -> int: + return self._size + + def setDataCapacity(self, capacity: int): + pass + + def appendFrom(self, parcel: "Parcel", offset: int, length: int): + pass + + def marshall(self) -> bytes: + import json + return json.dumps(self._data).encode("utf-8") + + @staticmethod + def unmarshall(data: bytes) -> "Parcel": + import json + p = Parcel() + try: + p._data = json.loads(data.decode("utf-8")) + except Exception: + pass + return p + + def writeValue(self, v: Any): + self._data["_value"] = v + + def readValue(self) -> Any: + return self._data.get("_value") + + def hasFileDescriptors(self) -> bool: + return False + + def toString(self) -> str: + return f"Parcel({self._size} bytes)" + + +# ═══════════════════════════════════════════════════════════════ +# Message +# ═══════════════════════════════════════════════════════════════ + +class Message(Stub): + """Message — Handler 消息""" + + def __init__(self): + super().__init__() + self.what = 0 + self.arg1 = 0 + self.arg2 = 0 + self.obj: Any = None + self.data: Optional[Bundle] = None + self.replyTo: Optional["Messenger"] = None + self._target: Optional["Handler"] = None + self._callback: Optional[Callable] = None + self.when: int = 0 + self._sending = False + + @staticmethod + def obtain(handler: Optional["Handler"] = None, what: int = 0) -> "Message": + msg = Message() + if handler: + msg._target = handler + msg.what = what + return msg + + def sendToTarget(self): + if self._target: + self._target.sendMessage(self) + + def getTarget(self) -> Optional["Handler"]: + return self._target + + def setTarget(self, handler: "Handler"): + self._target = handler + + def getCallback(self) -> Optional[Callable]: + return self._callback + + def copyFrom(self, other: "Message"): + self.what = other.what + self.arg1 = other.arg1 + self.arg2 = other.arg2 + self.obj = other.obj + self.data = other.data + self.replyTo = other.replyTo + + def recycle(self): + self.what = 0 + self.arg1 = 0 + self.arg2 = 0 + self.obj = None + self.data = None + self._target = None + self._callback = None + + def isAsynchronous(self) -> bool: + return False + + def setAsynchronous(self, async_: bool): + pass + + def toString(self) -> str: + return f"Message(what={self.what}, arg1={self.arg1}, arg2={self.arg2})" + + +# ═══════════════════════════════════════════════════════════════ +# Looper +# ═══════════════════════════════════════════════════════════════ + +class Looper(Stub): + """Looper — 消息循环""" + + _main_looper: Optional["Looper"] = None + _thread_local = threading.local() + + def __init__(self): + super().__init__() + self._thread = threading.current_thread() + self._running = False + self._message_queue: List[Message] = [] + self._quit_allowed = True + + @staticmethod + def prepare(): + looper = Looper() + Looper._thread_local.looper = looper + return looper + + @staticmethod + def prepareMainLooper(): + if Looper._main_looper is None: + Looper._main_looper = Looper.prepare() + + @staticmethod + def getMainLooper() -> "Looper": + if Looper._main_looper is None: + Looper.prepareMainLooper() + return Looper._main_looper + + @staticmethod + def myLooper() -> Optional["Looper"]: + return getattr(Looper._thread_local, "looper", None) + + @staticmethod + def loop(): + looper = Looper.myLooper() + if looper is None: + raise RuntimeError("No Looper prepared") + looper._running = True + + def quit(self): + self._running = False + + def quitSafely(self): + self._running = False + + def isCurrentThread(self) -> bool: + return threading.current_thread() == self._thread + + def getThread(self) -> threading.Thread: + return self._thread + + def getQueue(self): + return self._message_queue + + def dump(self, pw: Any, prefix: str): + pass + + def toString(self) -> str: + return f"Looper({self._thread.name})" + + +# ═══════════════════════════════════════════════════════════════ +# Handler +# ═══════════════════════════════════════════════════════════════ + +class Handler(Stub): + """Handler — 消息处理器""" + + def __init__(self, looper: Optional[Looper] = None, callback: Optional[Callable] = None): + super().__init__() + self._looper = looper or Looper.getMainLooper() + self._callback = callback + self._async = False + + def handleMessage(self, msg: Message): + pass + + def dispatchMessage(self, msg: Message): + if msg._callback: + msg._callback() + elif self._callback: + self._callback(msg) + else: + self.handleMessage(msg) + + def post(self, r: Callable) -> bool: + msg = Message.obtain(self) + msg._callback = r + return self.sendMessage(msg) + + def postDelayed(self, r: Callable, delay_ms: int) -> bool: + msg = Message.obtain(self) + msg._callback = r + msg.when = int(_time.time() * 1000) + delay_ms + return self.sendMessageDelayed(msg, delay_ms) + + def postAtTime(self, r: Callable, uptime_ms: int) -> bool: + msg = Message.obtain(self) + msg._callback = r + msg.when = uptime_ms + return self.sendMessageAtTime(msg, uptime_ms) + + def sendMessage(self, msg: Message) -> bool: + msg._target = self + self.dispatchMessage(msg) + return True + + def sendEmptyMessage(self, what: int) -> bool: + msg = Message.obtain(self, what) + return self.sendMessage(msg) + + def sendMessageDelayed(self, msg: Message, delay_ms: int) -> bool: + msg._target = self + msg.when = int(_time.time() * 1000) + delay_ms + self.dispatchMessage(msg) + return True + + def sendMessageAtTime(self, msg: Message, uptime_ms: int) -> bool: + msg._target = self + msg.when = uptime_ms + self.dispatchMessage(msg) + return True + + def sendMessageAtFrontOfQueue(self, msg: Message) -> bool: + msg._target = self + self.dispatchMessage(msg) + return True + + def removeCallbacks(self, r: Callable): + pass + + def removeMessages(self, what: int): + pass + + def obtainMessage(self, what: int = 0) -> Message: + return Message.obtain(self, what) + + def hasMessages(self, what: int) -> bool: + return False + + def getLooper(self) -> Looper: + return self._looper + + def toString(self) -> str: + return f"Handler({self.__class__.__name__})" + + +# ═══════════════════════════════════════════════════════════════ +# HandlerThread +# ═══════════════════════════════════════════════════════════════ + +class HandlerThread(threading.Thread, Stub): + """HandlerThread — 带 Looper 的工作线程""" + + def __init__(self, name: str, priority: int = Process.THREAD_PRIORITY_DEFAULT): + super().__init__() + Stub.__init__(self) + self._name = name + self._priority = priority + self._looper: Optional[Looper] = None + self._started = threading.Event() + + def run(self): + Looper.prepare() + self._looper = Looper.myLooper() + self._started.set() + Looper.loop() + + def getLooper(self) -> Optional[Looper]: + self._started.wait() + return self._looper + + def getThreadId(self) -> int: + return self.ident or 0 + + def quit(self) -> bool: + if self._looper: + self._looper.quit() + return True + return False + + def quitSafely(self) -> bool: + return self.quit() + + +# ═══════════════════════════════════════════════════════════════ +# AsyncTask +# ═══════════════════════════════════════════════════════════════ + +class AsyncTask(Stub): + """AsyncTask — 异步任务""" + + Status = type("Status", (), {"PENDING": 0, "RUNNING": 1, "FINISHED": 2}) + + SERIAL_EXECUTOR = None + THREAD_POOL_EXECUTOR = None + + def __init__(self): + super().__init__() + self._status = AsyncTask.Status.PENDING + self._result: Any = None + self._cancelled = False + + def doInBackground(self, *params) -> Any: + raise NotImplementedError + + def onPreExecute(self): + pass + + def onPostExecute(self, result: Any): + pass + + def onProgressUpdate(self, *values): + pass + + def onCancelled(self, result: Any = None): + pass + + def execute(self, *params) -> "AsyncTask": + self._status = AsyncTask.Status.RUNNING + self.onPreExecute() + try: + self._result = self.doInBackground(*params) + except Exception as e: + self._result = e + self._status = AsyncTask.Status.FINISHED + if not self._cancelled: + self.onPostExecute(self._result) + return self + + def executeOnExecutor(self, exec: Any, *params) -> "AsyncTask": + return self.execute(*params) + + def get(self) -> Any: + return self._result + + def getStatus(self) -> int: + return self._status + + def isCancelled(self) -> bool: + return self._cancelled + + def cancel(self, may_interrupt_if_running: bool) -> bool: + self._cancelled = True + return True + + def publishProgress(self, *values): + self.onProgressUpdate(*values) + + +# ═══════════════════════════════════════════════════════════════ +# CountDownTimer +# ═══════════════════════════════════════════════════════════════ + +class CountDownTimer(Stub): + """CountDownTimer — 倒计时""" + + def __init__(self, millis_in_future: int, count_down_interval: int): + super().__init__() + self._millis_in_future = millis_in_future + self._interval = count_down_interval + self._cancelled = False + + def onTick(self, millis_until_finished: int): + pass + + def onFinish(self): + pass + + def start(self) -> "CountDownTimer": + self._cancelled = False + remaining = self._millis_in_future + while remaining > 0 and not self._cancelled: + self.onTick(remaining) + _time.sleep(self._interval / 1000.0) + remaining -= self._interval + if not self._cancelled: + self.onFinish() + return self + + def cancel(self): + self._cancelled = True + + +# ═══════════════════════════════════════════════════════════════ +# Messenger +# ═══════════════════════════════════════════════════════════════ + +class Messenger(Stub): + """Messenger — 跨进程消息传递""" + + def __init__(self, handler: Handler): + super().__init__() + self._handler = handler + + def send(self, message: Message): + self._handler.sendMessage(message) + + def getBinder(self): + return None + + @staticmethod + def obtain(handler: Handler) -> "Messenger": + return Messenger(handler) + + +# ═══════════════════════════════════════════════════════════════ +# PowerManager / PowerManagerWakeLock +# ═══════════════════════════════════════════════════════════════ + +class PowerManager(Stub): + """PowerManager — 电源管理""" + + ACQUIRE_CAUSES_WAKEUP = 0x10000000 + DOZE_WAKE_LOCK = 0x00000040 + DRAW_WAKE_LOCK = 0x00000080 + FULL_WAKE_LOCK = 0x0000001A + LOCATION_HARDWARE_POWER_MODE = 0x00000080 + ON_AFTER_RELEASE = 0x20000000 + PARTIAL_WAKE_LOCK = 0x00000001 + PROXIMITY_SCREEN_OFF_WAKE_LOCK = 0x00000020 + SCREEN_BRIGHT_WAKE_LOCK = 0x0000000A + SCREEN_DIM_WAKE_LOCK = 0x00000006 + WAKE_LOCK_LEVEL_MASK = 0x0000FFFF + + def __init__(self): + super().__init__() + self._is_interactive = True + self._is_power_save = False + self._is_device_idle = False + + def newWakeLock(self, level_and_flags: int, tag: str) -> "PowerManagerWakeLock": + return PowerManagerWakeLock(level_and_flags, tag) + + def isInteractive(self) -> bool: + return self._is_interactive + + def isPowerSaveMode(self) -> bool: + return self._is_power_save + + def isDeviceIdleMode(self) -> bool: + return self._is_device_idle + + def isIgnoringBatteryOptimizations(self, package_name: str) -> bool: + return True + + def isSustainedPerformanceModeSupported(self) -> bool: + return False + + def reboot(self, reason: Optional[str] = None): + pass + + def goToSleep(self, time_ms: int): + pass + + def wakeUp(self, time_ms: int): + pass + + def userActivity(self, when: int, no_change_lights: bool = False): + pass + + +class PowerManagerWakeLock(Stub): + """PowerManagerWakeLock — 唤醒锁""" + + def __init__(self, level_and_flags: int, tag: str): + super().__init__() + self._level_and_flags = level_and_flags + self._tag = tag + self._held = False + self._work_source = None + self._history_tag = None + + def acquire(self, timeout_ms: int = 0): + self._held = True + sys.stderr.write(f"[PowerManager] acquire wakelock: {self._tag}\n") + + def acquireLocked(self): + self._held = True + + def release(self, flags: int = 0): + self._held = False + sys.stderr.write(f"[PowerManager] release wakelock: {self._tag}\n") + + def isHeld(self) -> bool: + return self._held + + def setWorkSource(self, work_source: Any): + self._work_source = work_source + + def setReferenceCounted(self, value: bool): + pass + + def setHistoryTag(self, tag: str): + self._history_tag = tag + + def toString(self) -> str: + return f"WakeLock({self._tag}, held={self._held})" + + +# ═══════════════════════════════════════════════════════════════ +# Vibrator / VibratorEffect +# ═══════════════════════════════════════════════════════════════ + +class Vibrator(Stub): + """Vibrator — 振动器""" + + def __init__(self): + super().__init__() + + def vibrate(self, milliseconds: int): + sys.stderr.write(f"[Vibrator] vibrate: {milliseconds}ms\n") + + def vibrate(self, pattern: List[int], repeat: int): + sys.stderr.write(f"[Vibrator] vibrate pattern: {pattern}, repeat={repeat}\n") + + def cancel(self): + sys.stderr.write("[Vibrator] cancel\n") + + def hasVibrator(self) -> bool: + return True + + def hasAmplitudeControl(self) -> bool: + return False + + def areEffectsSupported(self, effect_ids: List[int]) -> List[int]: + return [0] * len(effect_ids) + + def areAllEffectsSupported(self, effect_ids: List[int]) -> List[int]: + return [0] * len(effect_ids) + + +class VibratorEffect(Stub): + """VibratorEffect — 振动效果""" + + def __init__(self, effect_id: int = 0): + super().__init__() + self._effect_id = effect_id + + @staticmethod + def createOneShot(milliseconds: int, amplitude: int) -> "VibratorEffect": + return VibratorEffect(0) + + @staticmethod + def createWaveform(pattern: List[int], repeat: int) -> "VibratorEffect": + return VibratorEffect(0) + + @staticmethod + def createPredefined(effect_id: int) -> "VibratorEffect": + return VibratorEffect(effect_id) + + +# ═══════════════════════════════════════════════════════════════ +# BatteryManager +# ═══════════════════════════════════════════════════════════════ + +class BatteryManager(Stub): + """BatteryManager — 电池管理""" + + BATTERY_PROPERTY_CHARGE_COUNTER = 1 + BATTERY_PROPERTY_CURRENT_NOW = 2 + BATTERY_PROPERTY_CURRENT_AVERAGE = 3 + BATTERY_PROPERTY_CAPACITY = 4 + BATTERY_PROPERTY_STATUS = 5 + BATTERY_PROPERTY_ENERGY_COUNTER = 6 + BATTERY_PROPERTY_BATTERY_NOW = 7 + BATTERY_PROPERTY_BATTERY_LOW = 8 + + BATTERY_STATUS_UNKNOWN = 1 + BATTERY_STATUS_CHARGING = 2 + BATTERY_STATUS_DISCHARGING = 3 + BATTERY_STATUS_NOT_CHARGING = 4 + BATTERY_STATUS_FULL = 5 + + BATTERY_HEALTH_UNKNOWN = 1 + BATTERY_HEALTH_GOOD = 2 + BATTERY_HEALTH_OVERHEAT = 3 + BATTERY_HEALTH_DEAD = 4 + BATTERY_HEALTH_OVER_VOLTAGE = 5 + BATTERY_HEALTH_UNSPECIFIED_FAILURE = 6 + BATTERY_HEALTH_COLD = 7 + + EXTRA_STATUS = "status" + EXTRA_HEALTH = "health" + EXTRA_PRESENT = "present" + EXTRA_LEVEL = "level" + EXTRA_SCALE = "scale" + EXTRA_ICON_SMALL = "icon-small" + EXTRA_PLUGGED = "plugged" + EXTRA_VOLTAGE = "voltage" + EXTRA_TEMPERATURE = "temperature" + EXTRA_TECHNOLOGY = "technology" + EXTRA_BATTERY_LOW = "battery_low" + EXTRA_CHARGE_COUNTER = "charge_counter" + + BATTERY_PLUGGED_AC = 1 + BATTERY_PLUGGED_USB = 2 + BATTERY_PLUGGED_WIRELESS = 4 + + def __init__(self): + super().__init__() + self._capacity = 85 + self._status = self.BATTERY_STATUS_CHARGING + self._health = self.BATTERY_HEALTH_GOOD + self._present = True + self._level = 85 + self._plugged = self.BATTERY_PLUGGED_AC + self._voltage = 4200 + self._temperature = 250 + self._technology = "Li-ion" + self._charge_counter = 3500000 + self._current_now = -500000 + self._current_avg = -450000 + self._energy_counter = 15000000 + + def getIntProperty(self, prop_id: int) -> int: + if prop_id == self.BATTERY_PROPERTY_CAPACITY: + return self._capacity + if prop_id == self.BATTERY_PROPERTY_STATUS: + return self._status + if prop_id == self.BATTERY_PROPERTY_CHARGE_COUNTER: + return self._charge_counter + if prop_id == self.BATTERY_PROPERTY_CURRENT_NOW: + return self._current_now + if prop_id == self.BATTERY_PROPERTY_CURRENT_AVERAGE: + return self._current_avg + if prop_id == self.BATTERY_PROPERTY_ENERGY_COUNTER: + return self._energy_counter + return -1 + + def getLongProperty(self, prop_id: int) -> int: + return self.getIntProperty(prop_id) + + def isCharging(self) -> bool: + return self._status == self.BATTERY_STATUS_CHARGING + + def computeChargeTimeRemaining(self) -> int: + return 1800000 # 30 minutes + + def toString(self) -> str: + return f"BatteryManager(level={self._level}%, charging={self.isCharging()})" + + +# ═══════════════════════════════════════════════════════════════ +# 异常类 +# ═══════════════════════════════════════════════════════════════ + +class NetworkOnMainThreadException(RuntimeError): + """网络操作在主线程上执行""" + pass + + +class TransactionTooLargeException(RuntimeError): + """事务数据太大""" + pass + + +class OperationCanceledException(RuntimeError): + """操作被取消""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# 其他工具类 +# ═══════════════════════════════════════════════════════════════ + +class ConditionVariable(Stub): + """ConditionVariable — 条件变量""" + + def __init__(self): + super().__init__() + self._cond = threading.Condition() + self._open = False + + def block(self, timeout_ms: int = 0): + with self._cond: + if timeout_ms <= 0: + while not self._open: + self._cond.wait() + else: + if not self._open: + self._cond.wait(timeout_ms / 1000.0) + + def open(self): + with self._cond: + self._open = True + self._cond.notify_all() + + def close(self): + with self._cond: + self._open = False + + +class Semaphore(Stub): + """Semaphore — 信号量""" + + def __init__(self, permits: int): + super().__init__() + self._sem = threading.Semaphore(permits) + + def acquire(self): + self._sem.acquire() + + def tryAcquire(self) -> bool: + return self._sem.acquire(blocking=False) + + def release(self): + self._sem.release() + + def availablePermits(self) -> int: + return self._sem._value + + +class TokenWatcher(Stub): + """TokenWatcher — 令牌监视器""" + + def __init__(self, handler: Handler, tag: str): + super().__init__() + + def acquire(self, token: Any, tag: str): + pass + + def release(self, token: Any): + pass + + def isAcquired(self) -> bool: + return False + + def dump(self): + pass + + +class WorkSource(Stub): + """WorkSource — 工作来源""" + + def __init__(self, uid: int = 0): + super().__init__() + self._uids: List[int] = [uid] if uid else [] + + def add(self, uid: int): + if uid not in self._uids: + self._uids.append(uid) + + def remove(self, uid: int): + if uid in self._uids: + self._uids.remove(uid) + + def get(self, index: int) -> int: + if 0 <= index < len(self._uids): + return self._uids[index] + return 0 + + def size(self) -> int: + return len(self._uids) + + def clear(self): + self._uids.clear() + + def set(self, ws: "WorkSource"): + if ws: + self._uids = list(ws._uids) + + def toString(self) -> str: + return f"WorkSource({self._uids})" + + +class UserHandle(Stub): + """UserHandle — 用户句柄""" + + USER_ALL = -1 + USER_CURRENT = -2 + USER_CURRENT_OR_SELF = -3 + USER_NULL = -10000 + USER_OWNER = 0 + USER_SYSTEM = 0 + + def __init__(self, user_id: int = 0): + super().__init__() + self._user_id = user_id + + @staticmethod + def myUserId() -> int: + return 0 + + @staticmethod + def getUserHandleForUid(uid: int) -> "UserHandle": + return UserHandle(uid // 100000) + + @staticmethod + def getUserId(uid: int) -> int: + return uid // 100000 + + @staticmethod + def getUid(user_id: int, app_id: int) -> int: + return user_id * 100000 + app_id + + @staticmethod + def getAppId(uid: int) -> int: + return uid % 100000 + + def getIdentifier(self) -> int: + return self._user_id + + def hashCode(self) -> int: + return self._user_id + + def toString(self) -> str: + return f"UserHandle({self._user_id})" + + +class DropBoxManager(Stub): + """DropBoxManager — 系统日志收集""" + + IS_EMPTY = 0 + IS_TEXT = 1 + IS_GZIPPED = 2 + + class Entry(Stub): + def __init__(self, tag: str, milliseconds: int): + super().__init__() + self._tag = tag + self._millis = milliseconds + self._text = "" + + def getTag(self) -> str: + return self._tag + + def getTimeMillis(self) -> int: + return self._millis + + def getText(self, max_bytes: int) -> Optional[str]: + return self._text + + def getFlags(self, max_bytes: int) -> int: + return DropBoxManager.IS_TEXT + + def close(self): + pass + + def __init__(self): + super().__init__() + self._entries: List[DropBoxManager.Entry] = [] + + def addText(self, tag: str, text: str): + entry = DropBoxManager.Entry(tag, int(_time.time() * 1000)) + entry._text = text + self._entries.append(entry) + + def getNextEntry(self, tag: str, milliseconds: int) -> Optional[Entry]: + for entry in self._entries: + if entry.getTimeMillis() > milliseconds: + if not tag or entry.getTag() == tag: + return entry + return None + + def dump(self, pw: Any, prefix: str): + pass + + +class FileObserver(Stub): + """FileObserver — 文件监听""" + + ACCESS = 0x00000001 + MODIFY = 0x00000002 + ATTRIB = 0x00000004 + CLOSE_WRITE = 0x00000008 + CLOSE_NOWRITE = 0x00000010 + OPEN = 0x00000020 + MOVED_FROM = 0x00000040 + MOVED_TO = 0x00000080 + CREATE = 0x00000100 + DELETE = 0x00000200 + DELETE_SELF = 0x00000400 + MOVE_SELF = 0x00000800 + ALL_EVENTS = 0x00000FFF + + def __init__(self, path: str): + super().__init__() + self._path = path + self._watching = False + + def startWatching(self): + self._watching = True + + def stopWatching(self): + self._watching = False + + def onEvent(self, event: int, path: Optional[str]): + pass + + def finalize(self): + pass + + +class FileUtils(Stub): + """FileUtils — 文件工具""" + + @staticmethod + def readFileToString(file: Any) -> str: + return "" + + @staticmethod + def stringToFile(file: Any, string: str): + pass + + @staticmethod + def copyFile(src: Any, dest: Any) -> bool: + return True + + @staticmethod + def deleteDirectory(dir_: Any) -> bool: + return True + + @staticmethod + def deleteOlderFiles(dir_: Any, min_count: int, min_age_ms: int): + pass + + +class PatternMatcher(Stub): + """PatternMatcher — 模式匹配器""" + + PATTERN_LITERAL = 0 + PATTERN_PREFIX = 1 + PATTERN_SIMPLE_GLOB = 2 + PATTERN_ADVANCED_GLOB = 3 + PATTERN_SUFFIX = 4 + + def __init__(self, pattern: str, type_: int): + super().__init__() + self._pattern = pattern + self._type = type_ + + def getPath(self) -> str: + return self._pattern + + def getType(self) -> int: + return self._type + + def match(self, str_: str) -> bool: + if self._type == self.PATTERN_LITERAL: + return str_ == self._pattern + if self._type == self.PATTERN_PREFIX: + return str_.startswith(self._pattern) + if self._type == self.PATTERN_SUFFIX: + return str_.endswith(self._pattern) + if self._type == self.PATTERN_SIMPLE_GLOB: + import fnmatch + return fnmatch.fnmatch(str_, self._pattern) + return str_ == self._pattern + + def toString(self) -> str: + return f"PatternMatcher({self._pattern}, type={self._type})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Bundle", + "BaseBundle", + "PersistableBundle", + "Handler", + "Looper", + "Message", + "HandlerThread", + "PowerManager", + "PowerManagerWakeLock", + "Vibrator", + "VibratorEffect", + "Build", + "SystemClock", + "Environment", + "Parcel", + "Parcelable", + "Process", + "AsyncTask", + "CountDownTimer", + "Messenger", + "BatteryManager", + "PatternMatcher", + "ConditionVariable", + "Semaphore", + "TokenWatcher", + "WorkSource", + "UserHandle", + "DropBoxManager", + "FileObserver", + "FileUtils", + "NetworkOnMainThreadException", + "TransactionTooLargeException", + "OperationCanceledException", +] \ No newline at end of file diff --git a/android_pyapi/android/preference.py b/android_pyapi/android/preference.py new file mode 100644 index 0000000..f666cc1 --- /dev/null +++ b/android_pyapi/android/preference.py @@ -0,0 +1,654 @@ +""" +Android Preference 组件 — Preference, PreferenceScreen, PreferenceManager 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import json +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# PreferenceChangeListener +# ═══════════════════════════════════════════════════════════════ + +class PreferenceChangeListener(Stub): + """OnPreferenceChangeListener — 偏好变化监听器""" + + def onPreferenceChange(self, preference: "Preference", new_value: Any) -> bool: + return True + + +class PreferenceOnPreferenceClickListener(Stub): + """OnPreferenceClickListener — 偏好点击监听器""" + + def onPreferenceClick(self, preference: "Preference") -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# PreferenceManager +# ═══════════════════════════════════════════════════════════════ + +class PreferenceManager(Stub): + """PreferenceManager — 偏好管理器""" + + KEY_HAS_SET_DEFAULT_VALUES = "_has_set_default_values" + + def __init__(self, context: Any): + super().__init__() + self._context = context + self._shared_prefs_name: str = f"{_package_name}_preferences" + self._shared_prefs_mode: int = 0 + self._prefs_data: Dict[str, Any] = {} + self._prefs_path = os.path.join(_app_data_dir, "shared_prefs", + f"{self._shared_prefs_name}.json") + self._load() + + def _load(self): + os.makedirs(os.path.dirname(self._prefs_path), exist_ok=True) + if os.path.exists(self._prefs_path): + try: + with open(self._prefs_path, "r") as f: + self._prefs_data = json.load(f) + except (json.JSONDecodeError, OSError): + self._prefs_data = {} + else: + self._prefs_data = {} + + def _save(self): + os.makedirs(os.path.dirname(self._prefs_path), exist_ok=True) + with open(self._prefs_path, "w") as f: + json.dump(self._prefs_data, f) + + def getSharedPreferences(self) -> Any: + return self._prefs_data + + def setSharedPreferencesName(self, name: str): + self._shared_prefs_name = name + self._prefs_path = os.path.join(_app_data_dir, "shared_prefs", + f"{name}.json") + self._load() + + def setSharedPreferencesMode(self, mode: int): + self._shared_prefs_mode = mode + + @staticmethod + def getDefaultSharedPreferences(context: Any) -> Any: + mgr = PreferenceManager(context) + return mgr.getSharedPreferences() + + @staticmethod + def setDefaultValues(context: Any, res_id: int, read_again: bool): + pass + + @staticmethod + def setDefaultValues(context: Any, shared_prefs_name: str, + shared_prefs_mode: int, res_id: int, + read_again: bool): + pass + + def getPreference(self, key: str, default: Any = None) -> Any: + return self._prefs_data.get(key, default) + + def putPreference(self, key: str, value: Any): + self._prefs_data[key] = value + self._save() + + def removePreference(self, key: str): + if key in self._prefs_data: + del self._prefs_data[key] + self._save() + + +# ═══════════════════════════════════════════════════════════════ +# Preference +# ═══════════════════════════════════════════════════════════════ + +class Preference(Stub): + """Preference — 偏好设置项基类""" + + def __init__(self, context: Any = None, attrs: Any = None, + def_style_attr: int = 0): + super().__init__() + self._context = context + self._title: str = "" + self._summary: str = "" + self._key: Optional[str] = None + self._enabled: bool = True + self._default_value: Any = None + self._order: int = 0 + self._persistent: bool = True + self._icon: Any = None + self._dependents: List["Preference"] = [] + self._dependency_key: Optional[str] = None + self._preference_manager: Optional[PreferenceManager] = None + self._parent: Optional["PreferenceGroup"] = None + self._on_change_listener: Optional[PreferenceChangeListener] = None + self._on_click_listener: Optional[PreferenceOnPreferenceClickListener] = None + + def setTitle(self, title: str): + self._title = title + + def getTitle(self) -> str: + return self._title + + def setSummary(self, summary: str): + self._summary = summary + + def getSummary(self) -> str: + return self._summary + + def setKey(self, key: str): + self._key = key + + def getKey(self) -> Optional[str]: + return self._key + + def setEnabled(self, enabled: bool): + self._enabled = enabled + + def isEnabled(self) -> bool: + return self._enabled + + def setDefaultValue(self, value: Any): + self._default_value = value + + def setOnPreferenceClickListener(self, listener: PreferenceOnPreferenceClickListener): + self._on_click_listener = listener + + def setOnPreferenceChangeListener(self, listener: PreferenceChangeListener): + self._on_change_listener = listener + + def getSharedPreferences(self) -> Any: + if self._preference_manager: + return self._preference_manager.getSharedPreferences() + return {} + + def getEditor(self) -> Any: + if self._preference_manager: + return self._preference_manager + return None + + def shouldCommit(self) -> bool: + return True + + def shouldPersist(self) -> bool: + return self._persistent and self._key is not None + + def persistString(self, value: str) -> bool: + if self.shouldPersist() and self._preference_manager: + self._preference_manager.putPreference(self._key, value) + return True + return False + + def persistInt(self, value: int) -> bool: + if self.shouldPersist() and self._preference_manager: + self._preference_manager.putPreference(self._key, value) + return True + return False + + def persistBoolean(self, value: bool) -> bool: + if self.shouldPersist() and self._preference_manager: + self._preference_manager.putPreference(self._key, value) + return True + return False + + def persistFloat(self, value: float) -> bool: + if self.shouldPersist() and self._preference_manager: + self._preference_manager.putPreference(self._key, value) + return True + return False + + def persistLong(self, value: int) -> bool: + return self.persistInt(value) + + def getPersistedString(self, default_return: Optional[str] = None) -> Optional[str]: + if self.shouldPersist() and self._preference_manager: + val = self._preference_manager.getPreference(self._key, default_return) + return str(val) if val is not None else default_return + return default_return + + def getPersistedInt(self, default_return: int = 0) -> int: + if self.shouldPersist() and self._preference_manager: + return int(self._preference_manager.getPreference(self._key, default_return)) + return default_return + + def getPersistedBoolean(self, default_return: bool = False) -> bool: + if self.shouldPersist() and self._preference_manager: + val = self._preference_manager.getPreference(self._key, default_return) + if isinstance(val, bool): + return val + return str(val).lower() in ("true", "1", "yes") + return default_return + + def getPersistedFloat(self, default_return: float = 0.0) -> float: + if self.shouldPersist() and self._preference_manager: + return float(self._preference_manager.getPreference(self._key, default_return)) + return default_return + + def getPersistedLong(self, default_return: int = 0) -> int: + return self.getPersistedInt(default_return) + + def notifyChanged(self): + pass + + def notifyHierarchyChanged(self): + pass + + def onBindView(self, view: Any): + pass + + def onClick(self): + if self._on_click_listener: + self._on_click_listener.onPreferenceClick(self) + + def setOrder(self, order: int): + self._order = order + + def getOrder(self) -> int: + return self._order + + def setPersistent(self, persistent: bool): + self._persistent = persistent + + def isPersistent(self) -> bool: + return self._persistent + + def setDependency(self, dependency_key: str): + self._dependency_key = dependency_key + + def getDependency(self) -> Optional[str]: + return self._dependency_key + + def toString(self) -> str: + return f"Preference(key={self._key}, title={self._title})" + + def hashCode(self) -> int: + return hash(self._key) & 0x7FFFFFFF if self._key else 0 + + +# ═══════════════════════════════════════════════════════════════ +# PreferenceGroup +# ═══════════════════════════════════════════════════════════════ + +class PreferenceGroup(Preference): + """PreferenceGroup — 偏好组""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._preferences: List[Preference] = [] + + def addPreference(self, preference: Preference) -> bool: + if preference not in self._preferences: + self._preferences.append(preference) + preference._parent = self + return True + return False + + def removePreference(self, preference: Preference) -> bool: + if preference in self._preferences: + self._preferences.remove(preference) + preference._parent = None + return True + return False + + def removeAll(self): + for p in self._preferences: + p._parent = None + self._preferences.clear() + + def getPreference(self, index: int) -> Optional[Preference]: + if 0 <= index < len(self._preferences): + return self._preferences[index] + return None + + def getPreferenceCount(self) -> int: + return len(self._preferences) + + def findPreference(self, key: str) -> Optional[Preference]: + for p in self._preferences: + if p.getKey() == key: + return p + if isinstance(p, PreferenceGroup): + result = p.findPreference(key) + if result: + return result + return None + + def toString(self) -> str: + return f"PreferenceGroup(count={len(self._preferences)})" + + +# ═══════════════════════════════════════════════════════════════ +# PreferenceScreen +# ═══════════════════════════════════════════════════════════════ + +class PreferenceScreen(PreferenceGroup): + """PreferenceScreen — 根偏好屏幕""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._root: bool = True + + +# ═══════════════════════════════════════════════════════════════ +# PreferenceCategory +# ═══════════════════════════════════════════════════════════════ + +class PreferenceCategory(PreferenceGroup): + """PreferenceCategory — 偏好分类标题""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + + +# ═══════════════════════════════════════════════════════════════ +# TwoStatePreference +# ═══════════════════════════════════════════════════════════════ + +class TwoStatePreference(Preference): + """TwoStatePreference — 双状态偏好基类""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._checked: bool = False + self._summary_on: str = "" + self._summary_off: str = "" + self._disable_dependents_state: bool = False + + def isChecked(self) -> bool: + return self._checked + + def setChecked(self, checked: bool): + self._checked = checked + self.persistBoolean(checked) + + def setSummaryOn(self, summary: str): + self._summary_on = summary + + def setSummaryOff(self, summary: str): + self._summary_off = summary + + def setDisableDependentsState(self, state: bool): + self._disable_dependents_state = state + + +# ═══════════════════════════════════════════════════════════════ +# CheckBoxPreference +# ═══════════════════════════════════════════════════════════════ + +class CheckBoxPreference(TwoStatePreference): + """CheckBoxPreference — 复选框偏好""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + + +# ═══════════════════════════════════════════════════════════════ +# SwitchPreference +# ═══════════════════════════════════════════════════════════════ + +class SwitchPreference(TwoStatePreference): + """SwitchPreference — 开关偏好""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._switch_text_on: str = "" + self._switch_text_off: str = "" + + def setSwitchTextOn(self, text: str): + self._switch_text_on = text + + def setSwitchTextOff(self, text: str): + self._switch_text_off = text + + def getSwitchTextOn(self) -> str: + return self._switch_text_on + + def getSwitchTextOff(self) -> str: + return self._switch_text_off + + +# ═══════════════════════════════════════════════════════════════ +# DialogPreference +# ═══════════════════════════════════════════════════════════════ + +class DialogPreference(Preference): + """DialogPreference — 对话框偏好基类""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._dialog_title: str = "" + self._dialog_message: str = "" + self._dialog_icon: Any = None + self._positive_button_text: str = "" + self._negative_button_text: str = "" + + +# ═══════════════════════════════════════════════════════════════ +# EditTextPreference +# ═══════════════════════════════════════════════════════════════ + +class EditTextPreference(DialogPreference): + """EditTextPreference — 文本编辑偏好""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._text: str = "" + + def getText(self) -> str: + return self._text + + def setText(self, text: str): + self._text = text + self.persistString(text) + + +# ═══════════════════════════════════════════════════════════════ +# ListPreference +# ═══════════════════════════════════════════════════════════════ + +class ListPreference(DialogPreference): + """ListPreference — 列表偏好""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._entries: List[str] = [] + self._entry_values: List[str] = [] + self._value: str = "" + + def getEntries(self) -> List[str]: + return list(self._entries) + + def setEntries(self, entries: List[str]): + self._entries = list(entries) + + def getEntryValues(self) -> List[str]: + return list(self._entry_values) + + def setEntryValues(self, values: List[str]): + self._entry_values = list(values) + + def getValue(self) -> str: + return self._value + + def setValue(self, value: str): + self._value = value + self.persistString(value) + + def getEntry(self) -> Optional[str]: + try: + idx = self._entry_values.index(self._value) + if 0 <= idx < len(self._entries): + return self._entries[idx] + except ValueError: + pass + return None + + def getSummary(self) -> str: + entry = self.getEntry() + if entry: + return entry + return self._summary + + def setDefaultValue(self, value: Any): + super().setDefaultValue(value) + + +# ═══════════════════════════════════════════════════════════════ +# MultiSelectListPreference +# ═══════════════════════════════════════════════════════════════ + +class MultiSelectListPreference(DialogPreference): + """MultiSelectListPreference — 多选列表偏好""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._entries: List[str] = [] + self._entry_values: List[str] = [] + self._values: Set[str] = set() + + def getEntries(self) -> List[str]: + return list(self._entries) + + def setEntries(self, entries: List[str]): + self._entries = list(entries) + + def getEntryValues(self) -> List[str]: + return list(self._entry_values) + + def setEntryValues(self, values: List[str]): + self._entry_values = list(values) + + def getValues(self) -> Set[str]: + return set(self._values) + + def setValues(self, values: Set[str]): + self._values = set(values) + + +# ═══════════════════════════════════════════════════════════════ +# RingtonePreference / SeekBarPreference / DropDownPreference +# ═══════════════════════════════════════════════════════════════ + +class RingtonePreference(Preference): + """RingtonePreference — 铃声偏好""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._ringtone_type: int = 1 + self._show_default: bool = True + self._show_silent: bool = True + + +class SeekBarPreference(Preference): + """SeekBarPreference — 滑块偏好""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + self._min: int = 0 + self._max: int = 100 + self._current: int = 50 + self._show_progress: bool = True + self._adjustable: bool = True + + def getMin(self) -> int: + return self._min + + def setMin(self, min_val: int): + self._min = min_val + + def getMax(self) -> int: + return self._max + + def setMax(self, max_val: int): + self._max = max_val + + def getValue(self) -> int: + return self._current + + def setValue(self, value: int): + self._current = value + self.persistInt(value) + + +class DropDownPreference(ListPreference): + """DropDownPreference — 下拉列表偏好""" + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__(context, attrs) + + +# ═══════════════════════════════════════════════════════════════ +# PreferenceFragment / PreferenceFragmentCompat +# ═══════════════════════════════════════════════════════════════ + +class PreferenceFragment(Stub): + """PreferenceFragment — 偏好片段""" + + def __init__(self): + super().__init__() + self._preference_manager: Optional[PreferenceManager] = None + + def onCreate(self, savedInstanceState: Any): + pass + + def onCreatePreferences(self, savedInstanceState: Any, root_key: Optional[str]): + pass + + def addPreferencesFromResource(self, res_id: int): + pass + + def setPreferenceScreen(self, screen: PreferenceScreen): + pass + + def getPreferenceScreen(self) -> Optional[PreferenceScreen]: + return None + + def findPreference(self, key: str) -> Optional[Preference]: + screen = self.getPreferenceScreen() + if screen: + return screen.findPreference(key) + return None + + +class PreferenceFragmentCompat(PreferenceFragment): + """PreferenceFragmentCompat — 兼容偏好片段""" + + def __init__(self): + super().__init__() + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Preference", + "PreferenceScreen", + "PreferenceCategory", + "PreferenceManager", + "CheckBoxPreference", + "SwitchPreference", + "ListPreference", + "EditTextPreference", + "MultiSelectListPreference", + "PreferenceFragment", + "PreferenceFragmentCompat", + "PreferenceGroup", + "PreferenceChangeListener", + "PreferenceOnPreferenceClickListener", + "TwoStatePreference", + "DialogPreference", + "RingtonePreference", + "SeekBarPreference", + "DropDownPreference", +] \ No newline at end of file diff --git a/android_pyapi/android/provider.py b/android_pyapi/android/provider.py new file mode 100644 index 0000000..644057a --- /dev/null +++ b/android_pyapi/android/provider.py @@ -0,0 +1,550 @@ +""" +Android Provider 组件 — Settings, MediaStore, ContactsContract 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import json +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# OpenableColumns +# ═══════════════════════════════════════════════════════════════ + +class OpenableColumns(Stub): + DISPLAY_NAME = "_display_name" + SIZE = "_size" + + +# ═══════════════════════════════════════════════════════════════ +# Settings +# ═══════════════════════════════════════════════════════════════ + +class _SettingsBase(Stub): + """Settings 基类 — 存储到 JSON 文件""" + + _DATA_DIR = os.path.join(_app_data_dir, "shared_prefs") + _cache: Dict[str, Dict[str, Any]] = {} + + @classmethod + def _get_path(cls, name: str) -> str: + os.makedirs(cls._DATA_DIR, exist_ok=True) + return os.path.join(cls._DATA_DIR, f"{name}.json") + + @classmethod + def _load(cls, name: str) -> Dict[str, Any]: + if name in cls._cache: + return cls._cache[name] + path = cls._get_path(name) + if os.path.exists(path): + try: + with open(path, "r") as f: + data = json.load(f) + cls._cache[name] = data + return data + except (json.JSONDecodeError, OSError): + pass + cls._cache[name] = {} + return cls._cache[name] + + @classmethod + def _save(cls, name: str): + path = cls._get_path(name) + try: + with open(path, "w") as f: + json.dump(cls._cache.get(name, {}), f) + except OSError: + pass + + @staticmethod + def putString(name: str, key: str, value: str) -> bool: + data = _SettingsBase._load(name) + data[key] = value + _SettingsBase._save(name) + return True + + @staticmethod + def getString(name: str, key: str, default_value: Optional[str] = None) -> Optional[str]: + data = _SettingsBase._load(name) + return data.get(key, default_value) + + @staticmethod + def putInt(name: str, key: str, value: int) -> bool: + data = _SettingsBase._load(name) + data[key] = value + _SettingsBase._save(name) + return True + + @staticmethod + def getInt(name: str, key: str, default_value: int = 0) -> int: + data = _SettingsBase._load(name) + return data.get(key, default_value) + + @staticmethod + def putLong(name: str, key: str, value: int) -> bool: + return _SettingsBase.putInt(name, key, value) + + @staticmethod + def getLong(name: str, key: str, default_value: int = 0) -> int: + return _SettingsBase.getInt(name, key, default_value) + + @staticmethod + def putFloat(name: str, key: str, value: float) -> bool: + data = _SettingsBase._load(name) + data[key] = value + _SettingsBase._save(name) + return True + + @staticmethod + def getFloat(name: str, key: str, default_value: float = 0.0) -> float: + data = _SettingsBase._load(name) + return data.get(key, default_value) + + +class SettingsSystem(_SettingsBase): + _NAME = "settings_system" + + @staticmethod + def putInt(key: str, value: int) -> bool: + return _SettingsBase.putInt(SettingsSystem._NAME, key, value) + + @staticmethod + def getInt(key: str, default_value: int = 0) -> int: + return _SettingsBase.getInt(SettingsSystem._NAME, key, default_value) + + @staticmethod + def putString(key: str, value: str) -> bool: + return _SettingsBase.putString(SettingsSystem._NAME, key, value) + + @staticmethod + def getString(key: str, default_value: Optional[str] = None) -> Optional[str]: + return _SettingsBase.getString(SettingsSystem._NAME, key, default_value) + + @staticmethod + def putFloat(key: str, value: float) -> bool: + return _SettingsBase.putFloat(SettingsSystem._NAME, key, value) + + @staticmethod + def getFloat(key: str, default_value: float = 0.0) -> float: + return _SettingsBase.getFloat(SettingsSystem._NAME, key, default_value) + + @staticmethod + def putLong(key: str, value: int) -> bool: + return _SettingsBase.putLong(SettingsSystem._NAME, key, value) + + @staticmethod + def getLong(key: str, default_value: int = 0) -> int: + return _SettingsBase.getLong(SettingsSystem._NAME, key, default_value) + + +class SettingsSecure(_SettingsBase): + _NAME = "settings_secure" + + @staticmethod + def putInt(key: str, value: int) -> bool: + return _SettingsBase.putInt(SettingsSecure._NAME, key, value) + + @staticmethod + def getInt(key: str, default_value: int = 0) -> int: + return _SettingsBase.getInt(SettingsSecure._NAME, key, default_value) + + @staticmethod + def putString(key: str, value: str) -> bool: + return _SettingsBase.putString(SettingsSecure._NAME, key, value) + + @staticmethod + def getString(key: str, default_value: Optional[str] = None) -> Optional[str]: + return _SettingsBase.getString(SettingsSecure._NAME, key, default_value) + + @staticmethod + def putFloat(key: str, value: float) -> bool: + return _SettingsBase.putFloat(SettingsSecure._NAME, key, value) + + @staticmethod + def getFloat(key: str, default_value: float = 0.0) -> float: + return _SettingsBase.getFloat(SettingsSecure._NAME, key, default_value) + + @staticmethod + def putLong(key: str, value: int) -> bool: + return _SettingsBase.putLong(SettingsSecure._NAME, key, value) + + @staticmethod + def getLong(key: str, default_value: int = 0) -> int: + return _SettingsBase.getLong(SettingsSecure._NAME, key, default_value) + + +class SettingsGlobal(_SettingsBase): + _NAME = "settings_global" + + @staticmethod + def putInt(key: str, value: int) -> bool: + return _SettingsBase.putInt(SettingsGlobal._NAME, key, value) + + @staticmethod + def getInt(key: str, default_value: int = 0) -> int: + return _SettingsBase.getInt(SettingsGlobal._NAME, key, default_value) + + @staticmethod + def putString(key: str, value: str) -> bool: + return _SettingsBase.putString(SettingsGlobal._NAME, key, value) + + @staticmethod + def getString(key: str, default_value: Optional[str] = None) -> Optional[str]: + return _SettingsBase.getString(SettingsGlobal._NAME, key, default_value) + + @staticmethod + def putFloat(key: str, value: float) -> bool: + return _SettingsBase.putFloat(SettingsGlobal._NAME, key, value) + + @staticmethod + def getFloat(key: str, default_value: float = 0.0) -> float: + return _SettingsBase.getFloat(SettingsGlobal._NAME, key, default_value) + + @staticmethod + def putLong(key: str, value: int) -> bool: + return _SettingsBase.putLong(SettingsGlobal._NAME, key, value) + + @staticmethod + def getLong(key: str, default_value: int = 0) -> int: + return _SettingsBase.getLong(SettingsGlobal._NAME, key, default_value) + + +class Settings(Stub): + """Settings — 系统设置""" + + System = SettingsSystem + Secure = SettingsSecure + Global = SettingsGlobal + + ACTION_SETTINGS = "android.settings.SETTINGS" + ACTION_APPLICATION_DETAILS_SETTINGS = "android.settings.APPLICATION_DETAILS_SETTINGS" + EXTRA_APP_PACKAGE = "android.intent.extra.PACKAGE_NAME" + + +# ═══════════════════════════════════════════════════════════════ +# MediaStore +# ═══════════════════════════════════════════════════════════════ + +class MediaStore(Stub): + """MediaStore — 媒体存储""" + + class MediaColumns(Stub): + _ID = 0 + DATA = 1 + SIZE = 2 + DISPLAY_NAME = 3 + MIME_TYPE = 4 + TITLE = 5 + DATE_ADDED = 6 + DATE_MODIFIED = 7 + ORIENTATION = 8 + WIDTH = 9 + HEIGHT = 10 + + class Files(Stub): + @staticmethod + def getContentUri(volume_name: str) -> str: + return f"content://media/{volume_name}/file" + + class Images(Stub): + class Media(Stub): + EXTERNAL_CONTENT_URI = "content://media/external/images/media" + INTERNAL_CONTENT_URI = "content://media/internal/images/media" + + class Thumbnails(Stub): + EXTERNAL_CONTENT_URI = "content://media/external/images/thumbnails" + + class Video(Stub): + class Media(Stub): + EXTERNAL_CONTENT_URI = "content://media/external/video/media" + + class Audio(Stub): + class Media(Stub): + EXTERNAL_CONTENT_URI = "content://media/external/audio/media" + + @staticmethod + def getVersion(context: Any, uri: str) -> str: + return "0" + + @staticmethod + def setVersion(context: Any, uri: str, version: str) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# DocumentsContract +# ═══════════════════════════════════════════════════════════════ + +class DocumentsContract(Stub): + """DocumentsContract — 文档合约""" + + EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI" + EXTRA_LOADING = "android.provider.extra.LOADING" + EXTRA_INFO = "android.provider.extra.INFO" + EXTRA_PROMPT = "android.provider.extra.PROMPT" + + @staticmethod + def buildDocumentUri(authority: str, document_id: str) -> str: + return f"content://{authority}/document/{document_id}" + + @staticmethod + def buildDocumentUriUsingTree(tree_uri: str, document_id: str) -> str: + return f"{tree_uri}/document/{document_id}" + + @staticmethod + def buildChildDocumentsUri(authority: str, parent_document_id: str) -> str: + return f"content://{authority}/document/{parent_document_id}/children" + + @staticmethod + def buildChildDocumentsUriUsingTree(tree_uri: str, parent_document_id: str) -> str: + return f"{tree_uri}/document/{parent_document_id}/children" + + @staticmethod + def getDocumentId(document_uri: str) -> str: + return document_uri.rsplit("/", 1)[-1] + + @staticmethod + def getTreeDocumentId(document_uri: str) -> str: + parts = document_uri.split("/") + if "document" in parts: + idx = parts.index("document") + if idx + 1 < len(parts): + return parts[idx + 1] + return "" + + @staticmethod + def getRootId(root_uri: str) -> str: + parts = root_uri.split("/") + if "root" in parts: + idx = parts.index("root") + if idx + 1 < len(parts): + return parts[idx + 1] + return "" + + +# ═══════════════════════════════════════════════════════════════ +# ContactsContract +# ═══════════════════════════════════════════════════════════════ + +class ContactsContract(Stub): + """ContactsContract — 联系人合约""" + + class Contacts(Stub): + CONTENT_URI = "content://com.android.contacts/contacts" + CONTENT_FILTER_URI = "content://com.android.contacts/contacts/filter" + DISPLAY_NAME = "display_name" + PHOTO_URI = "photo_uri" + _ID = "_id" + + class CommonDataKinds(Stub): + class Email(Stub): + CONTENT_URI = "content://com.android.contacts/data/emails" + + class Phone(Stub): + CONTENT_URI = "content://com.android.contacts/data/phones" + + class Data(Stub): + CONTENT_URI = "content://com.android.contacts/data" + + +class ContactsContractContacts(ContactsContract.Contacts): + pass + + +class ContactsContractData(ContactsContract.Data): + pass + + +# ═══════════════════════════════════════════════════════════════ +# CallLog +# ═══════════════════════════════════════════════════════════════ + +class CallLog(Stub): + """CallLog — 通话记录""" + + class Calls(Stub): + CONTENT_URI = "content://call_log/calls" + _ID = "_id" + NUMBER = "number" + TYPE = "type" + TYPE_INCOMING = 1 + TYPE_OUTGOING = 2 + TYPE_MISSED = 3 + TYPE_VOICEMAIL = 4 + TYPE_REJECTED = 5 + TYPE_BLOCKED = 6 + TYPE_ANSWERED_EXTERNALLY = 7 + DATE = "date" + DURATION = "duration" + NEW = "new" + VOICEMAIL_URI = "voicemail_uri" + CACHED_NAME = "cached_name" + CACHED_NUMBER_LABEL = "cached_number_label" + GEOCODED_LOCATION = "geocoded_location" + COUNTRY_ISO = "country_iso" + + @staticmethod + def getLastOutgoingCall(context: Any) -> Optional[str]: + return None + + +# ═══════════════════════════════════════════════════════════════ +# CalendarContract +# ═══════════════════════════════════════════════════════════════ + +class CalendarContract(Stub): + """CalendarContract — 日历合约""" + + class Events(Stub): + CONTENT_URI = "content://com.android.calendar/events" + _ID = "_id" + TITLE = "title" + DESCRIPTION = "description" + EVENT_LOCATION = "event_location" + DTSTART = "dtstart" + DTEND = "dtend" + EVENT_TIMEZONE = "event_timezone" + DURATION = "duration" + ALL_DAY = "allDay" + CALENDAR_ID = "calendar_id" + ORGANIZER = "organizer" + STATUS = "eventStatus" + AVAILABILITY = "availability" + ACCESS_LEVEL = "accessLevel" + + class Attendees(Stub): + CONTENT_URI = "content://com.android.calendar/attendees" + + class Reminders(Stub): + CONTENT_URI = "content://com.android.calendar/reminders" + + +# ═══════════════════════════════════════════════════════════════ +# Browser / SearchRecentSuggestions / UserDictionary +# ═══════════════════════════════════════════════════════════════ + +class Browser(Stub): + """Browser — 浏览器书签""" + + class BookmarkColumns(Stub): + TITLE = "title" + URL = "url" + _ID = "_id" + BOOKMARK = "bookmark" + DATE = "date" + CREATED = "created" + VISITS = "visits" + + class SearchColumns(Stub): + URL = "url" + SEARCH = "search" + DATE = "date" + + HISTORY_PROJECTION = ["_id", "url", "title", "date", "visits", "bookmark", "created"] + SEARCHES_PROJECTION = ["_id", "search", "date"] + + +class SearchRecentSuggestions(Stub): + """SearchRecentSuggestions — 搜索建议""" + + def __init__(self, context: Any, authority: str, mode: int): + super().__init__() + self._authority = authority + self._mode = mode + + def saveRecentQuery(self, query_string: str, line2: str): + pass + + def clearHistory(self): + pass + + +class UserDictionary(Stub): + """UserDictionary — 用户词典""" + + class Words(Stub): + CONTENT_URI = "content://user_dictionary/words" + _ID = "_id" + WORD = "word" + FREQUENCY = "frequency" + LOCALE = "locale" + APP_ID = "app_id" + SHORTCUT = "shortcut" + + +# ═══════════════════════════════════════════════════════════════ +# SyncStateContract / LiveFolders +# ═══════════════════════════════════════════════════════════════ + +class SyncStateContract(Stub): + """SyncStateContract — 同步状态合约""" + + class Columns(Stub): + _ID = "_id" + ACCOUNT_NAME = "account_name" + ACCOUNT_TYPE = "account_type" + DATA = "data" + + class Helpers(Stub): + @staticmethod + def get(cr: Any, provider_uri: str, account: Any) -> Optional[bytes]: + return None + + @staticmethod + def set(cr: Any, provider_uri: str, account: Any, data: bytes): + pass + + @staticmethod + def insert(cr: Any, provider_uri: str, account: Any, data: bytes): + pass + + @staticmethod + def update(cr: Any, provider_uri: str, account: Any, data: bytes): + pass + + +class LiveFolders(Stub): + """LiveFolders — 实时文件夹""" + + NAME = "name" + DESCRIPTION = "description" + INTENT = "intent" + ICON_BITMAP = "icon_bitmap" + ICON_PACKAGE = "icon_package" + ICON_RESOURCE = "icon_resource" + DISPLAY_MODE = "display_mode" + DISPLAY_MODE_GRID = 1 + DISPLAY_MODE_LIST = 2 + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Settings", + "SettingsSystem", + "SettingsSecure", + "SettingsGlobal", + "CallLog", + "ContactsContract", + "ContactsContractContacts", + "ContactsContractData", + "MediaStore", + "DocumentsContract", + "CalendarContract", + "Browser", + "SearchRecentSuggestions", + "UserDictionary", + "SyncStateContract", + "LiveFolders", + "OpenableColumns", +] \ No newline at end of file diff --git a/android_pyapi/android/speech_tts.py b/android_pyapi/android/speech_tts.py new file mode 100644 index 0000000..60c8dcd --- /dev/null +++ b/android_pyapi/android/speech_tts.py @@ -0,0 +1,401 @@ +""" +Android Speech/TTS 组件 — TextToSpeech, Voice, SynthesisCallback 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import locale as _locale +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# TextToSpeech.OnInitListener +# ═══════════════════════════════════════════════════════════════ + +class TextToSpeechOnInitListener(Stub): + """OnInitListener — 初始化监听器""" + + def onInit(self, status: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# UtteranceProgressListener +# ═══════════════════════════════════════════════════════════════ + +class UtteranceProgressListener(Stub): + """UtteranceProgressListener — 发音进度监听器""" + + def onStart(self, utterance_id: str): + pass + + def onDone(self, utterance_id: str): + pass + + def onError(self, utterance_id: str, error_code: int = -1): + pass + + def onStop(self, utterance_id: str, interrupted: bool = False): + pass + + def onBeginSynthesis(self, utterance_id: str, sample_rate_in_hz: int, + audio_format: int, channel_count: int): + pass + + def onRangeStart(self, utterance_id: str, start: int, end: int, frame: int): + pass + + +class TextToSpeechOnUtteranceCompletedListener(Stub): + """OnUtteranceCompletedListener — 旧版发音完成监听器""" + + def onUtteranceCompleted(self, utterance_id: str): + pass + + +# ═══════════════════════════════════════════════════════════════ +# SynthesisRequest +# ═══════════════════════════════════════════════════════════════ + +class SynthesisRequest(Stub): + """SynthesisRequest — 合成请求""" + + def __init__(self): + super().__init__() + self._text: str = "" + self._speech_rate: float = 1.0 + self._voice: Optional["Voice"] = None + self._calling_uid: int = 0 + self._country: str = "" + self._language: str = "" + self._variant: str = "" + self._params: Dict[str, str] = {} + + def getText(self) -> str: + return self._text + + def getSpeechRate(self) -> float: + return self._speech_rate + + def getVoice(self) -> Optional["Voice"]: + return self._voice + + def getCallingUid(self) -> int: + return self._calling_uid + + def getCountry(self) -> str: + return self._country + + def getLanguage(self) -> str: + return self._language + + def getVariant(self) -> str: + return self._variant + + def getParams(self) -> Dict[str, str]: + return dict(self._params) + + +# ═══════════════════════════════════════════════════════════════ +# SynthesisCallback +# ═══════════════════════════════════════════════════════════════ + +class SynthesisCallback(Stub): + """SynthesisCallback — 合成回调""" + + def start(self, sample_rate_in_hz: int, audio_format: int, channel_count: int) -> int: + return 0 + + def getMaxBufferSize(self) -> int: + return 8192 + + def getChannelCount(self) -> int: + return 1 + + def getAudioFormat(self) -> int: + return 1 + + def getSampleRate(self) -> int: + return 22050 + + def availableBuffer(self, buffer_size: int) -> int: + return buffer_size + + def audioAvailable(self, audio: bytes) -> int: + return len(audio) + + def done(self) -> int: + return 0 + + def error(self) -> int: + return -1 + + def hasStarted(self) -> bool: + return False + + def hasFinished(self) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# Voice +# ═══════════════════════════════════════════════════════════════ + +class Voice(Stub): + """Voice — 语音""" + + QUALITY_DEFAULT = -1 + QUALITY_VERY_LOW = 100 + QUALITY_LOW = 200 + QUALITY_NORMAL = 300 + QUALITY_HIGH = 400 + QUALITY_VERY_HIGH = 500 + + LATENCY_DEFAULT = -1 + LATENCY_VERY_LOW = 100 + LATENCY_LOW = 200 + LATENCY_NORMAL = 300 + LATENCY_HIGH = 400 + LATENCY_VERY_HIGH = 500 + + def __init__(self, name: str = "", locale: Any = None, + quality: int = QUALITY_NORMAL, latency: int = LATENCY_NORMAL, + requires_network: bool = False, + features: Optional[List[str]] = None): + super().__init__() + self._name = name + self._locale = locale or _locale.getdefaultlocale()[0] + self._quality = quality + self._latency = latency + self._requires_network = requires_network + self._features = features or [] + + def getName(self) -> str: + return self._name + + def getLocale(self) -> Any: + return self._locale + + def getQuality(self) -> int: + return self._quality + + def getLatency(self) -> int: + return self._latency + + def isNetworkConnectionRequired(self) -> bool: + return self._requires_network + + def getFeatures(self) -> List[str]: + return list(self._features) + + def toString(self) -> str: + return f"Voice(name={self._name}, locale={self._locale})" + + +# ═══════════════════════════════════════════════════════════════ +# TextToSpeech.Engine +# ═══════════════════════════════════════════════════════════════ + +class TextToSpeechEngine(Stub): + """TextToSpeech.Engine — TTS 引擎元数据""" + + ACTION_CHECK_TTS_DATA = "android.speech.tts.engine.CHECK_TTS_DATA" + ACTION_INSTALL_TTS_DATA = "android.speech.tts.engine.INSTALL_TTS_DATA" + ACTION_GET_SAMPLE_TEXT = "android.speech.tts.engine.GET_SAMPLE_TEXT" + + KEY_PARAM_STREAM = "streamType" + KEY_PARAM_VOLUME = "volume" + KEY_PARAM_PAN = "pan" + KEY_PARAM_UTTERANCE_ID = "utteranceId" + KEY_PARAM_SESSION_ID = "sessionId" + + KEY_FEATURE_NETWORK_SYNTHESIS = "networkSynthesis" + KEY_FEATURE_EMBEDDED_SYNTHESIS = "embeddedSynthesis" + KEY_FEATURE_NOT_INSTALLED = "notInstalled" + + DEFAULT_STREAM = 3 + DEFAULT_LANG = "eng" + DEFAULT_COUNTRY = "USA" + DEFAULT_VARIANT = "" + + @staticmethod + def getLocaleLang(lang: str) -> str: + return lang[:3] if len(lang) >= 3 else lang + + @staticmethod + def getLocaleCountry(lang: str) -> str: + return lang[3:6] if len(lang) >= 6 else "" + + @staticmethod + def getLocaleVariant(lang: str) -> str: + return lang[6:] if len(lang) > 6 else "" + + +Engine = TextToSpeechEngine + + +# ═══════════════════════════════════════════════════════════════ +# TextToSpeech +# ═══════════════════════════════════════════════════════════════ + +class TextToSpeech(Stub): + """TextToSpeech — 文字转语音""" + + SUCCESS = 0 + ERROR = -1 + STOPPED = -2 + + QUEUE_FLUSH = 0 + QUEUE_ADD = 1 + + LANG_COUNTRY_AVAILABLE = 1 + LANG_AVAILABLE = 0 + LANG_COUNTRY_VAR_AVAILABLE = 2 + LANG_MISSING_DATA = -1 + LANG_NOT_SUPPORTED = -2 + + Engine = TextToSpeechEngine + + def __init__(self, context: Any = None, listener: Optional[TextToSpeechOnInitListener] = None): + super().__init__() + self._context = context + self._listener = listener + self._utterance_progress_listener: Optional[UtteranceProgressListener] = None + self._language: str = "" + self._country: str = "" + self._pitch: float = 1.0 + self._speech_rate: float = 1.0 + self._voice: Optional[Voice] = None + self._voices: List[Voice] = [ + Voice("en-us-1", "en_US", Voice.QUALITY_HIGH, Voice.LATENCY_NORMAL), + Voice("en-gb-1", "en_GB", Voice.QUALITY_NORMAL, Voice.LATENCY_NORMAL), + Voice("zh-cn-1", "zh_CN", Voice.QUALITY_HIGH, Voice.LATENCY_NORMAL), + ] + self._engine_package: str = "" + self._initialized = False + if listener: + try: + listener.onInit(self.SUCCESS) + except Exception: + pass + self._initialized = True + + def speak(self, text: str, queue_mode: int = QUEUE_FLUSH, + params: Any = None, utterance_id: str = "") -> int: + return self.SUCCESS + + def stop(self) -> int: + return self.SUCCESS + + def shutdown(self): + self._initialized = False + + def setLanguage(self, language: str, country: str = "", variant: str = "") -> int: + self._language = language + self._country = country + return self.LANG_AVAILABLE + + def getLanguage(self) -> Tuple[str, str, str]: + return (self._language, self._country, "") + + def isLanguageAvailable(self, language: str, country: str = "", + variant: str = "") -> int: + return self.LANG_AVAILABLE + + def setOnUtteranceProgressListener(self, listener: UtteranceProgressListener): + self._utterance_progress_listener = listener + + def setOnUtteranceCompletedListener(self, listener: TextToSpeechOnUtteranceCompletedListener): + pass + + def setPitch(self, pitch: float) -> int: + self._pitch = pitch + return self.SUCCESS + + def setSpeechRate(self, rate: float) -> int: + self._speech_rate = rate + return self.SUCCESS + + def getVoice(self) -> Optional[Voice]: + return self._voice + + def setVoice(self, voice: Voice) -> int: + self._voice = voice + return self.SUCCESS + + def getVoices(self) -> Set[Voice]: + return set(self._voices) + + def addSpeech(self, text: str, filename: str) -> int: + return self.SUCCESS + + def addEyes(self, text: str, filename: str) -> int: + return self.SUCCESS + + def synthesizeToFile(self, text: str, params: Any, filename: str) -> int: + return self.SUCCESS + + def setEngineByPackageName(self, package_name: str): + self._engine_package = package_name + + def getDefaultEngine(self) -> Optional[str]: + return "com.android.tts" + + def getEngines(self) -> List["EngineInfo"]: + return [] + + def getFeatures(self, locale: Any) -> Set[str]: + return set() + + def areDefaultsEnforced(self) -> bool: + return False + + def setQueueMode(self, queue_mode: int): + pass + + def isSpeaking(self) -> bool: + return False + + def toString(self) -> str: + return f"TextToSpeech(lang={self._language})" + + +# ═══════════════════════════════════════════════════════════════ +# EngineInfo +# ═══════════════════════════════════════════════════════════════ + +class EngineInfo(Stub): + """EngineInfo — 引擎信息""" + + def __init__(self): + super().__init__() + self.name: str = "" + self.label: str = "" + self.icon: int = 0 + self.priority: int = 0 + self.system: bool = False + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "TextToSpeech", + "TextToSpeechEngine", + "TextToSpeechOnInitListener", + "TextToSpeechOnUtteranceCompletedListener", + "SynthesisCallback", + "SynthesisRequest", + "Voice", + "UtteranceProgressListener", + "Engine", + "EngineInfo", +] \ No newline at end of file diff --git a/android_pyapi/android/telephony.py b/android_pyapi/android/telephony.py new file mode 100644 index 0000000..1ab18ce --- /dev/null +++ b/android_pyapi/android/telephony.py @@ -0,0 +1,676 @@ +""" +Android Telephony 组件 — TelephonyManager, CellInfo, PhoneStateListener 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +import random +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# CellIdentity 基类集合 +# ═══════════════════════════════════════════════════════════════ + +class CellIdentity(Stub): + def __init__(self): + super().__init__() + + +class CellIdentityGsm(CellIdentity): + def __init__(self, mcc: int = 0, mnc: int = 0, lac: int = 0, cid: int = 0, arfcn: int = 0): + super().__init__() + self._mcc = mcc + self._mnc = mnc + self._lac = lac + self._cid = cid + self._arfcn = arfcn + + def getMcc(self) -> int: return self._mcc + def getMnc(self) -> int: return self._mnc + def getLac(self) -> int: return self._lac + def getCid(self) -> int: return self._cid + def getArfcn(self) -> int: return self._arfcn + + +class CellIdentityCdma(CellIdentity): + def __init__(self, network_id: int = 0, system_id: int = 0, basestation_id: int = 0, + latitude: int = 0, longitude: int = 0): + super().__init__() + self._network_id = network_id + self._system_id = system_id + self._basestation_id = basestation_id + self._latitude = latitude + self._longitude = longitude + + def getNetworkId(self) -> int: return self._network_id + def getSystemId(self) -> int: return self._system_id + def getBasestationId(self) -> int: return self._basestation_id + def getLatitude(self) -> int: return self._latitude + def getLongitude(self) -> int: return self._longitude + + +class CellIdentityLte(CellIdentity): + def __init__(self, mcc: int = 0, mnc: int = 0, ci: int = 0, pci: int = 0, tac: int = 0): + super().__init__() + self._mcc = mcc; self._mnc = mnc + self._ci = ci; self._pci = pci; self._tac = tac + + def getMcc(self) -> int: return self._mcc + def getMnc(self) -> int: return self._mnc + def getCi(self) -> int: return self._ci + def getPci(self) -> int: return self._pci + def getTac(self) -> int: return self._tac + + +class CellIdentityWcdma(CellIdentity): + def __init__(self, mcc: int = 0, mnc: int = 0, lac: int = 0, cid: int = 0, psc: int = 0): + super().__init__() + self._mcc = mcc; self._mnc = mnc + self._lac = lac; self._cid = cid; self._psc = psc + + def getMcc(self) -> int: return self._mcc + def getMnc(self) -> int: return self._mnc + def getLac(self) -> int: return self._lac + def getCid(self) -> int: return self._cid + def getPsc(self) -> int: return self._psc + + +class CellIdentityNr(CellIdentity): + def __init__(self, mcc: int = 0, mnc: int = 0, nci: int = 0, pci: int = 0, tac: int = 0): + super().__init__() + self._mcc = mcc; self._mnc = mnc + self._nci = nci; self._pci = pci; self._tac = tac + + def getMcc(self) -> int: return self._mcc + def getMnc(self) -> int: return self._mnc + def getNci(self) -> int: return self._nci + def getPci(self) -> int: return self._pci + def getTac(self) -> int: return self._tac + + +# ═══════════════════════════════════════════════════════════════ +# CellSignalStrength 基类集合 +# ═══════════════════════════════════════════════════════════════ + +class CellSignalStrength(Stub): + SIGNAL_STRENGTH_NONE_OR_UNKNOWN = 0 + SIGNAL_STRENGTH_POOR = 1 + SIGNAL_STRENGTH_MODERATE = 2 + SIGNAL_STRENGTH_GOOD = 3 + SIGNAL_STRENGTH_GREAT = 4 + + def getLevel(self) -> int: + return self.SIGNAL_STRENGTH_NONE_OR_UNKNOWN + + def getAsuLevel(self) -> int: + return 0 + + def getDbm(self) -> int: + return 0 + + +class CellSignalStrengthGsm(CellSignalStrength): + def __init__(self, rssi: int = 0, ber: int = 0, ta: int = 0): + super().__init__() + self._rssi = rssi; self._ber = ber; self._ta = ta + + def getRssi(self) -> int: return self._rssi + def getDbm(self) -> int: return self._rssi - 113 if self._rssi > 0 else 0 + + +class CellSignalStrengthCdma(CellSignalStrength): + def __init__(self, dbm: int = 0, ecio: int = 0, evdo_dbm: int = 0, evdo_ecio: int = 0, + evdo_snr: int = 0): + super().__init__() + self._dbm = dbm; self._ecio = ecio + self._evdo_dbm = evdo_dbm; self._evdo_ecio = evdo_ecio; self._evdo_snr = evdo_snr + + def getDbm(self) -> int: return self._dbm + + +class CellSignalStrengthLte(CellSignalStrength): + def __init__(self, rssi: int = 0, rsrp: int = 0, rsrq: int = 0, rssnr: int = 0, + cqi: int = 0, ta: int = 0): + super().__init__() + self._rssi = rssi; self._rsrp = rsrp; self._rsrq = rsrq + self._rssnr = rssnr; self._cqi = cqi; self._ta = ta + + def getRsrp(self) -> int: return self._rsrp + def getRsrq(self) -> int: return self._rsrq + def getRssnr(self) -> int: return self._rssnr + def getDbm(self) -> int: return self._rsrp + + +class CellSignalStrengthWcdma(CellSignalStrength): + def __init__(self, rscp: int = 0, ecno: int = 0): + super().__init__() + self._rscp = rscp; self._ecno = ecno + + def getRscp(self) -> int: return self._rscp + def getEcno(self) -> int: return self._ecno + def getDbm(self) -> int: return self._rscp - 113 if self._rscp > 0 else 0 + + +class CellSignalStrengthNr(CellSignalStrength): + def __init__(self, ssRsrp: int = 0, ssRsrq: int = 0, ssSinr: int = 0, csiRsrp: int = 0): + super().__init__() + self._ssRsrp = ssRsrp; self._ssRsrq = ssRsrq + self._ssSinr = ssSinr; self._csiRsrp = csiRsrp + + def getSsRsrp(self) -> int: return self._ssRsrp + def getDbm(self) -> int: return self._ssRsrp + + +# ═══════════════════════════════════════════════════════════════ +# CellInfo 基类集合 +# ═══════════════════════════════════════════════════════════════ + +class CellInfo(Stub): + UNAVAILABLE = 0x7FFFFFFF + TIMESTAMP_TYPE_UNKNOWN = 0 + TIMESTAMP_TYPE_GSM = 1 + TIMESTAMP_TYPE_UMTS = 2 + TIMESTAMP_TYPE_CDMA = 3 + TIMESTAMP_TYPE_LTE = 4 + TIMESTAMP_TYPE_WCDMA = 5 + TIMESTAMP_TYPE_NR = 6 + + def __init__(self): + super().__init__() + self._registered = False + self._timestamp = 0 + self._connection_status = 0 + + def isRegistered(self) -> bool: return self._registered + def getTimeStamp(self) -> int: return self._timestamp + def getCellConnectionStatus(self) -> int: return self._connection_status + + +class CellInfoGsm(CellInfo): + def __init__(self): + super().__init__() + self._identity = CellIdentityGsm() + self._signal_strength = CellSignalStrengthGsm() + + def getCellIdentity(self) -> CellIdentityGsm: return self._identity + def getCellSignalStrength(self) -> CellSignalStrengthGsm: return self._signal_strength + + +class CellInfoCdma(CellInfo): + def __init__(self): + super().__init__() + self._identity = CellIdentityCdma() + self._signal_strength = CellSignalStrengthCdma() + + def getCellIdentity(self) -> CellIdentityCdma: return self._identity + def getCellSignalStrength(self) -> CellSignalStrengthCdma: return self._signal_strength + + +class CellInfoLte(CellInfo): + def __init__(self): + super().__init__() + self._identity = CellIdentityLte() + self._signal_strength = CellSignalStrengthLte() + + def getCellIdentity(self) -> CellIdentityLte: return self._identity + def getCellSignalStrength(self) -> CellSignalStrengthLte: return self._signal_strength + + +class CellInfoWcdma(CellInfo): + def __init__(self): + super().__init__() + self._identity = CellIdentityWcdma() + self._signal_strength = CellSignalStrengthWcdma() + + def getCellIdentity(self) -> CellIdentityWcdma: return self._identity + def getCellSignalStrength(self) -> CellSignalStrengthWcdma: return self._signal_strength + + +class CellInfoNr(CellInfo): + def __init__(self): + super().__init__() + self._identity = CellIdentityNr() + self._signal_strength = CellSignalStrengthNr() + + def getCellIdentity(self) -> CellIdentityNr: return self._identity + def getCellSignalStrength(self) -> CellSignalStrengthNr: return self._signal_strength + + +# ═══════════════════════════════════════════════════════════════ +# PhoneStateListener +# ═══════════════════════════════════════════════════════════════ + +class PhoneStateListener(Stub): + LISTEN_NONE = 0 + LISTEN_SERVICE_STATE = 1 + LISTEN_SIGNAL_STRENGTHS = 2 + LISTEN_MESSAGE_WAITING_INDICATOR = 4 + LISTEN_CALL_FORWARDING_INDICATOR = 8 + LISTEN_CELL_LOCATION = 16 + LISTEN_CALL_STATE = 32 + LISTEN_DATA_CONNECTION_STATE = 64 + LISTEN_DATA_ACTIVITY = 128 + LISTEN_SIGNAL_STRENGTH = 256 + LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH = 512 + LISTEN_CELL_INFO = 1024 + LISTEN_PRECISE_CALL_STATE = 2048 + LISTEN_PRECISE_DATA_CONNECTION_STATE = 4096 + LISTEN_DATA_CONNECTION_REAL_TIME_INFO = 8192 + LISTEN_OEM_HOOK_RAW_EVENT = 32768 + LISTEN_SRVCC_STATE_CHANGED = 65536 + LISTEN_CARRIER_NETWORK_CHANGE = 131072 + LISTEN_VOICE_ACTIVATION_STATE = 262144 + LISTEN_DATA_ACTIVATION_STATE = 524288 + LISTEN_USER_MOBILE_DATA_STATE = 1048576 + LISTEN_DISPLAY_INFO_CHANGED = 2097152 + LISTEN_PHONE_CAPABILITY_CHANGE = 4194304 + LISTEN_ACTIVE_PHONE_TYPE_CHANGE = 8388608 + LISTEN_EMERGENCY_NUMBER_LIST = 16777216 + LISTEN_EMERGENCY_NUMBER_SOURCE = 33554432 + LISTEN_CALL_ATTRIBUTES_CHANGED = 67108864 + LISTEN_BARRING_INFO_CHANGED = 134217728 + LISTEN_ALL = 2147483647 + + def __init__(self, looper: Any = None): + super().__init__() + self._looper = looper + + def onServiceStateChanged(self, service_state: "ServiceState"): + pass + + def onSignalStrengthsChanged(self, signal_strength: "SignalStrength"): + pass + + def onMessageWaitingIndicatorChanged(self, mwi: bool): + pass + + def onCallForwardingIndicatorChanged(self, cfi: bool): + pass + + def onCellLocationChanged(self, location: Any): + pass + + def onCallStateChanged(self, state: int, phone_number: str): + pass + + def onDataConnectionStateChanged(self, state: int, network_type: int = 0): + pass + + def onDataActivity(self, direction: int): + pass + + def onSignalStrengthChanged(self, asu: int): + pass + + def onCellInfoChanged(self, cell_info: List[CellInfo]): + pass + + +# ═══════════════════════════════════════════════════════════════ +# ServiceState / SignalStrength +# ═══════════════════════════════════════════════════════════════ + +class ServiceState(Stub): + STATE_IN_SERVICE = 0 + STATE_OUT_OF_SERVICE = 1 + STATE_EMERGENCY_ONLY = 2 + STATE_POWER_OFF = 3 + + def __init__(self): + super().__init__() + self._state = self.STATE_OUT_OF_SERVICE + self._roaming = False + self._operator_alpha_long: str = "" + self._operator_alpha_short: str = "" + self._operator_numeric: str = "" + self._is_manual_selection = False + self._cdma_default_roaming_indicator = -1 + self._cdma_eri_icon_index = -1 + self._cdma_eri_icon_mode = -1 + self._cdma_roaming_indicator = -1 + self._channel_number = 0 + + def getState(self) -> int: return self._state + def setState(self, state: int): self._state = state + def getRoaming(self) -> bool: return self._roaming + def setRoaming(self, roaming: bool): self._roaming = roaming + def getOperatorAlphaLong(self) -> str: return self._operator_alpha_long + def getOperatorAlphaShort(self) -> str: return self._operator_alpha_short + def getOperatorNumeric(self) -> str: return self._operator_numeric + def getIsManualSelection(self) -> bool: return self._is_manual_selection + def getCdmaDefaultRoamingIndicator(self) -> int: return self._cdma_default_roaming_indicator + def getCdmaEriIconIndex(self) -> int: return self._cdma_eri_icon_index + def getCdmaEriIconMode(self) -> int: return self._cdma_eri_icon_mode + def getCdmaRoamingIndicator(self) -> int: return self._cdma_roaming_indicator + def getChannelNumber(self) -> int: return self._channel_number + def getNetworkRegistrationInfo(self, domain: int, transport_type: int) -> Any: + return None + def toString(self) -> str: + return f"ServiceState(state={self._state}, roaming={self._roaming})" + + +class SignalStrength(Stub): + NUM_SIGNAL_STRENGTH_BINS = 5 + SIGNAL_STRENGTH_NONE_OR_UNKNOWN = 0 + SIGNAL_STRENGTH_POOR = 1 + SIGNAL_STRENGTH_MODERATE = 2 + SIGNAL_STRENGTH_GOOD = 3 + SIGNAL_STRENGTH_GREAT = 4 + + def __init__(self): + super().__init__() + self._gsm_signal_strength = 0 + self._cdma_dbm = 0 + self._cdma_ecio = 0 + self._evdo_dbm = 0 + self._evdo_ecio = 0 + self._evdo_snr = 0 + self._lte_signal_strength = 0 + self._td_scdma_rscp = 0 + + def getGsmSignalStrength(self) -> int: return self._gsm_signal_strength + def getCdmaDbm(self) -> int: return self._cdma_dbm + def getCdmaEcio(self) -> int: return self._cdma_ecio + def getEvdoDbm(self) -> int: return self._evdo_dbm + def getEvdoEcio(self) -> int: return self._evdo_ecio + def getEvdoSnr(self) -> int: return self._evdo_snr + def getLteSignalStrength(self) -> int: return self._lte_signal_strength + def getLevel(self) -> int: return self.SIGNAL_STRENGTH_NONE_OR_UNKNOWN + def getAsuLevel(self) -> int: return 0 + def getDbm(self) -> int: return 0 + + +# ═══════════════════════════════════════════════════════════════ +# SmsManager +# ═══════════════════════════════════════════════════════════════ + +class SmsManager(Stub): + RESULT_ERROR_GENERIC_FAILURE = 1 + RESULT_ERROR_RADIO_OFF = 2 + RESULT_ERROR_NULL_PDU = 3 + RESULT_ERROR_NO_SERVICE = 4 + RESULT_ERROR_LIMIT_EXCEEDED = 5 + RESULT_ERROR_FDN_CHECK_FAILURE = 6 + RESULT_ERROR_SHORT_CODE_NOT_ALLOWED = 7 + RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED = 8 + RESULT_RIL_GENERIC_FAILURE = 9 + RESULT_RIL_RADIO_NOT_AVAILABLE = 10 + STATUS_ON_ICC_FREE = 0 + STATUS_ON_ICC_READ = 1 + STATUS_ON_ICC_UNREAD = 3 + STATUS_ON_ICC_SENT = 5 + STATUS_ON_ICC_UNSENT = 7 + + _instance: Optional["SmsManager"] = None + + def __init__(self): + super().__init__() + + @staticmethod + def getDefault() -> "SmsManager": + if SmsManager._instance is None: + SmsManager._instance = SmsManager() + return SmsManager._instance + + def sendTextMessage(self, dest_address: str, sc_address: Optional[str], + text: str, sent_intent: Any, delivery_intent: Any): + pass + + def sendMultipartTextMessage(self, dest_address: str, sc_address: Optional[str], + parts: List[str], sent_intents: Optional[List[Any]], + delivery_intents: Optional[List[Any]]): + pass + + def sendDataMessage(self, dest_address: str, sc_address: Optional[str], + dest_port: int, data: bytes, sent_intent: Any, + delivery_intent: Any): + pass + + def sendMultimediaMessage(self, context: Any, content_uri: str, + location_url: Optional[str], + config_overrides: Optional[Any], + sent_intent: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# PhoneNumberUtils +# ═══════════════════════════════════════════════════════════════ + +class PhoneNumberUtils(Stub): + + FORMAT_JAPAN = 2 + FORMAT_NANP = 1 + FORMAT_UNKNOWN = 0 + + @staticmethod + def formatNumber(source: str) -> str: + return source + + @staticmethod + def isGlobalPhoneNumber(phone_number: str) -> bool: + return bool(phone_number and len(phone_number) >= 7) + + @staticmethod + def compare(a: str, b: str) -> bool: + return a.replace("-", "").replace(" ", "") == b.replace("-", "").replace(" ", "") + + @staticmethod + def formatNumberToE164(phone_number: str, default_country_iso: str) -> Optional[str]: + return phone_number + + @staticmethod + def formatNumberToRFC3966(phone_number: str, default_country_iso: str) -> Optional[str]: + return phone_number + + @staticmethod + def isEmergencyNumber(phone_number: str) -> bool: + return phone_number in ("911", "112", "999", "000", "110", "119", "120", "122") + + @staticmethod + def isVoiceMailNumber(phone_number: str) -> bool: + return False + + @staticmethod + def convertKeypadLettersToDigits(input_: str) -> str: + mapping = {"A": "2", "B": "2", "C": "2", "D": "3", "E": "3", + "F": "3", "G": "4", "H": "4", "I": "4", "J": "5", + "K": "5", "L": "5", "M": "6", "N": "6", "O": "6", + "P": "7", "Q": "7", "R": "7", "S": "7", "T": "8", + "U": "8", "V": "8", "W": "9", "X": "9", "Y": "9", "Z": "9"} + result = "" + for c in input_.upper(): + result += mapping.get(c, c) + return result + + @staticmethod + def toCallerIDMinMatch(phone_number: str) -> str: + return phone_number[-7:] if len(phone_number) >= 7 else phone_number + + +# ═══════════════════════════════════════════════════════════════ +# TelephonyManager +# ═══════════════════════════════════════════════════════════════ + +class TelephonyManager(Stub): + """TelephonyManager — 电话管理器""" + + PHONE_TYPE_NONE = 0 + PHONE_TYPE_GSM = 1 + PHONE_TYPE_CDMA = 2 + PHONE_TYPE_SIP = 3 + + NETWORK_TYPE_UNKNOWN = 0 + NETWORK_TYPE_GPRS = 1 + NETWORK_TYPE_EDGE = 2 + NETWORK_TYPE_UMTS = 3 + NETWORK_TYPE_CDMA = 4 + NETWORK_TYPE_EVDO_0 = 5 + NETWORK_TYPE_EVDO_A = 6 + NETWORK_TYPE_1xRTT = 7 + NETWORK_TYPE_HSDPA = 8 + NETWORK_TYPE_HSUPA = 9 + NETWORK_TYPE_HSPA = 10 + NETWORK_TYPE_IDEN = 11 + NETWORK_TYPE_EVDO_B = 12 + NETWORK_TYPE_LTE = 13 + NETWORK_TYPE_EHRPD = 14 + NETWORK_TYPE_HSPAP = 15 + NETWORK_TYPE_GSM = 16 + NETWORK_TYPE_TD_SCDMA = 17 + NETWORK_TYPE_IWLAN = 18 + NETWORK_TYPE_LTE_CA = 19 + NETWORK_TYPE_NR = 20 + + SIM_STATE_UNKNOWN = 0 + SIM_STATE_ABSENT = 1 + SIM_STATE_PIN_REQUIRED = 2 + SIM_STATE_PUK_REQUIRED = 3 + SIM_STATE_NETWORK_LOCKED = 4 + SIM_STATE_READY = 5 + SIM_STATE_NOT_READY = 6 + SIM_STATE_PERM_DISABLED = 7 + + DATA_DISCONNECTED = 0 + DATA_CONNECTING = 1 + DATA_CONNECTED = 2 + DATA_SUSPENDED = 3 + + CALL_STATE_IDLE = 0 + CALL_STATE_RINGING = 1 + CALL_STATE_OFFHOOK = 2 + + def __init__(self, context: Any = None): + super().__init__() + self._context = context + + def getNetworkOperatorName(self) -> str: + return "MockOperator" + + def getNetworkOperator(self) -> str: + return "310260" + + def getNetworkCountryIso(self) -> str: + return "us" + + def getNetworkType(self) -> int: + return self.NETWORK_TYPE_LTE + + def getPhoneType(self) -> int: + return self.PHONE_TYPE_GSM + + def getDataNetworkType(self) -> int: + return self.NETWORK_TYPE_LTE + + def getVoiceNetworkType(self) -> int: + return self.NETWORK_TYPE_LTE + + def getSimOperator(self) -> str: + return "310260" + + def getSimOperatorName(self) -> str: + return "MockSIM" + + def getSimCountryIso(self) -> str: + return "us" + + def getSimSerialNumber(self) -> Optional[str]: + return "89014103211118510720" + + def getSubscriberId(self) -> Optional[str]: + return "310260123456789" + + def getDeviceId(self) -> Optional[str]: + return "000000000000000" + + def getDeviceSoftwareVersion(self) -> Optional[str]: + return "00.00.00.00" + + def getLine1Number(self) -> Optional[str]: + return "15551234567" + + def getGroupIdLevel1(self) -> Optional[str]: + return "" + + def getIccAuthentication(self, app_type: int, aid: str, data: bytes) -> Optional[bytes]: + return None + + def getVoiceMailNumber(self) -> Optional[str]: + return "15551234567" + + def getVoiceMailAlphaTag(self) -> str: + return "Voicemail" + + def isNetworkRoaming(self) -> bool: + return False + + def hasIccCard(self) -> bool: + return True + + def getSimState(self) -> int: + return self.SIM_STATE_READY + + def getCellLocation(self) -> Any: + return None + + def getAllCellInfo(self) -> Optional[List[CellInfo]]: + return [] + + def getMmsUserAgent(self) -> str: + return "Android-Mms/1.0" + + def getMmsUAProfUrl(self) -> str: + return "http://www.example.com/uaprof.xml" + + def listen(self, listener: PhoneStateListener, events: int): + pass + + def getDataState(self) -> int: + return self.DATA_CONNECTED + + def getDataActivity(self) -> int: + return 0 # DATA_ACTIVITY_NONE + + def getCallState(self) -> int: + return self.CALL_STATE_IDLE + + def toString(self) -> str: + return "TelephonyManager()" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "TelephonyManager", + "CellInfo", + "CellInfoGsm", + "CellInfoCdma", + "CellInfoLte", + "CellInfoWcdma", + "CellInfoNr", + "CellIdentityGsm", + "CellIdentityCdma", + "CellIdentityLte", + "CellIdentityWcdma", + "CellIdentityNr", + "CellSignalStrengthGsm", + "CellSignalStrengthCdma", + "CellSignalStrengthLte", + "CellSignalStrengthWcdma", + "CellSignalStrengthNr", + "PhoneStateListener", + "PhoneNumberUtils", + "ServiceState", + "SignalStrength", + "SmsManager", +] \ No newline at end of file diff --git a/android_pyapi/android/text.py b/android_pyapi/android/text.py new file mode 100644 index 0000000..0b1405f --- /dev/null +++ b/android_pyapi/android/text.py @@ -0,0 +1,1106 @@ +""" +Android Text 组件 — TextUtils, SpannableString, Html, Layout 等 +方法签名完全对齐 Android SDK +""" + +import html as _html +import re as _re +import sys +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, +) + + +# ═══════════════════════════════════════════════════════════════ +# Typeface +# ═══════════════════════════════════════════════════════════════ + +class Typeface(Stub): + """Typeface — 字体""" + + NORMAL = 0 + BOLD = 1 + ITALIC = 2 + BOLD_ITALIC = 3 + + DEFAULT: "Typeface" = None + DEFAULT_BOLD: "Typeface" = None + MONOSPACE: "Typeface" = None + SANS_SERIF: "Typeface" = None + SERIF: "Typeface" = None + + def __init__(self, family_name: str = "", style: int = NORMAL): + super().__init__() + self._family_name = family_name + self._style = style + self._weight = 400 if style in (self.NORMAL,) else 700 + + @staticmethod + def defaultFromStyle(style: int) -> "Typeface": + tf = Typeface("sans-serif", style) + return tf + + @staticmethod + def create(family_name: str, style: int) -> "Typeface": + return Typeface(family_name, style) + + @staticmethod + def createFromFile(path: str) -> "Typeface": + return Typeface("custom") + + def getStyle(self) -> int: + return self._style + + def getWeight(self) -> int: + return self._weight + + def isBold(self) -> bool: + return self._style in (self.BOLD, self.BOLD_ITALIC) + + def isItalic(self) -> bool: + return self._style in (self.ITALIC, self.BOLD_ITALIC) + + def toString(self) -> str: + return f"Typeface({self._family_name}, style={self._style})" + + +# Initialize Typeface constants +Typeface.DEFAULT = Typeface("sans-serif", Typeface.NORMAL) +Typeface.DEFAULT_BOLD = Typeface("sans-serif", Typeface.BOLD) +Typeface.MONOSPACE = Typeface("monospace", Typeface.NORMAL) +Typeface.SANS_SERIF = Typeface("sans-serif", Typeface.NORMAL) +Typeface.SERIF = Typeface("serif", Typeface.NORMAL) + + +# ═══════════════════════════════════════════════════════════════ +# TextPaint +# ═══════════════════════════════════════════════════════════════ + +class TextPaint(Stub): + """TextPaint — 文本绘制画笔""" + + def __init__(self, flags: int = 0): + super().__init__() + self.bgColor = 0 + self.bgColorSet = False + self.linkColor = 0xFF0000FF # blue + self.underlineWidth = 0.0 + self.baselineShift = 0 + self.bulletRadius = 0.0 + self.bulletGap = 0 + self.bulletColor = 0 + self.color = 0xFF000000 + self.textSize = 14.0 + self.typeface: Optional[Typeface] = Typeface.DEFAULT + self.flags = flags + self.antiAlias = True + self.style = Typeface.NORMAL + self.strokeWidth = 0.0 + self.strokeCap = "butt" + self.strokeJoin = "miter" + self.shadowDx = 0.0 + self.shadowDy = 0.0 + self.shadowRadius = 0.0 + self.shadowColor = 0 + self.elevation = 0.0 + self.letterSpacing = 0.0 + self.fontFeatureSettings = "" + self.fontVariationSettings = "" + + def setUnderlineText(self, underline: bool): + if underline: + self.underlineWidth = 1.0 + else: + self.underlineWidth = 0.0 + + def getColor(self) -> int: + return self.color + + def setColor(self, color: int): + self.color = color + + def setARGB(self, a: int, r: int, g: int, b: int): + self.color = ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF) + + def getTextSize(self) -> float: + return self.textSize + + def setTextSize(self, size: float): + self.textSize = size + + def getTypeface(self) -> Optional[Typeface]: + return self.typeface + + def setTypeface(self, tf: Typeface): + self.typeface = tf + + def setAntiAlias(self, aa: bool): + self.antiAlias = aa + + def measureText(self, text: str) -> float: + return len(text) * self.textSize * 0.5 + + def toString(self) -> str: + return f"TextPaint(size={self.textSize}, color={self.color})" + + +# ═══════════════════════════════════════════════════════════════ +# TextUtils +# ═══════════════════════════════════════════════════════════════ + +class TextUtils(Stub): + """TextUtils — 文本工具""" + + CAP_MODE_SENTENCES = 0 + CAP_MODE_WORDS = 1 + CAP_MODE_CHARACTERS = 2 + + @staticmethod + def isEmpty(str_: Optional[str]) -> bool: + return str_ is None or len(str_) == 0 + + @staticmethod + def isBlank(str_: Optional[str]) -> bool: + return str_ is None or str_.strip() == "" + + @staticmethod + def equals(a: Optional[str], b: Optional[str]) -> bool: + return a == b + + @staticmethod + def join(delimiter: str, tokens: Sequence[str]) -> str: + return delimiter.join(tokens) + + @staticmethod + def split(text: str, delimiter: str) -> List[str]: + return text.split(delimiter) + + @staticmethod + def htmlEncode(s: str) -> str: + return _html.escape(s, quote=True) + + @staticmethod + def htmlDecode(s: str) -> str: + return _html.unescape(s) + + @staticmethod + def getReverse(s: str, start: int, end: int) -> str: + return s[start:end][::-1] + + @staticmethod + def substring(text: str, start: int, end: int) -> str: + return text[start:end] + + @staticmethod + def delimitedListToStringArray(text: str, delimiter: str, delimiter2: str = "") -> List[str]: + if not text: + return [] + return [t.strip() for t in text.split(delimiter)] + + @staticmethod + def commaEllipsize(text: str, max_width: int, paint: Any, + one_origin: bool = False, ellipsize: bool = False) -> str: + if len(text) <= max_width: + return text + return text[:max_width] + "..." + + @staticmethod + def ellipsize(text: str, paint: Any, avail: float, + where: Any, ellipsis: str = "...") -> str: + if paint and hasattr(paint, 'measureText'): + text_width = paint.measureText(text) + if text_width <= avail: + return text + max_chars = int(avail / 7) if avail else len(text) // 2 + if max_chars >= len(text): + return text + if str(where) == "END": + return text[:max_chars] + ellipsis + elif str(where) == "START": + return ellipsis + text[-max_chars:] + elif str(where) == "MIDDLE": + half = max_chars // 2 + return text[:half] + ellipsis + text[-(max_chars - half):] + return text[:max_chars] + ellipsis + + @staticmethod + def isDigitsOnly(s: str) -> bool: + return s.isdigit() if s else False + + @staticmethod + def isGraphical(s: str) -> bool: + return bool(s and s.isprintable()) + + @staticmethod + def capitalize(s: str) -> str: + return s.capitalize() + + @staticmethod + def trim(s: str) -> str: + return s.strip() + + @staticmethod + def getTrimmedLength(s: str) -> int: + return len(s.strip()) + + @staticmethod + def stringOrEmpty(s: Optional[str]) -> str: + return s or "" + + @staticmethod + def copySpansFrom(source: Any, start: int, end: int, kind: type, + dest: Any, dest_offset: int = 0): + pass + + @staticmethod + def removeEmptySpans(spans: List, source: Any, kind: type) -> List: + return [s for s in spans if s is not None] + + @staticmethod + def expandTemplate(template: str, values: List[str]) -> str: + result = template + for i, v in enumerate(values): + result = result.replace("$" + str(i + 1), v) + return result + + +# ═══════════════════════════════════════════════════════════════ +# Span 相关接口和基类 +# ═══════════════════════════════════════════════════════════════ + +class Spanned(Stub): + """Spanned 接口 — 带标记的字符串""" + + SPAN_POINT_MARK = 0 + SPAN_MARK_MARK = 1 + SPAN_POINT_POINT = 2 + SPAN_MARK_POINT = 3 + SPAN_PARAGRAPH = 3 + SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_POINT + SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_MARK + SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_POINT + SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_MARK + SPAN_COMPOSING = 256 + SPAN_USER = 0x01000000 + SPAN_USER_SHIFT = 24 + SPAN_PRIORITY = 0x0000FF00 + SPAN_PRIORITY_SHIFT = 8 + SPAN_INTERMEDIATE = 0x00100000 + + def __init__(self, source: str = ""): + super().__init__() + self._source = source + self._spans: Dict[Tuple[type, int], Tuple[int, int, Any]] = {} # (type, id) -> (start, end, span_obj) + + def setSpan(self, span_obj: Any, start: int, end: int, flags: int): + span_id = id(span_obj) + span_type = type(span_obj) + self._spans[(span_type, span_id)] = (start, end, span_obj) + + def removeSpan(self, span_obj: Any): + span_id = id(span_obj) + span_type = type(span_obj) + self._spans.pop((span_type, span_id), None) + + def getSpans(self, start: int, end: int, span_type: Optional[type] = None) -> List[Any]: + result = [] + for (st, sid), (s, e, obj) in self._spans.items(): + if span_type is None or st == span_type: + if s < end and e > start: + result.append(obj) + return result + + def getSpanStart(self, span_obj: Any) -> int: + span_id = id(span_obj) + span_type = type(span_obj) + data = self._spans.get((span_type, span_id)) + return data[0] if data else -1 + + def getSpanEnd(self, span_obj: Any) -> int: + span_id = id(span_obj) + span_type = type(span_obj) + data = self._spans.get((span_type, span_id)) + return data[1] if data else -1 + + def getSpanFlags(self, span_obj: Any) -> int: + span_id = id(span_obj) + span_type = type(span_obj) + data = self._spans.get((span_type, span_id)) + return 0 if data is None else 0 + + def nextSpanTransition(self, start: int, end: int, span_type: Optional[type] = None) -> int: + return end + + def toString(self) -> str: + return self._source + + def __len__(self) -> int: + return len(self._source) + + def __getitem__(self, index): + return self._source[index] + + def __str__(self): + return self._source + + def length(self) -> int: + return len(self._source) + + +class Editable(Spanned): + """Editable — 可编辑文本接口""" + + def __init__(self, source: str = ""): + super().__init__(source) + + def append(self, text: Any) -> "Editable": + self._source += str(text) + return self + + def insert(self, where: int, text: Any): + self._source = self._source[:where] + str(text) + self._source[where:] + + def replace(self, start: int, end: int, text: Any): + self._source = self._source[:start] + str(text) + self._source[end:] + + def delete(self, start: int, end: int): + self._source = self._source[:start] + self._source[end:] + + def clear(self): + self._source = "" + + def clearSpans(self): + self._spans.clear() + + def getChars(self, start: int, end: int, dest: List[str], destoff: int): + for i, c in enumerate(self._source[start:end]): + if destoff + i < len(dest): + dest[destoff + i] = c + + +# ═══════════════════════════════════════════════════════════════ +# SpannableString +# ═══════════════════════════════════════════════════════════════ + +class SpannableString(Spanned): + """SpannableString — 不可变但可标记的字符串""" + + def __init__(self, source: str): + super().__init__(source) + self._source = source + + def setSpan(self, span_obj: Any, start: int, end: int, flags: int): + super().setSpan(span_obj, start, end, flags) + + def removeSpan(self, span_obj: Any): + super().removeSpan(span_obj) + + def toString(self) -> str: + return self._source + + def __str__(self): + return self._source + + @staticmethod + def valueOf(source: str) -> "SpannableString": + return SpannableString(source) + + def length(self) -> int: + return len(self._source) + + def charAt(self, index: int) -> str: + return self._source[index] + + def subSequence(self, start: int, end: int) -> str: + return self._source[start:end] + + +# ═══════════════════════════════════════════════════════════════ +# SpannableStringBuilder +# ═══════════════════════════════════════════════════════════════ + +class SpannableStringBuilder(Editable): + """SpannableStringBuilder — 可变可标记字符串构建器""" + + def __init__(self, source: Any = None): + if source is None: + super().__init__("") + elif isinstance(source, str): + super().__init__(source) + elif isinstance(source, Spanned): + super().__init__(source.toString()) + else: + super().__init__(str(source)) + + def append(self, text: Any) -> "SpannableStringBuilder": + super().append(text) + return self + + def append(self, text: Any, start: int, end: int) -> "SpannableStringBuilder": + if isinstance(text, str): + self._source += text[start:end] + else: + self._source += str(text)[start:end] + return self + + def insert(self, where: int, text: Any) -> "SpannableStringBuilder": + super().insert(where, text) + return self + + def replace(self, start: int, end: int, text: Any) -> "SpannableStringBuilder": + super().replace(start, end, text) + return self + + def delete(self, start: int, end: int) -> "SpannableStringBuilder": + super().delete(start, end) + return self + + def setSpan(self, span_obj: Any, start: int, end: int, flags: int): + super().setSpan(span_obj, start, end, flags) + + def clear(self): + super().clear() + + def clearSpans(self): + super().clearSpans() + + def length(self) -> int: + return len(self._source) + + def charAt(self, index: int) -> str: + return self._source[index] if 0 <= index < len(self._source) else "" + + def subSequence(self, start: int, end: int) -> str: + return self._source[start:end] + + def toString(self) -> str: + return self._source + + def __str__(self): + return self._source + + def __iadd__(self, other: Any) -> "SpannableStringBuilder": + self.append(str(other)) + return self + + +# ═══════════════════════════════════════════════════════════════ +# TextWatcher +# ═══════════════════════════════════════════════════════════════ + +class TextWatcher(Stub): + """TextWatcher — 文本变化监听器""" + + def beforeTextChanged(self, s: str, start: int, count: int, after: int): + pass + + def onTextChanged(self, s: str, start: int, before: int, count: int): + pass + + def afterTextChanged(self, s: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# InputFilter / LengthFilter +# ═══════════════════════════════════════════════════════════════ + +class InputFilter(Stub): + """InputFilter — 输入过滤器接口""" + + class LengthFilter(Stub): + """LengthFilter — 长度限制过滤器""" + + def __init__(self, max_length: int): + super().__init__() + self._max_length = max_length + + def filter(self, source: str, start: int, end: int, dest: Spanned, + dstart: int, dend: int) -> Optional[str]: + keep = self._max_length - (len(dest.toString()) - (dend - dstart)) + if keep <= 0: + return "" + if keep >= (end - start): + return None # keep original + return source[start:start + keep] + + def getMax(self) -> int: + return self._max_length + + @staticmethod + def allCaps() -> "InputFilter": + f = InputFilter() + f._transform = lambda s: s.upper() + return f + + def filter(self, source: str, start: int, end: int, dest: Spanned, + dstart: int, dend: int) -> Optional[str]: + return None + + +# ═══════════════════════════════════════════════════════════════ +# Html +# ═══════════════════════════════════════════════════════════════ + +class Html(Stub): + """Html — HTML 文本处理""" + + FROM_HTML_MODE_COMPACT = 0 + FROM_HTML_MODE_LEGACY = 1 + FROM_HTML_OPTION_USE_CSS_COLORS = 256 + FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE = 0x00000001 + FROM_HTML_SEPARATOR_LINE_BREAK_DIV = 0x00000002 + FROM_HTML_SEPARATOR_LINE_BREAK_HEADING = 0x00000004 + FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM = 0x00000008 + FROM_HTML_SEPARATOR_LINE_BREAK_LIST = 0x00000020 + FROM_HTML_SEPARATOR_LINE_BREAK_TABLE = 0x00000040 + TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001 + TO_HTML_PARAGRAPH_LINE_CONSECUTIVE = 0x00000002 + + _TAG_RE = _re.compile(r"<[^>]*>") + _BR_RE = _re.compile(r"", _re.IGNORECASE) + _P_RE = _re.compile(r"

|]*>", _re.IGNORECASE) + + @staticmethod + def fromHtml(html_text: str, flags: int = 0) -> str: + """将 HTML 转为纯文本""" + text = html_text + text = Html._BR_RE.sub("\n", text) + text = Html._P_RE.sub("\n\n", text) + text = Html._TAG_RE.sub("", text) + text = _html.unescape(text) + return text.strip() + + @staticmethod + def toHtml(text: str, option: int = 0) -> str: + """将纯文本转为 HTML""" + paragraphs = text.split("\n") + html_parts = [] + for p in paragraphs: + escaped = _html.escape(p) + html_parts.append(f"

{escaped}

") + return "".join(html_parts) + + @staticmethod + def fromHtml(html_text: str, image_getter: Any, tag_handler: Any) -> str: + return Html.fromHtml(html_text) + + @staticmethod + def toHtmlWithOptions(text: str, options: int) -> str: + return Html.toHtml(text) + + class ImageGetter: + def getDrawable(self, source: str): + return None + + class TagHandler: + def handleTag(self, opening: bool, tag: str, output: Editable, xml_reader: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# CharacterStyle / 各 Span 实现 +# ═══════════════════════════════════════════════════════════════ + +class CharacterStyle(Stub): + """CharacterStyle — 字符样式基类""" + + def underLine(self): + pass + + def updateDrawState(self, tp: TextPaint): + pass + + def getUnderline(self) -> bool: + return False + + @staticmethod + def wrap(cs: "CharacterStyle") -> "CharacterStyle": + return cs + + +class StyleSpan(CharacterStyle): + """StyleSpan — 字体样式""" + + def __init__(self, style: int): + super().__init__() + self._style = style + + def getStyle(self) -> int: + return self._style + + def updateDrawState(self, tp: TextPaint): + tp.style = self._style + + +class TypefaceSpan(CharacterStyle): + """TypefaceSpan — 字体族""" + + def __init__(self, family: str): + super().__init__() + self._family = family + + def getFamily(self) -> str: + return self._family + + def updateDrawState(self, tp: TextPaint): + if self._family == "monospace": + tp.typeface = Typeface.MONOSPACE + elif self._family == "serif": + tp.typeface = Typeface.SERIF + else: + tp.typeface = Typeface.DEFAULT + + +class RelativeSizeSpan(CharacterStyle): + """RelativeSizeSpan — 相对大小""" + + def __init__(self, size: float): + super().__init__() + self._size = size + + def getSizeChange(self) -> float: + return self._size + + def updateDrawState(self, tp: TextPaint): + tp.textSize *= self._size + + +class ForegroundColorSpan(CharacterStyle): + """ForegroundColorSpan — 前景色""" + + def __init__(self, color: int): + super().__init__() + self._color = color + + def getForegroundColor(self) -> int: + return self._color + + def updateDrawState(self, tp: TextPaint): + tp.color = self._color + + +class BackgroundColorSpan(CharacterStyle): + """BackgroundColorSpan — 背景色""" + + def __init__(self, color: int): + super().__init__() + self._color = color + + def getBackgroundColor(self) -> int: + return self._color + + def updateDrawState(self, tp: TextPaint): + tp.bgColor = self._color + tp.bgColorSet = True + + +class UnderlineSpan(CharacterStyle): + """UnderlineSpan — 下划线""" + + def updateDrawState(self, tp: TextPaint): + tp.setUnderlineText(True) + + +class StrikethroughSpan(CharacterStyle): + """StrikethroughSpan — 删除线""" + + def updateDrawState(self, tp: TextPaint): + pass # desktop 无直接等价 + + +class SubscriptSpan(CharacterStyle): + """SubscriptSpan — 下标""" + + def updateDrawState(self, tp: TextPaint): + tp.baselineShift = -tp.textSize / 4 + + +class SuperscriptSpan(CharacterStyle): + """SuperscriptSpan — 上标""" + + def updateDrawState(self, tp: TextPaint): + tp.baselineShift = tp.textSize / 2 + + +class ImageSpan(CharacterStyle): + """ImageSpan — 图片""" + + ALIGN_BOTTOM = 0 + ALIGN_BASELINE = 1 + ALIGN_CENTER = 2 + ALIGN_TOP = 3 + + def __init__(self, drawable: Any, source: str = "", align: int = ALIGN_BOTTOM): + super().__init__() + self._drawable = drawable + self._source = source + self._align = align + + def getDrawable(self): + return self._drawable + + def getSource(self) -> str: + return self._source + + def getAlignment(self) -> int: + return self._align + + +class ClickableSpan(CharacterStyle): + """ClickableSpan — 可点击文本""" + + def __init__(self): + super().__init__() + + def onClick(self, view: Any): + pass + + def updateDrawState(self, tp: TextPaint): + tp.color = tp.linkColor + tp.setUnderlineText(True) + + +class URLSpan(ClickableSpan): + """URLSpan — URL 链接""" + + def __init__(self, url: str): + super().__init__() + self._url = url + + def getURL(self) -> str: + return self._url + + def onClick(self, view: Any): + sys.stderr.write(f"[URLSpan] onClick: {self._url}\n") + + def updateDrawState(self, tp: TextPaint): + super().updateDrawState(tp) + + +# ═══════════════════════════════════════════════════════════════ +# NoCopySpan / ParagraphStyle 接口 +# ═══════════════════════════════════════════════════════════════ + +class NoCopySpan(Stub): + """NoCopySpan — 标记不在拷贝时复制的 span""" + pass + +class NoCopySpanConcise(NoCopySpan): + """NoCopySpan.Concrete — 具体实现""" + pass + + +class ParagraphStyle(Stub): + """ParagraphStyle — 段落样式接口""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# Ellipsize / TruncateAt 枚举 +# ═══════════════════════════════════════════════════════════════ + +class Ellipsize(Stub): + """Ellipsize 常量""" + END = "END" + MIDDLE = "MIDDLE" + MARQUEE = "MARQUEE" + START = "START" + NONE = "NONE" + + +class TruncateAt(Stub): + """TruncateAt 常量""" + END = "END" + MIDDLE = "MIDDLE" + MARQUEE = "MARQUEE" + START = "START" + + +# ═══════════════════════════════════════════════════════════════ +# Align +# ═══════════════════════════════════════════════════════════════ + +class Align(Stub): + """Layout.Alignment 枚举""" + ALIGN_NORMAL = 0 + ALIGN_OPPOSITE = 1 + ALIGN_CENTER = 2 + + +# ═══════════════════════════════════════════════════════════════ +# TextDirectionHeuristic / TextDirectionHeuristics +# ═══════════════════════════════════════════════════════════════ + +class TextDirectionHeuristic(Stub): + """TextDirectionHeuristic — 文本方向启发式""" + pass + + +class TextDirectionHeuristics(Stub): + """TextDirectionHeuristics — 文本方向启发式常量""" + + FIRSTSTRONG_LTR = TextDirectionHeuristic() + FIRSTSTRONG_RTL = TextDirectionHeuristic() + ANYRTL_LTR = TextDirectionHeuristic() + LTR = TextDirectionHeuristic() + RTL = TextDirectionHeuristic() + LOCALE = TextDirectionHeuristic() + + +# ═══════════════════════════════════════════════════════════════ +# LoginFilter +# ═══════════════════════════════════════════════════════════════ + +class LoginFilter(InputFilter): + """LoginFilter — 登录名输入过滤器""" + + def __init__(self, append_ok: bool = True): + super().__init__() + self._append_ok = append_ok + + def isAllowed(self, c: str) -> bool: + return c.isalnum() or c in "@._-" + + def filter(self, source: str, start: int, end: int, dest: Spanned, + dstart: int, dend: int) -> Optional[str]: + filtered = [] + for c in source[start:end]: + if self.isAllowed(c): + filtered.append(c) + elif self._append_ok: + pass + result = "".join(filtered) + return result if result != source else None + + class UsernameFilterGMail(InputFilter): + def filter(self, source, start, end, dest, dstart, dend): + result = [] + for c in source[start:end]: + if c.isalnum() or c in "@._-": + result.append(c) + return "".join(result) or None + + class PasswordFilterGMail(InputFilter): + def filter(self, source, start, end, dest, dstart, dend): + return None + + +# ═══════════════════════════════════════════════════════════════ +# Method / MovementMethod +# ═══════════════════════════════════════════════════════════════ + +class Method(Stub): + """Method — 输入法接口""" + pass + + +class MovementMethod(Stub): + """MovementMethod — 移动方式接口""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# Layout / StaticLayout / DynamicLayout / BoringLayout +# ═══════════════════════════════════════════════════════════════ + +class Layout(Stub): + """Layout — 布局基类""" + + Alignment = Align + + BREAK_STRATEGY_SIMPLE = 0 + BREAK_STRATEGY_HIGH_QUALITY = 1 + BREAK_STRATEGY_BALANCED = 2 + + HYPHENATION_FREQUENCY_NONE = 0 + HYPHENATION_FREQUENCY_NORMAL = 1 + HYPHENATION_FREQUENCY_FULL = 2 + + JUSTIFICATION_MODE_NONE = 0 + JUSTIFICATION_MODE_INTER_WORD = 1 + + def __init__(self, text: str, paint: TextPaint, width: int, alignment: int, + spacing_mult: float, spacing_add: float): + super().__init__() + self._text = text + self._paint = paint + self._width = width + self._alignment = alignment + self._spacing_mult = spacing_mult + self._spacing_add = spacing_add + self._line_count = max(1, len(text) // max(1, width // 10) + 1) + + def getLineCount(self) -> int: + return self._line_count + + def getLineBounds(self, line: int, bounds: Optional[Any]) -> int: + line_height = int(self._paint.textSize * self._spacing_mult + self._spacing_add) + return line * line_height + + def getPrimaryHorizontal(self, offset: int) -> float: + line = 0 + return (offset % max(1, self._width)) * self._paint.measureText("x") + + def getSecondaryHorizontal(self, offset: int) -> float: + return 0.0 + + def getLineTop(self, line: int) -> int: + line_height = int(self._paint.textSize * self._spacing_mult + self._spacing_add) + return line * line_height + + def getLineBottom(self, line: int) -> int: + line_height = int(self._paint.textSize * self._spacing_mult + self._spacing_add) + return (line + 1) * line_height + + def getLineStart(self, line: int) -> int: + chars_per_line = max(1, self._width // max(1, int(self._paint.measureText("x")))) + return line * chars_per_line + + def getLineEnd(self, line: int) -> int: + chars_per_line = max(1, self._width // max(1, int(self._paint.measureText("x")))) + return min((line + 1) * chars_per_line, len(self._text)) + + def getText(self) -> str: + return self._text + + def getPaint(self) -> TextPaint: + return self._paint + + def getWidth(self) -> int: + return self._width + + def getAlignment(self) -> int: + return self._alignment + + def getSpacingMultiplier(self) -> float: + return self._spacing_mult + + def getSpacingAdd(self) -> float: + return self._spacing_add + + def isRtlCharAt(self, offset: int) -> bool: + return False + + def getParagraphDirection(self, line: int) -> int: + return 1 + + def draw(self, canvas: Any): + pass + + +class StaticLayout(Layout): + """StaticLayout — 静态布局""" + + def __init__(self, source: str, paint: TextPaint, width: int, alignment: int, + spacing_mult: float, spacing_add: float, include_pad: bool): + super().__init__(source, paint, width, alignment, spacing_mult, spacing_add) + self._include_pad = include_pad + + +class DynamicLayout(Layout): + """DynamicLayout — 动态布局""" + + def __init__(self, base: Any, paint: TextPaint, width: int, alignment: int, + spacing_mult: float, spacing_add: float, include_pad: bool, + ellipsize: Any = None, ellipsize_width: int = 0): + super().__init__(str(base), paint, width, alignment, spacing_mult, spacing_add) + self._include_pad = include_pad + + +class BoringLayout(Layout): + """BoringLayout — 单行布局""" + + def __init__(self, source: str, paint: TextPaint, width: int, alignment: int, + spacing_mult: float, spacing_add: float, include_pad: bool, + ellipsize: Any = None, ellipsize_width: int = 0): + super().__init__(source, paint, width, alignment, spacing_mult, spacing_add) + self._include_pad = include_pad + self._line_count = 1 if source else 0 + + class Metrics(Stub): + """Metrics for BoringLayout""" + width = 0 + height = 0 + + @staticmethod + def isBoring(text: str, paint: TextPaint) -> Optional["BoringLayout.Metrics"]: + m = BoringLayout.Metrics() + m.width = int(paint.measureText(text)) + m.height = int(paint.textSize) + return m if text else None + + @staticmethod + def make(source: str, paint: TextPaint, width: int, alignment: int, + spacing_mult: float, spacing_add: float, metrics: "BoringLayout.Metrics", + include_pad: bool) -> "BoringLayout": + return BoringLayout(source, paint, width, alignment, spacing_mult, spacing_add, include_pad) + + +class ClipRectangle(Stub): + """ClipRectangle — 裁剪矩形""" + def __init__(self, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0): + super().__init__() + self.left = left + self.top = top + self.right = right + self.bottom = bottom + + +# ═══════════════════════════════════════════════════════════════ +# Spannable (接口别名) +# ═══════════════════════════════════════════════════════════════ + +class Spannable(Spanned): + """Spannable — 可标记接口""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "TextUtils", + "SpannableString", + "SpannableStringBuilder", + "Spannable", + "Editable", + "Spanned", + "TextWatcher", + "InputFilter", + "Html", + "Layout", + "StaticLayout", + "DynamicLayout", + "BoringLayout", + "ClipRectangle", + "TextPaint", + "TextDirectionHeuristic", + "TextDirectionHeuristics", + "Align", + "Ellipsize", + "TruncateAt", + "LoginFilter", + "Method", + "MovementMethod", + "NoCopySpan", + "ParagraphStyle", + "CharacterStyle", + "StyleSpan", + "TypefaceSpan", + "RelativeSizeSpan", + "ForegroundColorSpan", + "BackgroundColorSpan", + "UnderlineSpan", + "StrikethroughSpan", + "SubscriptSpan", + "SuperscriptSpan", + "ImageSpan", + "ClickableSpan", + "URLSpan", + "Typeface", +] \ No newline at end of file diff --git a/android_pyapi/android/util.py b/android_pyapi/android/util.py new file mode 100644 index 0000000..37b5e32 --- /dev/null +++ b/android_pyapi/android/util.py @@ -0,0 +1,1055 @@ +""" +Android Util 组件 — Log, SparseArray, LruCache, DisplayMetrics 等 +方法签名完全对齐 Android SDK +""" + +import math +import re as _re +import sys +from datetime import datetime +from typing import Any, Callable, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, +) + + +# ═══════════════════════════════════════════════════════════════ +# Log +# ═══════════════════════════════════════════════════════════════ + +class Log(Stub): + """Log — 日志输出""" + + ASSERT = 7 + DEBUG = 3 + ERROR = 6 + INFO = 4 + VERBOSE = 2 + WARN = 5 + + _min_log_level = VERBOSE + + @staticmethod + def v(tag: str, msg: str) -> int: + if Log._min_log_level <= Log.VERBOSE: + sys.stderr.write(f"V/{tag}: {msg}\n") + return 0 + + @staticmethod + def d(tag: str, msg: str) -> int: + if Log._min_log_level <= Log.DEBUG: + sys.stderr.write(f"D/{tag}: {msg}\n") + return 0 + + @staticmethod + def i(tag: str, msg: str) -> int: + if Log._min_log_level <= Log.INFO: + sys.stderr.write(f"I/{tag}: {msg}\n") + return 0 + + @staticmethod + def w(tag: str, msg: str) -> int: + if Log._min_log_level <= Log.WARN: + sys.stderr.write(f"W/{tag}: {msg}\n") + return 0 + + @staticmethod + def e(tag: str, msg: str) -> int: + if Log._min_log_level <= Log.ERROR: + sys.stderr.write(f"E/{tag}: {msg}\n") + return 0 + + @staticmethod + def wtf(tag: str, msg: str) -> int: + sys.stderr.write(f"WTF/{tag}: {msg}\n") + return 0 + + @staticmethod + def println(log_level: int, tag: str, msg: str) -> int: + level_map = { + Log.VERBOSE: "V", + Log.DEBUG: "D", + Log.INFO: "I", + Log.WARN: "W", + Log.ERROR: "E", + Log.ASSERT: "A", + } + prefix = level_map.get(log_level, "?") + sys.stderr.write(f"{prefix}/{tag}: {msg}\n") + return 0 + + @staticmethod + def isLoggable(tag: str, level: int) -> bool: + return level >= Log._min_log_level + + @staticmethod + def getStackTraceString(tr: Exception) -> str: + import traceback + return "".join(traceback.format_exception(type(tr), tr, tr.__traceback__)) + + @staticmethod + def setMinLogLevel(level: int): + Log._min_log_level = level + + @staticmethod + def wtf(tag: str, tr: Exception) -> int: + sys.stderr.write(f"WTF/{tag}: {Log.getStackTraceString(tr)}\n") + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# SparseArray +# ═══════════════════════════════════════════════════════════════ + +T = TypeVar("T") + +class SparseArray(Generic[T], Stub): + """SparseArray — 稀疏数组(int 键映射)""" + + def __init__(self, initial_capacity: int = 10): + super().__init__() + self._keys: List[int] = [] + self._values: List[Optional[T]] = [] + self._size = 0 + + def put(self, key: int, value: T): + """如果 key 已存在则替换""" + try: + idx = self._keys.index(key) + self._values[idx] = value + except ValueError: + self._keys.append(key) + self._values.append(value) + self._size += 1 + + def get(self, key: int, default: Optional[T] = None) -> Optional[T]: + try: + idx = self._keys.index(key) + return self._values[idx] + except ValueError: + return default + + def delete(self, key: int): + try: + idx = self._keys.index(key) + self._keys.pop(idx) + self._values.pop(idx) + self._size -= 1 + except ValueError: + pass + + def remove(self, key: int): + self.delete(key) + + def removeAt(self, index: int): + if 0 <= index < self._size: + self._keys.pop(index) + self._values.pop(index) + self._size -= 1 + + def size(self) -> int: + return self._size + + def keyAt(self, index: int) -> int: + if 0 <= index < self._size: + return self._keys[index] + return 0 + + def valueAt(self, index: int) -> Optional[T]: + if 0 <= index < self._size: + return self._values[index] + return None + + def setValueAt(self, index: int, value: T): + if 0 <= index < self._size: + self._values[index] = value + + def indexOfKey(self, key: int) -> int: + try: + return self._keys.index(key) + except ValueError: + return -1 + + def indexOfValue(self, value: T) -> int: + try: + return self._values.index(value) + except ValueError: + return -1 + + def clear(self): + self._keys.clear() + self._values.clear() + self._size = 0 + + def append(self, key: int, value: T): + self.put(key, value) + + def containsKey(self, key: int) -> bool: + return key in self._keys + + def putAll(self, other: "SparseArray[T]"): + for i in range(other.size()): + self.put(other.keyAt(i), other.valueAt(i)) + + def clone(self) -> "SparseArray[T]": + sa = SparseArray[T]() + sa._keys = list(self._keys) + sa._values = list(self._values) + sa._size = self._size + return sa + + def toString(self) -> str: + pairs = ", ".join( + f"{k}={v}" for k, v in zip(self._keys, self._values) + ) + return f"SparseArray{{{pairs}}}" + + def __getitem__(self, key: int) -> Optional[T]: + return self.get(key) + + def __setitem__(self, key: int, value: T): + self.put(key, value) + + def __delitem__(self, key: int): + self.delete(key) + + def __len__(self) -> int: + return self._size + + def __contains__(self, key: int) -> bool: + return key in self._keys + + +class SparseIntArray(Stub): + """SparseIntArray — int 到 int 的映射""" + + def __init__(self, initial_capacity: int = 10): + super().__init__() + self._keys: List[int] = [] + self._values: List[int] = [] + + def put(self, key: int, value: int): + try: + idx = self._keys.index(key) + self._values[idx] = value + except ValueError: + self._keys.append(key) + self._values.append(value) + + def get(self, key: int, default: int = 0) -> int: + try: + idx = self._keys.index(key) + return self._values[idx] + except ValueError: + return default + + def delete(self, key: int): + try: + idx = self._keys.index(key) + self._keys.pop(idx) + self._values.pop(idx) + except ValueError: + pass + + def removeAt(self, index: int): + if 0 <= index < len(self._keys): + self._keys.pop(index) + self._values.pop(index) + + def size(self) -> int: + return len(self._keys) + + def keyAt(self, index: int) -> int: + if 0 <= index < len(self._keys): + return self._keys[index] + return 0 + + def valueAt(self, index: int) -> int: + if 0 <= index < len(self._values): + return self._values[index] + return 0 + + def indexOfKey(self, key: int) -> int: + try: + return self._keys.index(key) + except ValueError: + return -1 + + def indexOfValue(self, value: int) -> int: + try: + return self._values.index(value) + except ValueError: + return -1 + + def clear(self): + self._keys.clear() + self._values.clear() + + def append(self, key: int, value: int): + self.put(key, value) + + def toString(self) -> str: + pairs = ", ".join(f"{k}={v}" for k, v in zip(self._keys, self._values)) + return f"SparseIntArray{{{pairs}}}" + + +class SparseBooleanArray(Stub): + """SparseBooleanArray — int 到 bool 的映射""" + + def __init__(self, initial_capacity: int = 10): + super().__init__() + self._keys: List[int] = [] + self._values: List[bool] = [] + + def put(self, key: int, value: bool): + try: + idx = self._keys.index(key) + self._values[idx] = value + except ValueError: + self._keys.append(key) + self._values.append(value) + + def get(self, key: int, default: bool = False) -> bool: + try: + idx = self._keys.index(key) + return self._values[idx] + except ValueError: + return default + + def delete(self, key: int): + try: + idx = self._keys.index(key) + self._keys.pop(idx) + self._values.pop(idx) + except ValueError: + pass + + def size(self) -> int: + return len(self._keys) + + def keyAt(self, index: int) -> int: + if 0 <= index < len(self._keys): + return self._keys[index] + return 0 + + def valueAt(self, index: int) -> bool: + if 0 <= index < len(self._values): + return self._values[index] + return False + + def indexOfKey(self, key: int) -> int: + try: + return self._keys.index(key) + except ValueError: + return -1 + + def indexOfValue(self, value: bool) -> int: + try: + return self._values.index(value) + except ValueError: + return -1 + + def clear(self): + self._keys.clear() + self._values.clear() + + def append(self, key: int, value: bool): + self.put(key, value) + + def toString(self) -> str: + pairs = ", ".join(f"{k}={v}" for k, v in zip(self._keys, self._values)) + return f"SparseBooleanArray{{{pairs}}}" + + +# ═══════════════════════════════════════════════════════════════ +# LruCache +# ═══════════════════════════════════════════════════════════════ + +class LruCache(Stub): + """LruCache — LRU 缓存""" + + def __init__(self, max_size: int): + super().__init__() + self._max_size = max_size + self._size = 0 + self._map: Dict[Any, Any] = {} + + def get(self, key: Any) -> Optional[Any]: + return self._map.get(key) + + def put(self, key: Any, value: Any) -> Optional[Any]: + old = self._map.get(key) + self._map[key] = value + self._size += self._safe_size_of(key, value) + if old is not None: + self._size -= self._safe_size_of(key, old) + self._trim_to_size(self._max_size) + return old + + def remove(self, key: Any) -> Optional[Any]: + value = self._map.pop(key, None) + if value is not None: + self._size -= self._safe_size_of(key, value) + return value + + def size(self) -> int: + return self._size + + def maxSize(self) -> int: + return self._max_size + + def trimToSize(self, max_size: int): + self._trim_to_size(max_size) + + def _trim_to_size(self, max_size: int): + while self._size > max_size and self._map: + # Remove oldest (first key) + oldest_key = next(iter(self._map)) + self.remove(oldest_key) + + def snapshot(self) -> Dict[Any, Any]: + return dict(self._map) + + def evictAll(self): + self._map.clear() + self._size = 0 + + def _safe_size_of(self, key: Any, value: Any) -> int: + try: + return self.sizeOf(key, value) + except Exception: + return 1 + + def sizeOf(self, key: Any, value: Any) -> int: + return 1 + + def entryRemoved(self, evicted: bool, key: Any, old_value: Any, new_value: Optional[Any]): + pass + + def create(self, key: Any) -> Optional[Any]: + return None + + def getCount(self) -> int: + return len(self._map) + + def putCount(self) -> int: + return len(self._map) + + def evictionCount(self) -> int: + return 0 + + def hitCount(self) -> int: + return 0 + + def missCount(self) -> int: + return 0 + + def toString(self) -> str: + return f"LruCache(maxSize={self._max_size}, size={self._size})" + + +# ═══════════════════════════════════════════════════════════════ +# DisplayMetrics +# ═══════════════════════════════════════════════════════════════ + +class DisplayMetrics(Stub): + """DisplayMetrics — 屏幕显示度量""" + + DENSITY_LOW = 120 + DENSITY_MEDIUM = 160 + DENSITY_TV = 213 + DENSITY_HIGH = 240 + DENSITY_XHIGH = 320 + DENSITY_XXHIGH = 480 + DENSITY_XXXHIGH = 640 + DENSITY_DEFAULT = DENSITY_MEDIUM + DENSITY_DEVICE_STABLE = DENSITY_XXXHIGH + + def __init__(self): + super().__init__() + self.widthPixels = 1080 + self.heightPixels = 1920 + self.density = 2.75 + self.densityDpi = 440 + self.scaledDensity = 2.75 + self.xdpi = 440.0 + self.ydpi = 440.0 + self.noncompatWidthPixels = 1080 + self.noncompatHeightPixels = 1920 + self.noncompatDensity = 2.75 + self.noncompatDensityDpi = 440 + self.noncompatScaledDensity = 2.75 + self.noncompatXdpi = 440.0 + self.noncompatYdpi = 440.0 + + def setTo(self, other: "DisplayMetrics"): + self.widthPixels = other.widthPixels + self.heightPixels = other.heightPixels + self.density = other.density + self.densityDpi = other.densityDpi + self.scaledDensity = other.scaledDensity + self.xdpi = other.xdpi + self.ydpi = other.ydpi + + def setToDefaults(self): + self.__init__() + + def toString(self) -> str: + return (f"DisplayMetrics({self.widthPixels}x{self.heightPixels}, " + f"density={self.density}, dpi={self.densityDpi})") + + +# ═══════════════════════════════════════════════════════════════ +# TypedValue +# ═══════════════════════════════════════════════════════════════ + +class TypedValue(Stub): + """TypedValue — 类型化值的容器""" + + COMPLEX_UNIT_PX = 0 + COMPLEX_UNIT_DIP = 1 + COMPLEX_UNIT_SP = 2 + COMPLEX_UNIT_PT = 3 + COMPLEX_UNIT_IN = 4 + COMPLEX_UNIT_MM = 5 + COMPLEX_UNIT_FRACTION = 6 + COMPLEX_UNIT_FRACTION_PARENT = 7 + + TYPE_NULL = 0x00 + TYPE_REFERENCE = 0x01 + TYPE_ATTRIBUTE = 0x02 + TYPE_STRING = 0x03 + TYPE_FLOAT = 0x04 + TYPE_DIMENSION = 0x05 + TYPE_FRACTION = 0x06 + TYPE_FIRST_INT = 0x10 + TYPE_INT_DEC = 0x10 + TYPE_INT_HEX = 0x11 + TYPE_INT_BOOLEAN = 0x12 + TYPE_INT_COLOR_ARGB8 = 0x1C + TYPE_INT_COLOR_RGB8 = 0x1D + TYPE_INT_COLOR_ARGB4 = 0x1E + TYPE_INT_COLOR_RGB4 = 0x1F + TYPE_LAST_COLOR_INT = 0x1F + TYPE_LAST_INT = 0x1F + + def __init__(self): + super().__init__() + self.type = self.TYPE_NULL + self.data = 0 + self.assetCookie = 0 + self.resourceId = 0 + self.changingConfigurations = 0 + self.string = None + self.density = 0 + + @staticmethod + def applyDimension(unit: int, value: float, metrics: DisplayMetrics) -> float: + if unit == TypedValue.COMPLEX_UNIT_PX: + return value + if unit == TypedValue.COMPLEX_UNIT_DIP: + return value * metrics.density + if unit == TypedValue.COMPLEX_UNIT_SP: + return value * metrics.scaledDensity + if unit == TypedValue.COMPLEX_UNIT_PT: + return value * metrics.xdpi * (1.0 / 72) + if unit == TypedValue.COMPLEX_UNIT_IN: + return value * metrics.xdpi + if unit == TypedValue.COMPLEX_UNIT_MM: + return value * metrics.xdpi * (1.0 / 25.4) + return value + + @staticmethod + def complexToDimension(data: int, metrics: DisplayMetrics) -> float: + unit = data & 0xF + value = (data >> 4) & 0xFFFFFF + # Sign-extend + if value & 0x800000: + value = value - 0x1000000 + return TypedValue.applyDimension(unit, float(value), metrics) + + @staticmethod + def complexToFloat(data: int) -> float: + value = (data & 0xFFFFFF00) >> 8 + exp = (data & 0xFF) - 150 + return float(value) * (10.0 ** exp) + + @staticmethod + def complexToDimensionPixelOffset(data: int, metrics: DisplayMetrics) -> int: + return int(TypedValue.complexToDimension(data, metrics)) + + @staticmethod + def complexToDimensionPixelSize(data: int, metrics: DisplayMetrics) -> int: + val = TypedValue.complexToDimension(data, metrics) + if val >= 0: + return int(val + 0.5) + return int(val - 0.5) + + def getFloat(self) -> float: + return float(self.data) + + def toString(self) -> str: + return f"TypedValue(type={self.type}, data={self.data})" + + +# ═══════════════════════════════════════════════════════════════ +# Pair +# ═══════════════════════════════════════════════════════════════ + +class Pair(Stub): + """Pair — 有序对""" + + def __init__(self, first: Any, second: Any): + super().__init__() + self.first = first + self.second = second + + @staticmethod + def create(a: Any, b: Any) -> "Pair": + return Pair(a, b) + + def hashCode(self) -> int: + return hash((self.first, self.second)) + + def equals(self, other: Any) -> bool: + if not isinstance(other, Pair): + return False + return self.first == other.first and self.second == other.second + + def toString(self) -> str: + return f"Pair({self.first}, {self.second})" + + def __eq__(self, other): + return self.equals(other) + + def __hash__(self): + return self.hashCode() + + def __repr__(self): + return self.toString() + + def __iter__(self): + return iter((self.first, self.second)) + + +# ═══════════════════════════════════════════════════════════════ +# Size / SizeF +# ═══════════════════════════════════════════════════════════════ + +class Size(Stub): + """Size — 整数尺寸""" + + def __init__(self, width: int, height: int): + super().__init__() + self._width = width + self._height = height + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def equals(self, other: Any) -> bool: + if not isinstance(other, Size): + return False + return self._width == other._width and self._height == other._height + + def hashCode(self) -> int: + return (self._height << 16) ^ self._width + + def toString(self) -> str: + return f"{self._width}x{self._height}" + + @staticmethod + def parseSize(string: str) -> "Size": + parts = string.split("x") + if len(parts) == 2: + return Size(int(parts[0]), int(parts[1])) + raise ValueError(f"Invalid size: {string}") + + def __eq__(self, other): + return self.equals(other) + + def __hash__(self): + return self.hashCode() + + def __repr__(self): + return self.toString() + + +class SizeF(Stub): + """SizeF — 浮点数尺寸""" + + def __init__(self, width: float, height: float): + super().__init__() + self._width = width + self._height = height + + def getWidth(self) -> float: + return self._width + + def getHeight(self) -> float: + return self._height + + def equals(self, other: Any) -> bool: + if not isinstance(other, SizeF): + return False + return abs(self._width - other._width) < 0.001 and abs(self._height - other._height) < 0.001 + + def hashCode(self) -> int: + return hash((self._width, self._height)) + + def toString(self) -> str: + return f"{self._width:.1f}x{self._height:.1f}" + + @staticmethod + def parseSizeF(string: str) -> "SizeF": + parts = string.split("x") + if len(parts) == 2: + return SizeF(float(parts[0]), float(parts[1])) + raise ValueError(f"Invalid size: {string}") + + def __eq__(self, other): + return self.equals(other) + + def __hash__(self): + return self.hashCode() + + def __repr__(self): + return self.toString() + + +# ═══════════════════════════════════════════════════════════════ +# StateSet +# ═══════════════════════════════════════════════════════════════ + +class StateSet(Stub): + """StateSet — 状态集合""" + + VIEW_STATE_WINDOW_FOCUSED = 1 + VIEW_STATE_SELECTED = 1 << 1 + VIEW_STATE_FOCUSED = 1 << 2 + VIEW_STATE_ENABLED = 1 << 3 + VIEW_STATE_PRESSED = 1 << 4 + VIEW_STATE_ACTIVATED = 1 << 5 + VIEW_STATE_ACCELERATED = 1 << 6 + VIEW_STATE_HOVERED = 1 << 7 + VIEW_STATE_DRAG_CAN_ACCEPT = 1 << 8 + VIEW_STATE_DRAG_HOVERED = 1 << 9 + + WILD_CARD = 0x00000000 + + @staticmethod + def isWildCard(state_spec_or_mask: Any) -> bool: + if isinstance(state_spec_or_mask, list): + return len(state_spec_or_mask) == 0 + return state_spec_or_mask == StateSet.WILD_CARD + + @staticmethod + def trimStateSet(states: List[int], new_size: int) -> List[int]: + return states[:new_size] + + @staticmethod + def dump(states: List[int]) -> str: + if not states: + return "[]" + return "[" + ", ".join(str(s) for s in states) + "]" + + +# ═══════════════════════════════════════════════════════════════ +# MathUtils +# ═══════════════════════════════════════════════════════════════ + +class MathUtils(Stub): + """MathUtils — 数学工具""" + + DEG_TO_RAD = math.pi / 180.0 + RAD_TO_DEG = 180.0 / math.pi + + @staticmethod + def abs(v: float) -> float: + return abs(v) + + @staticmethod + def constrain(amount: float, low: float, high: float) -> float: + return max(low, min(high, amount)) + + @staticmethod + def constrainInt(amount: int, low: int, high: int) -> int: + return max(low, min(high, amount)) + + @staticmethod + def lerp(start: float, stop: float, amount: float) -> float: + return start + (stop - start) * amount + + @staticmethod + def lerpInv(a: float, b: float, value: float) -> float: + if a != b: + return (value - a) / (b - a) + return 0.0 + + @staticmethod + def norm(start: float, stop: float, value: float) -> float: + return MathUtils.lerpInv(start, stop, value) + + @staticmethod + def map(min_start: float, min_stop: float, max_start: float, max_stop: float, value: float) -> float: + return max_start + (value - min_start) * (max_stop - max_start) / (min_stop - min_start) + + @staticmethod + def dist(x1: float, y1: float, x2: float, y2: float) -> float: + return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + + @staticmethod + def max(var1: float, var2: float) -> float: + return max(var1, var2) + + @staticmethod + def min(var1: float, var2: float) -> float: + return min(var1, var2) + + @staticmethod + def degrees(radians: float) -> float: + return math.degrees(radians) + + @staticmethod + def radians(degrees: float) -> float: + return math.radians(degrees) + + @staticmethod + def exp(a: float) -> float: + return math.exp(a) + + @staticmethod + def log(a: float) -> float: + return math.log(a) + + @staticmethod + def pow(a: float, b: float) -> float: + return a ** b + + @staticmethod + def sqrt(a: float) -> float: + return math.sqrt(a) + + @staticmethod + def sin(angle: float) -> float: + return math.sin(angle) + + @staticmethod + def cos(angle: float) -> float: + return math.cos(angle) + + @staticmethod + def tan(angle: float) -> float: + return math.tan(angle) + + @staticmethod + def atan2(a: float, b: float) -> float: + return math.atan2(a, b) + + @staticmethod + def random() -> float: + import random + return random.random() + + @staticmethod + def isNaN(v: float) -> bool: + return math.isnan(v) + + @staticmethod + def nanToZero(value: float) -> float: + return 0.0 if math.isnan(value) else value + + +# ═══════════════════════════════════════════════════════════════ +# Pattern (re) +# ═══════════════════════════════════════════════════════════════ + +class Pattern(Stub): + """Pattern — 正则表达式模式""" + + CASE_INSENSITIVE = 2 + MULTILINE = 8 + DOTALL = 32 + UNICODE_CASE = 64 + CANON_EQ = 128 + UNIX_LINES = 1 + LITERAL = 16 + COMMENTS = 4 + + def __init__(self, pattern: str, flags: int = 0): + super().__init__() + self._pattern_str = pattern + self._flags = flags + re_flags = 0 + if flags & self.CASE_INSENSITIVE: + re_flags |= _re.IGNORECASE + if flags & self.MULTILINE: + re_flags |= _re.MULTILINE + if flags & self.DOTALL: + re_flags |= _re.DOTALL + if flags & self.UNICODE_CASE: + re_flags |= _re.UNICODE + if flags & self.UNIX_LINES: + pass # no re equivalent + self._compiled = _re.compile(pattern, re_flags) + + @staticmethod + def compile(pattern: str, flags: int = 0) -> "Pattern": + return Pattern(pattern, flags) + + def matcher(self, input_str: str) -> "Matcher": + return Matcher(self._compiled, input_str) + + def split(self, input_str: str, limit: int = 0) -> List[str]: + if limit < 0: + return self._compiled.split(input_str, maxsplit=0) + return self._compiled.split(input_str, maxsplit=limit - 1 if limit > 0 else 0) + + def pattern(self) -> str: + return self._pattern_str + + def flags(self) -> int: + return self._flags + + @staticmethod + def matches(regex: str, input_str: str) -> bool: + return bool(_re.fullmatch(regex, input_str)) + + @staticmethod + def quote(s: str) -> str: + return _re.escape(s) + + def toString(self) -> str: + return self._pattern_str + + +class Matcher(Stub): + """Matcher — 正则匹配器""" + + def __init__(self, compiled_pattern: "Pattern", input_str: str): + super().__init__() + self._pattern = compiled_pattern + self._input = input_str + self._region_start = 0 + self._region_end = len(input_str) + self._match: Optional[Any] = None + + def matches(self) -> bool: + self._match = self._pattern.fullmatch(self._input, + self._region_start, self._region_end) + return self._match is not None + + def find(self, start: int = 0) -> bool: + self._match = self._pattern.search(self._input, start or self._region_start, + self._region_end) + return self._match is not None + + def lookingAt(self) -> bool: + self._match = self._pattern.match(self._input, self._region_start, + self._region_end) + return self._match is not None + + def group(self, group: int = 0) -> Optional[str]: + if self._match: + return self._match.group(group) + return None + + def groupCount(self) -> int: + if self._match: + return len(self._match.groups()) + return 0 + + def start(self, group: int = 0) -> int: + if self._match: + return self._match.start(group) + return -1 + + def end(self, group: int = 0) -> int: + if self._match: + return self._match.end(group) + return -1 + + def replaceAll(self, replacement: str) -> str: + return self._pattern.sub(replacement, self._input) + + def replaceFirst(self, replacement: str) -> str: + return self._pattern.sub(replacement, self._input, count=1) + + def region(self, start: int, end: int) -> "Matcher": + self._region_start = start + self._region_end = end + return self + + def regionStart(self) -> int: + return self._region_start + + def regionEnd(self) -> int: + return self._region_end + + def reset(self, input_str: Optional[str] = None) -> "Matcher": + if input_str is not None: + self._input = input_str + self._match = None + return self + + def hitEnd(self) -> bool: + return False + + def requireEnd(self) -> bool: + return False + + def usePattern(self, new_pattern: "Pattern") -> "Matcher": + self._pattern = new_pattern + return self + + def appendReplacement(self, sb: Any, replacement: str): + pass + + def appendTail(self, sb: Any): + pass + + def toString(self) -> str: + return f"Matcher({self._pattern.pattern()})" + + +# ═══════════════════════════════════════════════════════════════ +# TYPED_VALUE 常量 +# ═══════════════════════════════════════════════════════════════ + +TYPED_VALUE = TypedValue # 别名 + + +# ═══════════════════════════════════════════════════════════════ +# Pair 反向兼容 (android.util.Pair) +# ═══════════════════════════════════════════════════════════════ + +# Pair 已在上面定义 + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Log", + "SparseArray", + "SparseIntArray", + "SparseBooleanArray", + "LruCache", + "DisplayMetrics", + "TypedValue", + "Pair", + "Size", + "SizeF", + "StateSet", + "MathUtils", + "Pattern", + "Matcher", + "TYPED_VALUE", +] \ No newline at end of file diff --git a/android_pyapi/android/view.py b/android_pyapi/android/view.py new file mode 100644 index 0000000..42df7fd --- /dev/null +++ b/android_pyapi/android/view.py @@ -0,0 +1,3801 @@ +""" +Android View 组件 — View, ViewGroup, MotionEvent, KeyEvent, GestureDetector 等 +方法签名完全对齐 Android SDK +""" + +import math +import time +import weakref +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +from .._core import Stub, AndroidOnlyError, android_context, HAS_PYAUTOGUI + + +# ═══════════════════════════════════════════════════════════════ +# Gravity +# ═══════════════════════════════════════════════════════════════ + +class Gravity: + """Gravity — 布局重力常量""" + NO_GRAVITY = 0x0000 + TOP = 0x0030 + BOTTOM = 0x0050 + LEFT = 0x0003 + RIGHT = 0x0005 + CENTER = 0x0011 + CENTER_HORIZONTAL = 0x0001 + CENTER_VERTICAL = 0x0010 + FILL = 0x0077 + FILL_HORIZONTAL = 0x0007 + FILL_VERTICAL = 0x0070 + CLIP_HORIZONTAL = 0x0008 + CLIP_VERTICAL = 0x0080 + START = 0x00800001 + END = 0x00800005 + RELATIVE_LAYOUT_DIRECTION = 0x00800000 + + @staticmethod + def getAbsoluteGravity(gravity: int, layout_direction: int) -> int: + from ..os import View + if layout_direction == View.LAYOUT_DIRECTION_RTL: + if gravity & Gravity.START: + gravity = (gravity & ~Gravity.START) | Gravity.RIGHT + if gravity & Gravity.END: + gravity = (gravity & ~Gravity.END) | Gravity.LEFT + else: + if gravity & Gravity.START: + gravity = (gravity & ~Gravity.START) | Gravity.LEFT + if gravity & Gravity.END: + gravity = (gravity & ~Gravity.END) | Gravity.RIGHT + return gravity + + @staticmethod + def apply(gravity: int, w: int, h: int, container: "Rect") -> int: + return 0 + + +Rect = None + + +def _set_rect_class(): + global Rect + if Rect is None: + class _Rect: + def __init__(self, l=0, t=0, r=0, b=0): + self.left = l + self.top = t + self.right = r + self.bottom = b + def width(self): return self.right - self.left + def height(self): return self.bottom - self.top + def set(self, l, t, r, b): + self.left, self.top, self.right, self.bottom = l, t, r, b + def isEmpty(self): return self.width() <= 0 or self.height() <= 0 + def toString(self): return f"Rect({self.left}, {self.top} - {self.right}, {self.bottom})" + Rect = _Rect + + +_set_rect_class() + + +# ═══════════════════════════════════════════════════════════════ +# View +# ═══════════════════════════════════════════════════════════════ + +class View(Stub): + """View — 所有 UI 组件的基类""" + + # 可见性常量 + VISIBLE = 0 + INVISIBLE = 4 + GONE = 8 + + # 测量常量 + MEASURED_SIZE_STATE_MASK = 0xFF000000 + MEASURED_STATE_MASK = 0xFF000000 + MEASURED_HEIGHT_STATE_SHIFT = 16 + MEASURED_WIDTH_STATE_SHIFT = 0 + + # 测量模式 + EXACTLY = 0x40000000 + AT_MOST = 0x80000000 # -2147483648 + UNSPECIFIED = 0 + + # 滚动轴 + SCROLL_AXIS_NONE = 0 + SCROLL_AXIS_HORIZONTAL = 1 + SCROLL_AXIS_VERTICAL = 2 + + # 布局方向 + LAYOUT_DIRECTION_LTR = 0 + LAYOUT_DIRECTION_RTL = 1 + + # 绘制缓存 + DRAWING_CACHE_QUALITY_LOW = 0x00040000 + DRAWING_CACHE_QUALITY_HIGH = 0x00080000 + DRAWING_CACHE_QUALITY_AUTO = 0 + + # 焦点方向 + FOCUS_BACKWARD = 0x00000001 + FOCUS_FORWARD = 0x00000002 + FOCUS_LEFT = 0x00000011 + FOCUS_UP = 0x00000021 + FOCUS_RIGHT = 0x00000042 + FOCUS_DOWN = 0x00000082 + + # Keep screen on + KEEP_SCREEN_ON = 0x02000000 + + # 状态 + STATUS_BAR_HIDDEN = 0x00000001 + STATUS_BAR_VISIBLE = 0x00000000 + NAVIGATION_BAR_HIDDEN = 0x00000002 + NAVIGATION_BAR_VISIBLE = 0x00000000 + + # 系统UI可见性 + SYSTEM_UI_FLAG_VISIBLE = 0 + SYSTEM_UI_FLAG_LOW_PROFILE = 0x00000001 + SYSTEM_UI_FLAG_HIDE_NAVIGATION = 0x00000002 + SYSTEM_UI_FLAG_FULLSCREEN = 0x00000004 + SYSTEM_UI_FLAG_LAYOUT_STABLE = 0x00000100 + SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 0x00000200 + SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400 + SYSTEM_UI_FLAG_IMMERSIVE = 0x00000800 + SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000 + + _next_id = 0x10000000 + _id_lock = None + + def __init__(self, context=None): + super().__init__() + if context is None: + context = android_context + self._context = context + self._id = View.NO_ID + self._tag: Any = None + self._layout_params: Optional["ViewGroup.LayoutParams"] = None + self._visibility = View.VISIBLE + self._enabled = True + self._clickable = False + self._focusable = False + self._selected = False + self._activated = False + self._x: float = 0.0 + self._y: float = 0.0 + self._translation_x: float = 0.0 + self._translation_y: float = 0.0 + self._rotation: float = 0.0 + self._scale_x: float = 1.0 + self._scale_y: float = 1.0 + self._alpha: float = 1.0 + self._pivot_x: float = 0.0 + self._pivot_y: float = 0.0 + self._measured_width: int = 0 + self._measured_height: int = 0 + self._left: int = 0 + self._top: int = 0 + self._right: int = 0 + self._bottom: int = 0 + self._padding_left: int = 0 + self._padding_top: int = 0 + self._padding_right: int = 0 + self._padding_bottom: int = 0 + self._parent: Optional["ViewGroup"] = None + self._background_color: Optional[int] = None + self._background_drawable: Any = None + self._background_resource: int = 0 + self._on_click_listener: Optional[Callable] = None + self._on_long_click_listener: Optional[Callable] = None + self._on_touch_listener: Optional[Callable] = None + self._on_key_listener: Optional[Callable] = None + self._callbacks: Dict[Any, Tuple[Callable, float]] = {} + self._scroll_x: int = 0 + self._scroll_y: int = 0 + self._will_not_draw = False + self._layer_type = View.LAYER_TYPE_NONE + self._layout_direction = View.LAYOUT_DIRECTION_LTR + self._min_width: int = 0 + self._min_height: int = 0 + self._elevation: float = 0.0 + self._clickable = False + self._long_clickable = False + self._context_clickable = False + self._children: List["View"] = [] + + # ─── 常量 ────────────────────────────────────────────────── + + NO_ID = -1 + DRAWING_CACHE_QUALITY_LOW = 0x00040000 + DRAWING_CACHE_QUALITY_HIGH = 0x00080000 + DRAWING_CACHE_QUALITY_AUTO = 0 + LAYER_TYPE_NONE = 0 + LAYER_TYPE_SOFTWARE = 1 + LAYER_TYPE_HARDWARE = 2 + FADING_EDGE_NONE = 0 + FADING_EDGE_HORIZONTAL = 0x00001000 + FADING_EDGE_VERTICAL = 0x00002000 + OVER_SCROLL_ALWAYS = 0 + OVER_SCROLL_IF_CONTENT_SCROLLS = 1 + OVER_SCROLL_NEVER = 2 + + # ─── id / tag ────────────────────────────────────────────── + + def setId(self, id_: int): + self._id = id_ + + def getId(self) -> int: + return self._id + + def setTag(self, tag: Any): + self._tag = tag + + def getTag(self) -> Any: + return self._tag + + # ─── layout params ───────────────────────────────────────── + + def setLayoutParams(self, params: "ViewGroup.LayoutParams"): + self._layout_params = params + + def getLayoutParams(self) -> Optional["ViewGroup.LayoutParams"]: + return self._layout_params + + # ─── visibility ──────────────────────────────────────────── + + def setVisibility(self, visibility: int): + self._visibility = visibility + + def getVisibility(self) -> int: + return self._visibility + + def isShown(self) -> bool: + return self._visibility == View.VISIBLE + + # ─── position / size ─────────────────────────────────────── + + def getX(self) -> float: + return self._x + + def setX(self, x: float): + self._x = x + + def getY(self) -> float: + return self._y + + def setY(self, y: float): + self._y = y + + def getWidth(self) -> int: + return self._right - self._left + + def getHeight(self) -> int: + return self._bottom - self._top + + def getLeft(self) -> int: + return self._left + + def getTop(self) -> int: + return self._top + + def getRight(self) -> int: + return self._right + + def getBottom(self) -> int: + return self._bottom + + def getMeasuredWidth(self) -> int: + return self._measured_width + + def getMeasuredHeight(self) -> int: + return self._measured_height + + def setFrame(self, left: int, top: int, right: int, bottom: int) -> bool: + changed = (left != self._left or top != self._top or + right != self._right or bottom != self._bottom) + self._left, self._top, self._right, self._bottom = left, top, right, bottom + return changed + + def layout(self, l: int, t: int, r: int, b: int): + old_l, old_t, old_r, old_b = self._left, self._top, self._right, self._bottom + changed = self.setFrame(l, t, r, b) + if changed: + self.onLayout(changed, l - old_l, t - old_t, r - old_r, b - old_b) + + def onLayout(self, changed: bool, left: int, top: int, right: int, bottom: int): + pass + + def measure(self, width_measure_spec: int, height_measure_spec: int): + self.onMeasure(width_measure_spec, height_measure_spec) + + def onMeasure(self, width_measure_spec: int, height_measure_spec: int): + w = View.getDefaultSize(self.getSuggestedMinimumWidth(), width_measure_spec) + h = View.getDefaultSize(self.getSuggestedMinimumHeight(), height_measure_spec) + self.setMeasuredDimension(w, h) + + def setMeasuredDimension(self, measured_width: int, measured_height: int): + self._measured_width = measured_width + self._measured_height = measured_height + + def getSuggestedMinimumWidth(self) -> int: + return self._min_width + + def getSuggestedMinimumHeight(self) -> int: + return self._min_height + + def getMinimumWidth(self) -> int: + return self._min_width + + def getMinimumHeight(self) -> int: + return self._min_height + + def setMinimumWidth(self, min_w: int): + self._min_width = min_w + + def setMinimumHeight(self, min_h: int): + self._min_height = min_h + + def draw(self, canvas: Any): + self.onDraw(canvas) + + def onDraw(self, canvas: Any): + pass + + # ─── listeners ───────────────────────────────────────────── + + def setOnClickListener(self, listener: Optional[Callable]): + self._on_click_listener = listener + + def setOnLongClickListener(self, listener: Optional[Callable]) -> None: + self._on_long_click_listener = listener + + def setOnTouchListener(self, listener: Optional[Callable]) -> None: + self._on_touch_listener = listener + + def setOnKeyListener(self, listener: Optional[Callable]) -> None: + self._on_key_listener = listener + + def callOnClick(self) -> bool: + if self._on_click_listener: + self._on_click_listener(self) + return True + return False + + def performClick(self) -> bool: + return self.callOnClick() + + def performLongClick(self) -> bool: + if self._on_long_click_listener: + self._on_long_click_listener(self) + return True + return False + + # ─── post / delayed ──────────────────────────────────────── + + def post(self, action: Callable) -> bool: + action() + return True + + def postDelayed(self, action: Callable, delay_millis: int) -> bool: + import threading + timer = threading.Timer(delay_millis / 1000.0, action) + timer.daemon = True + timer.start() + self._callbacks[action] = (action, time.time() + delay_millis / 1000.0) + return True + + def removeCallbacks(self, action: Optional[Callable] = None): + if action is None: + self._callbacks.clear() + else: + self._callbacks.pop(action, None) + + # ─── invalidate / requestLayout ──────────────────────────── + + def invalidate(self): + pass + + def requestLayout(self): + pass + + def requestFocus(self, direction: int = FOCUS_FORWARD) -> bool: + if self._visibility != View.VISIBLE or not self._enabled: + return False + return True + + # ─── padding ─────────────────────────────────────────────── + + def setPadding(self, left: int, top: int, right: int, bottom: int): + self._padding_left = left + self._padding_top = top + self._padding_right = right + self._padding_bottom = bottom + + def getPaddingLeft(self) -> int: + return self._padding_left + + def getPaddingTop(self) -> int: + return self._padding_top + + def getPaddingRight(self) -> int: + return self._padding_right + + def getPaddingBottom(self) -> int: + return self._padding_bottom + + def getPaddingStart(self) -> int: + return self._padding_left + + def getPaddingEnd(self) -> int: + return self._padding_right + + # ─── background ──────────────────────────────────────────── + + def setBackgroundColor(self, color: int): + self._background_color = color + self._background_drawable = None + + def setBackground(self, background: Any): + self._background_drawable = background + + def setBackgroundDrawable(self, background: Any): + self._background_drawable = background + + def setBackgroundResource(self, resid: int): + self._background_resource = resid + + def getBackground(self) -> Any: + return self._background_drawable + + # ─── transform properties ────────────────────────────────── + + def setAlpha(self, alpha: float): + self._alpha = alpha + + def getAlpha(self) -> float: + return self._alpha + + def setRotation(self, rotation: float): + self._rotation = rotation + + def getRotation(self) -> float: + return self._rotation + + def setScaleX(self, scale_x: float): + self._scale_x = scale_x + + def getScaleX(self) -> float: + return self._scale_x + + def setScaleY(self, scale_y: float): + self._scale_y = scale_y + + def getScaleY(self) -> float: + return self._scale_y + + def setTranslationX(self, translation_x: float): + self._translation_x = translation_x + + def getTranslationX(self) -> float: + return self._translation_x + + def setTranslationY(self, translation_y: float): + self._translation_y = translation_y + + def getTranslationY(self) -> float: + return self._translation_y + + def getPivotX(self) -> float: + return self._pivot_x + + def setPivotX(self, pivot_x: float): + self._pivot_x = pivot_x + + def getPivotY(self) -> float: + return self._pivot_y + + def setPivotY(self, pivot_y: float): + self._pivot_y = pivot_y + + def setElevation(self, elevation: float): + self._elevation = elevation + + def getElevation(self) -> float: + return self._elevation + + def animate(self) -> "ViewPropertyAnimator": + return ViewPropertyAnimator(self) + + # ─── parent / root ───────────────────────────────────────── + + def getParent(self) -> Optional["ViewParent"]: + return self._parent + + def getRootView(self) -> "View": + v = self + while v._parent is not None: + v = v._parent + return v + + # ─── hierarchy ───────────────────────────────────────────── + + def findViewById(self, id_: int) -> Optional["View"]: + if self._id == id_: + return self + if hasattr(self, "_children"): + for child in self._children: + result = child.findViewById(id_) + if result is not None: + return result + return None + + def bringToFront(self): + if self._parent is not None: + self._parent.bringChildToFront(self) + + # ─── enable / focusable / clickable ──────────────────────── + + def setEnabled(self, enabled: bool): + self._enabled = enabled + + def isEnabled(self) -> bool: + return self._enabled + + def setFocusable(self, focusable: bool): + self._focusable = focusable + + def isFocusable(self) -> bool: + return self._focusable + + def setClickable(self, clickable: bool): + self._clickable = clickable + + def isClickable(self) -> bool: + return self._clickable + + def setLongClickable(self, long_clickable: bool): + self._long_clickable = long_clickable + + def isLongClickable(self) -> bool: + return self._long_clickable + + def setContextClickable(self, context_clickable: bool): + self._context_clickable = context_clickable + + def isContextClickable(self) -> bool: + return self._context_clickable + + def setSelected(self, selected: bool): + self._selected = selected + + def isSelected(self) -> bool: + return self._selected + + def setActivated(self, activated: bool): + self._activated = activated + + def isActivated(self) -> bool: + return self._activated + + # ─── context / resources ─────────────────────────────────── + + def getContext(self): + return self._context + + def getResources(self): + if hasattr(self._context, "getResources"): + return self._context.getResources() + return None + + # ─── scroll ──────────────────────────────────────────────── + + def setScrollX(self, value: int): + self._scroll_x = value + + def setScrollY(self, value: int): + self._scroll_y = value + + def getScrollX(self) -> int: + return self._scroll_x + + def getScrollY(self) -> int: + return self._scroll_y + + def scrollTo(self, x: int, y: int): + self._scroll_x = x + self._scroll_y = y + + def scrollBy(self, x: int, y: int): + self._scroll_x += x + self._scroll_y += y + + def computeScroll(self): + pass + + # ─── drawing / layer ─────────────────────────────────────── + + def setWillNotDraw(self, will_not_draw: bool): + self._will_not_draw = will_not_draw + + def willNotDraw(self) -> bool: + return self._will_not_draw + + def setLayerType(self, layer_type: int, paint: Any = None): + self._layer_type = layer_type + + def getLayerType(self) -> int: + return self._layer_type + + def buildLayer(self): + pass + + def setDrawingCacheQuality(self, quality: int): + pass + + def getDrawingCacheQuality(self) -> int: + return View.DRAWING_CACHE_QUALITY_AUTO + + def getDrawingCache(self, auto_scale: bool = True) -> Any: + return None + + def buildDrawingCache(self, auto_scale: bool = True): + pass + + def destroyDrawingCache(self): + pass + + # ─── layout direction ────────────────────────────────────── + + def setLayoutDirection(self, layout_direction: int): + self._layout_direction = layout_direction + + def getLayoutDirection(self) -> int: + return self._layout_direction + + def setTextDirection(self, text_direction: int): + pass + + def getTextDirection(self) -> int: + return 0 + + def setTextAlignment(self, text_alignment: int): + pass + + def getTextAlignment(self) -> int: + return 0 + + # ─── hit / focus ─────────────────────────────────────────── + + def onTouchEvent(self, event: "MotionEvent") -> bool: + return self._clickable or self._long_clickable + + def onTrackballEvent(self, event: "MotionEvent") -> bool: + return False + + def onKeyDown(self, key_code: int, event: "KeyEvent") -> bool: + return False + + def onKeyUp(self, key_code: int, event: "KeyEvent") -> bool: + return False + + def onKeyLongPress(self, key_code: int, event: "KeyEvent") -> bool: + return False + + def onKeyMultiple(self, key_code: int, repeat_count: int, event: "KeyEvent") -> bool: + return False + + def dispatchTouchEvent(self, event: "MotionEvent") -> bool: + if self._on_touch_listener is not None: + handled = self._on_touch_listener(self, event) + if handled: + return True + return self.onTouchEvent(event) + + def dispatchKeyEvent(self, event: "KeyEvent") -> bool: + if self._on_key_listener is not None: + handled = self._on_key_listener(self, event.getKeyCode(), event) + if handled: + return True + if event.getAction() == KeyEvent.ACTION_DOWN: + return self.onKeyDown(event.getKeyCode(), event) + elif event.getAction() == KeyEvent.ACTION_UP: + return self.onKeyUp(event.getKeyCode(), event) + return False + + def dispatchGenericMotionEvent(self, event: "MotionEvent") -> bool: + return False + + def hasFocus(self) -> bool: + return False + + def isFocused(self) -> bool: + return self.hasFocus() + + def clearFocus(self): + pass + + def onWindowFocusChanged(self, has_window_focus: bool): + pass + + def hasWindowFocus(self) -> bool: + return True + + def getWindowToken(self) -> Any: + return None + + def getWindowVisibility(self) -> int: + return View.VISIBLE + + def getWindowAttachCount(self) -> int: + return 1 + + def onAttachedToWindow(self): + pass + + def onDetachedFromWindow(self): + pass + + def isAttachedToWindow(self) -> bool: + return self._parent is not None + + # ─── Haptic ──────────────────────────────────────────────── + + def performHapticFeedback(self, feedback_constant: int) -> bool: + return False + + HapticFeedbackConstants = None + + # ─── static helpers ──────────────────────────────────────── + + @staticmethod + def generateViewId() -> int: + View._next_id += 1 + return View._next_id + + @staticmethod + def getDefaultSize(size: int, measure_spec: int) -> int: + spec_mode = View.MeasureSpec.getMode(measure_spec) + spec_size = View.MeasureSpec.getSize(measure_spec) + if spec_mode == View.UNSPECIFIED: + return size + else: + return spec_size + + @staticmethod + def combineMeasuredStates(cur_state: int, new_state: int) -> int: + return cur_state | new_state + + @staticmethod + def resolveSize(size: int, measure_spec: int) -> int: + return View.getDefaultSize(size, measure_spec) + + @staticmethod + def resolveSizeAndState(size: int, measure_spec: int, child_measured_state: int) -> int: + result = View.resolveSize(size, measure_spec) + return result | (child_measured_state & View.MEASURED_STATE_MASK) + + @staticmethod + def inflate(context, resource, root): + return View(context) + + +class MeasureSpec: + """MeasureSpec — 测量规格包装""" + EXACTLY = View.EXACTLY + AT_MOST = 0x80000000 # -2147483648 + UNSPECIFIED = 0 + MODE_SHIFT = 30 + MODE_MASK = 0xC0000000 + + @staticmethod + def makeMeasureSpec(size: int, mode: int) -> int: + return (size & ~MeasureSpec.MODE_MASK) | (mode & MeasureSpec.MODE_MASK) + + @staticmethod + def getMode(measure_spec: int) -> int: + return measure_spec & MeasureSpec.MODE_MASK + + @staticmethod + def getSize(measure_spec: int) -> int: + return measure_spec & ~MeasureSpec.MODE_MASK + + @staticmethod + def toString(measure_spec: int) -> str: + mode = MeasureSpec.getMode(measure_spec) + size = MeasureSpec.getSize(measure_spec) + mode_str = "UNSPECIFIED" + if mode == MeasureSpec.EXACTLY: + mode_str = "EXACTLY" + elif mode == MeasureSpec.AT_MOST: + mode_str = "AT_MOST" + return f"MeasureSpec: {mode_str} {size}" + + +View.MeasureSpec = MeasureSpec + + +class ViewPropertyAnimator: + """ViewPropertyAnimator — View 属性动画""" + + def __init__(self, view: View): + self._view = view + self._duration: int = 300 + self._interpolator: Any = None + + def setDuration(self, duration: int) -> "ViewPropertyAnimator": + self._duration = duration + return self + + def alpha(self, value: float) -> "ViewPropertyAnimator": + self._view.setAlpha(value) + return self + + def translationX(self, value: float) -> "ViewPropertyAnimator": + self._view.setTranslationX(value) + return self + + def translationY(self, value: float) -> "ViewPropertyAnimator": + self._view.setTranslationY(value) + return self + + def rotation(self, value: float) -> "ViewPropertyAnimator": + self._view.setRotation(value) + return self + + def scaleX(self, value: float) -> "ViewPropertyAnimator": + self._view.setScaleX(value) + return self + + def scaleY(self, value: float) -> "ViewPropertyAnimator": + self._view.setScaleY(value) + return self + + def x(self, value: float) -> "ViewPropertyAnimator": + self._view.setX(value) + return self + + def y(self, value: float) -> "ViewPropertyAnimator": + self._view.setY(value) + return self + + def start(self): + pass + + def cancel(self): + pass + + def setInterpolator(self, interpolator: Any) -> "ViewPropertyAnimator": + self._interpolator = interpolator + return self + + def withStartAction(self, runnable: Callable) -> "ViewPropertyAnimator": + return self + + def withEndAction(self, runnable: Callable) -> "ViewPropertyAnimator": + return self + + +# ═══════════════════════════════════════════════════════════════ +# ViewParent / ViewManager +# ═══════════════════════════════════════════════════════════════ + +class ViewParent(Stub): + """ViewParent — 视图父接口""" + + def requestLayout(self): + pass + + def invalidateChild(self, child: View, r: Any): + pass + + def requestChildFocus(self, child: View, focused: View): + pass + + def clearChildFocus(self, child: View): + pass + + def getParent(self) -> Optional["ViewParent"]: + return None + + def bringChildToFront(self, child: View): + pass + + def focusSearch(self, child: View, direction: int) -> Optional[View]: + return None + + def requestChildRectangleOnScreen(self, child: View, rectangle: Any, immediate: bool) -> bool: + return False + + def getChildVisibleRect(self, child: View, r: Any, offset: Any) -> bool: + return False + + def requestTransparentRegion(self, child: View): + pass + + def onStartNestedScroll(self, child: View, target: View, nested_scroll_axes: int) -> bool: + return False + + def onStopNestedScroll(self, target: View): + pass + + def onNestedScroll(self, target: View, dx_consumed: int, dy_consumed: int, + dx_unconsumed: int, dy_unconsumed: int): + pass + + def onNestedPreScroll(self, target: View, dx: int, dy: int, consumed: List[int]): + pass + + def onNestedFling(self, target: View, velocity_x: float, velocity_y: float, consumed: bool) -> bool: + return False + + def onNestedPreFling(self, target: View, velocity_x: float, velocity_y: float) -> bool: + return False + + def isNestedScrollingEnabled(self) -> bool: + return False + + def setNestedScrollingEnabled(self, enabled: bool): + pass + + def notifySubtreeAccessibilityStateChanged(self, child: View, source: View, change_type: int): + pass + + +class ViewManager(Stub): + """ViewManager — 视图管理接口""" + + def addView(self, view: View, params: "ViewGroup.LayoutParams"): + pass + + def updateViewLayout(self, view: View, params: "ViewGroup.LayoutParams"): + pass + + def removeView(self, view: View): + pass + + +# ═══════════════════════════════════════════════════════════════ +# ViewGroup +# ═══════════════════════════════════════════════════════════════ + +class ViewGroup(View, ViewParent, ViewManager): + """ViewGroup — 可包含子视图的容器""" + + CLIP_TO_PADDING_MASK = 0x00000001 + CLIP_CHILDREN_MASK = 0x00000002 + FLAG_CLIP_CHILDREN = 0x00000001 + FLAG_CLIP_TO_PADDING = 0x00000002 + FLAG_USE_CHILD_DRAWING_ORDER = 0x00000400 + + def __init__(self, context=None): + super().__init__(context) + self._children: List[View] = [] + self._clip_children = True + self._clip_to_padding = True + self._layout_mode = 0 + self._child_drawing_order_reversed = False + self._persistent_drawing_cache = False + self._always_drawn_with_cache = True + self._animate_on_set_layout = True + self._add_stateset = True + self._layout_animations = False + + # ─── child management ────────────────────────────────────── + + def addView(self, child: View, index: int = -1, + params: Optional["ViewGroup.LayoutParams"] = None): + if params is None: + params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + if index < 0 or index >= len(self._children): + self._children.append(child) + else: + self._children.insert(index, child) + child._parent = self + child.setLayoutParams(params) + child.onAttachedToWindow() + + def addView(self, child: View, params: Optional["ViewGroup.LayoutParams"] = None): + self.addView(child, -1, params) + + def removeView(self, view: View): + if view in self._children: + self._children.remove(view) + view._parent = None + view.onDetachedFromWindow() + + def removeViewAt(self, index: int): + if 0 <= index < len(self._children): + self.removeView(self._children[index]) + + def removeAllViews(self): + for child in list(self._children): + child._parent = None + child.onDetachedFromWindow() + self._children.clear() + + def getChildCount(self) -> int: + return len(self._children) + + def getChildAt(self, index: int) -> Optional[View]: + if 0 <= index < len(self._children): + return self._children[index] + return None + + def indexOfChild(self, child: View) -> int: + try: + return self._children.index(child) + except ValueError: + return -1 + + def bringChildToFront(self, child: View): + if child in self._children: + self._children.remove(child) + self._children.append(child) + + def requestChildFocus(self, child: View, focused: View): + pass + + def onInterceptTouchEvent(self, event: "MotionEvent") -> bool: + return False + + def onInterceptHoverEvent(self, event: "MotionEvent") -> bool: + return False + + def addChildrenForAccessibility(self, children: List[View]): + for child in self._children: + children.append(child) + + def setClipChildren(self, clip_children: bool): + self._clip_children = clip_children + + def getClipChildren(self) -> bool: + return self._clip_children + + def setClipToPadding(self, clip_to_padding: bool): + self._clip_to_padding = clip_to_padding + + def getClipToPadding(self) -> bool: + return self._clip_to_padding + + def setLayoutAnimation(self, layout_animation: Any): + pass + + def getLayoutAnimation(self) -> Any: + return None + + def setLayoutMode(self, layout_mode: int): + self._layout_mode = layout_mode + + def getLayoutMode(self) -> int: + return self._layout_mode + + def setPersistentDrawingCache(self, drawing_cache_to_keep: int): + self._persistent_drawing_cache = bool(drawing_cache_to_keep) + + def setAddStatesFromChildren(self, add_states: bool): + self._add_stateset = add_states + + def addStatesFromChildren(self) -> bool: + return self._add_stateset + + def setChildDrawingOrderCallback(self, callback: Any): + pass + + def setChildrenDrawingOrderEnabled(self, enabled: bool): + self._child_drawing_order_reversed = enabled + + def isChildrenDrawingOrderEnabled(self) -> bool: + return self._child_drawing_order_reversed + + def setChildrenDrawnWithCacheEnabled(self, enabled: bool): + self._always_drawn_with_cache = enabled + + def isChildrenDrawnWithCacheEnabled(self) -> bool: + return self._always_drawn_with_cache + + def setMotionEventSplittingEnabled(self, split: bool): + pass + + def isMotionEventSplittingEnabled(self) -> bool: + return True + + def setTransitionGroup(self, is_transition_group: bool): + pass + + def isTransitionGroup(self) -> bool: + return False + + def startLayoutAnimation(self): + pass + + def scheduleLayoutAnimation(self): + pass + + def gatherTransparentRegion(self, region: Any) -> bool: + return False + + def requestTransparentRegion(self, child: View): + pass + + def onChildVisibilityChanged(self, child: View, old_visibility: int, new_visibility: int): + pass + + def focusSearch(self, focused: View, direction: int) -> Optional[View]: + return None + + def focusableViewAvailable(self, v: View): + pass + + def showContextMenuForChild(self, original_view: View) -> bool: + return False + + def createContextMenu(self, menu: Any): + pass + + def showContextMenu(self) -> bool: + return False + + def startActionModeForChild(self, original_view: View, action_mode: Any) -> Optional[Any]: + return None + + # ─── LayoutParams ────────────────────────────────────────── + + class LayoutParams: + """ViewGroup.LayoutParams — 布局参数""" + + MATCH_PARENT = -1 + WRAP_CONTENT = -2 + + def __init__(self, width: int = WRAP_CONTENT, height: int = WRAP_CONTENT): + self.width = width + self.height = height + + @staticmethod + def getSize(size: int) -> int: + return size + + # ─── MarginLayoutParams ──────────────────────────────────── + + class MarginLayoutParams(LayoutParams): + """ViewGroup.MarginLayoutParams — 带边距的布局参数""" + + def __init__(self, width: int = -2, height: int = -2): + super().__init__(width, height) + self.leftMargin = 0 + self.topMargin = 0 + self.rightMargin = 0 + self.bottomMargin = 0 + self.startMargin = 0 + self.endMargin = 0 + + def setMargins(self, left: int, top: int, right: int, bottom: int): + self.leftMargin = left + self.topMargin = top + self.rightMargin = right + self.bottomMargin = bottom + + def getMarginLeft(self) -> int: + return self.leftMargin + + def getMarginTop(self) -> int: + return self.topMargin + + def getMarginRight(self) -> int: + return self.rightMargin + + def getMarginBottom(self) -> int: + return self.bottomMargin + + def getMarginStart(self) -> int: + return self.startMargin if self.startMargin != 0 else self.leftMargin + + def getMarginEnd(self) -> int: + return self.endMargin if self.endMargin != 0 else self.rightMargin + + def setMarginStart(self, start: int): + self.startMargin = start + + def setMarginEnd(self, end: int): + self.endMargin = end + + +# ═══════════════════════════════════════════════════════════════ +# WindowManager +# ═══════════════════════════════════════════════════════════════ + +class WindowManager(ViewManager): + """WindowManager — 窗口管理器""" + + class LayoutParams(ViewGroup.LayoutParams): + """WindowManager.LayoutParams — 窗口布局参数""" + + # 窗口类型 + TYPE_FIRST_APPLICATION = 1 + TYPE_APPLICATION = 2 + TYPE_APPLICATION_STARTING = 3 + TYPE_APPLICATION_PANEL = 1000 + TYPE_APPLICATION_MEDIA = 1001 + TYPE_APPLICATION_SUB_PANEL = 1002 + TYPE_APPLICATION_ATTACHED_DIALOG = 1003 + TYPE_APPLICATION_OVERLAY = 1004 + + # 标志位 + FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001 + FLAG_DIM_BEHIND = 0x00000002 + FLAG_BLUR_BEHIND = 0x00000004 + FLAG_NOT_FOCUSABLE = 0x00000008 + FLAG_NOT_TOUCHABLE = 0x00000010 + FLAG_NOT_TOUCH_MODAL = 0x00000020 + FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040 + FLAG_KEEP_SCREEN_ON = 0x00000080 + FLAG_LAYOUT_IN_SCREEN = 0x00000100 + FLAG_LAYOUT_NO_LIMITS = 0x00000200 + FLAG_FULLSCREEN = 0x00000400 + FLAG_FORCE_NOT_FULLSCREEN = 0x00000800 + FLAG_DITHER = 0x00001000 + FLAG_SECURE = 0x00002000 + FLAG_SCALED = 0x00004000 + FLAG_IGNORE_CHEEK_PRESSES = 0x00008000 + FLAG_LAYOUT_INSET_DECOR = 0x00010000 + FLAG_ALT_FOCUSABLE_IM = 0x00020000 + FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000 + FLAG_SHOW_WHEN_LOCKED = 0x00080000 + FLAG_SHOW_WALLPAPER = 0x00100000 + FLAG_TURN_SCREEN_ON = 0x00200000 + FLAG_DISMISS_KEYGUARD = 0x00400000 + FLAG_SPLIT_TOUCH = 0x00800000 + FLAG_HARDWARE_ACCELERATED = 0x01000000 + FLAG_LOCAL_FOCUS_MODE = 0x02000000 + FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x04000000 + FLAG_TRANSLUCENT_STATUS = 0x04000000 + FLAG_TRANSLUCENT_NAVIGATION = 0x08000000 + + # 软输入模式 + SOFT_INPUT_STATE_UNSPECIFIED = 0 + SOFT_INPUT_STATE_UNCHANGED = 1 + SOFT_INPUT_STATE_HIDDEN = 2 + SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3 + SOFT_INPUT_STATE_VISIBLE = 4 + SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5 + SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00 + SOFT_INPUT_ADJUST_RESIZE = 0x10 + SOFT_INPUT_ADJUST_PAN = 0x20 + SOFT_INPUT_ADJUST_NOTHING = 0x30 + SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100 + + # 屏幕方向 + SCREEN_ORIENTATION_UNSPECIFIED = -1 + SCREEN_ORIENTATION_LANDSCAPE = 0 + SCREEN_ORIENTATION_PORTRAIT = 1 + SCREEN_ORIENTATION_USER = 2 + SCREEN_ORIENTATION_BEHIND = 3 + SCREEN_ORIENTATION_SENSOR = 4 + SCREEN_ORIENTATION_NOSENSOR = 5 + SCREEN_ORIENTATION_SENSOR_LANDSCAPE = 6 + SCREEN_ORIENTATION_SENSOR_PORTRAIT = 7 + SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8 + SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9 + SCREEN_ORIENTATION_FULL_SENSOR = 10 + SCREEN_ORIENTATION_USER_LANDSCAPE = 11 + SCREEN_ORIENTATION_USER_PORTRAIT = 12 + SCREEN_ORIENTATION_FULL_USER = 13 + SCREEN_ORIENTATION_LOCKED = 14 + + # 重力 + gravity = Gravity.NO_GRAVITY + x = 0 + y = 0 + horizontalMargin = 0.0 + verticalMargin = 0.0 + + def __init__(self, w: int = ViewGroup.LayoutParams.WRAP_CONTENT, + h: int = ViewGroup.LayoutParams.WRAP_CONTENT): + super().__init__(w, h) + self.type = WindowManager.LayoutParams.TYPE_APPLICATION + self.flags = 0 + self.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED + self.screenOrientation = WindowManager.LayoutParams.SCREEN_ORIENTATION_UNSPECIFIED + self.gravity = Gravity.NO_GRAVITY + self.x = 0 + self.y = 0 + self.token: Any = None + self.windowAnimations: int = 0 + self.packageName: str = "" + self.screenBrightness = -1.0 + self.buttonBrightness = -1.0 + self.rotationAnimation = 0 + self.preferredDisplayModeId = 0 + self.colorMode = 0 + self.format = -1 + + def __init__(self): + super().__init__() + self._views: Dict[View, WindowManager.LayoutParams] = {} + + def addView(self, view: View, params: LayoutParams): + self._views[view] = params + + def updateViewLayout(self, view: View, params: LayoutParams): + if view in self._views: + self._views[view] = params + + def removeView(self, view: View): + self._views.pop(view, None) + + def removeViewImmediate(self, view: View): + self._views.pop(view, None) + + def getDefaultDisplay(self) -> "Display": + return Display() + + +# ═══════════════════════════════════════════════════════════════ +# MotionEvent +# ═══════════════════════════════════════════════════════════════ + +class MotionEvent(Stub): + """MotionEvent — 触摸事件""" + + # 动作常量 + ACTION_DOWN = 0 + ACTION_UP = 1 + ACTION_MOVE = 2 + ACTION_CANCEL = 3 + ACTION_OUTSIDE = 4 + ACTION_POINTER_DOWN = 5 + ACTION_POINTER_UP = 6 + ACTION_HOVER_MOVE = 7 + ACTION_HOVER_ENTER = 8 + ACTION_HOVER_EXIT = 9 + ACTION_SCROLL = 11 + ACTION_BUTTON_PRESS = 12 + ACTION_BUTTON_RELEASE = 13 + + # 动作掩码 + ACTION_MASK = 0xFF + ACTION_POINTER_INDEX_MASK = 0xFF00 + ACTION_POINTER_INDEX_SHIFT = 8 + + # 按钮 + BUTTON_PRIMARY = 1 + BUTTON_SECONDARY = 2 + BUTTON_TERTIARY = 4 + BUTTON_BACK = 8 + BUTTON_FORWARD = 16 + BUTTON_STYLUS_PRIMARY = 32 + BUTTON_STYLUS_SECONDARY = 64 + + # 边缘 + EDGE_LEFT = 1 + EDGE_TOP = 2 + EDGE_RIGHT = 4 + EDGE_BOTTOM = 8 + + # 坐标分类 + AXIS_X = 0 + AXIS_Y = 1 + AXIS_PRESSURE = 2 + AXIS_SIZE = 3 + AXIS_TOUCH_MAJOR = 4 + AXIS_TOUCH_MINOR = 5 + AXIS_TOOL_MAJOR = 6 + AXIS_TOOL_MINOR = 7 + AXIS_ORIENTATION = 8 + AXIS_VSCROLL = 9 + AXIS_HSCROLL = 10 + AXIS_Z = 11 + AXIS_RX = 12 + AXIS_RY = 13 + AXIS_RZ = 14 + AXIS_HAT_X = 15 + AXIS_HAT_Y = 16 + AXIS_LTRIGGER = 17 + AXIS_RTRIGGER = 18 + AXIS_THROTTLE = 19 + AXIS_RUDDER = 20 + AXIS_WHEEL = 21 + AXIS_GAS = 22 + AXIS_BRAKE = 23 + AXIS_DISTANCE = 24 + AXIS_TILT = 25 + AXIS_SCROLL = 26 + AXIS_RELATIVE_X = 27 + AXIS_RELATIVE_Y = 28 + AXIS_GENERIC_1 = 32 + AXIS_GENERIC_2 = 33 + AXIS_GENERIC_3 = 34 + AXIS_GENERIC_4 = 35 + AXIS_GENERIC_5 = 36 + AXIS_GENERIC_6 = 37 + AXIS_GENERIC_7 = 38 + AXIS_GENERIC_8 = 39 + AXIS_GENERIC_9 = 40 + AXIS_GENERIC_10 = 41 + AXIS_GENERIC_11 = 42 + AXIS_GENERIC_12 = 43 + AXIS_GENERIC_13 = 44 + AXIS_GENERIC_14 = 45 + AXIS_GENERIC_15 = 46 + AXIS_GENERIC_16 = 47 + + # 输入类型 + TOOL_TYPE_UNKNOWN = 0 + TOOL_TYPE_FINGER = 1 + TOOL_TYPE_STYLUS = 2 + TOOL_TYPE_MOUSE = 3 + TOOL_TYPE_ERASER = 4 + + INVALID_POINTER_ID = -1 + + def __init__(self, action: int = 0, x: float = 0.0, y: float = 0.0, + event_time: int = 0, pointer_count: int = 1, + meta_state: int = 0, button_state: int = 0): + super().__init__() + self._action = action + self._x = x + self._y = y + self._raw_x = x + self._raw_y = y + self._event_time = event_time if event_time else int(time.time() * 1000) + self._down_time = self._event_time + self._pointer_count = pointer_count + self._meta_state = meta_state + self._button_state = button_state + self._pointers: List[Dict] = [] + for i in range(pointer_count): + self._pointers.append({ + "id": i, + "x": x, + "y": y, + "pressure": 1.0, + "size": 0.5, + "tool_type": MotionEvent.TOOL_TYPE_FINGER, + }) + + @classmethod + def obtain(cls, down_time: int, event_time: int, action: int, + x: float, y: float, meta_state: int = 0) -> "MotionEvent": + return cls(action=action, x=x, y=y, event_time=event_time) + + @classmethod + def obtainNoHistory(cls, me: "MotionEvent") -> "MotionEvent": + return cls(action=me._action, x=me._x, y=me._y, + event_time=me._event_time, pointer_count=me._pointer_count) + + def recycle(self): + pass + + def getAction(self) -> int: + return self._action + + def getActionMasked(self) -> int: + return self._action & MotionEvent.ACTION_MASK + + def getActionIndex(self) -> int: + return (self._action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT + + def getX(self, pointer_index: int = 0) -> float: + if 0 <= pointer_index < len(self._pointers): + return self._pointers[pointer_index]["x"] + return self._x + + def getY(self, pointer_index: int = 0) -> float: + if 0 <= pointer_index < len(self._pointers): + return self._pointers[pointer_index]["y"] + return self._y + + def getRawX(self) -> float: + return self._raw_x + + def getRawY(self) -> float: + return self._raw_y + + def getPointerCount(self) -> int: + return self._pointer_count + + def getPointerId(self, pointer_index: int) -> int: + if 0 <= pointer_index < len(self._pointers): + return self._pointers[pointer_index]["id"] + return pointer_index + + def findPointerIndex(self, pointer_id: int) -> int: + for i, p in enumerate(self._pointers): + if p["id"] == pointer_id: + return i + return -1 + + def getPressure(self, pointer_index: int = 0) -> float: + if 0 <= pointer_index < len(self._pointers): + return self._pointers[pointer_index]["pressure"] + return 1.0 + + def getSize(self, pointer_index: int = 0) -> float: + if 0 <= pointer_index < len(self._pointers): + return self._pointers[pointer_index]["size"] + return 0.5 + + def getEventTime(self) -> int: + return self._event_time + + def getDownTime(self) -> int: + return self._down_time + + def getMetaState(self) -> int: + return self._meta_state + + def getButtonState(self) -> int: + return self._button_state + + def getToolType(self, pointer_index: int = 0) -> int: + if 0 <= pointer_index < len(self._pointers): + return self._pointers[pointer_index]["tool_type"] + return MotionEvent.TOOL_TYPE_FINGER + + def setLocation(self, x: float, y: float): + self._x = x + self._y = y + + def offsetLocation(self, dx: float, dy: float): + self._x += dx + self._y += dy + + def transform(self, matrix: Any): + pass + + def isLongPress(self) -> bool: + return (self._event_time - self._down_time) > 500 + + def getHistoricalSize(self, pos: int, pointer: int = 0) -> float: + return 0.5 + + +# ═══════════════════════════════════════════════════════════════ +# KeyEvent +# ═══════════════════════════════════════════════════════════════ + +class KeyEvent(Stub): + """KeyEvent — 按键事件""" + + # 动作 + ACTION_DOWN = 0 + ACTION_UP = 1 + ACTION_MULTIPLE = 2 + + # 键码 + KEYCODE_UNKNOWN = 0 + KEYCODE_SOFT_LEFT = 1 + KEYCODE_SOFT_RIGHT = 2 + KEYCODE_HOME = 3 + KEYCODE_BACK = 4 + KEYCODE_CALL = 5 + KEYCODE_ENDCALL = 6 + KEYCODE_0 = 7 + KEYCODE_1 = 8 + KEYCODE_2 = 9 + KEYCODE_3 = 10 + KEYCODE_4 = 11 + KEYCODE_5 = 12 + KEYCODE_6 = 13 + KEYCODE_7 = 14 + KEYCODE_8 = 15 + KEYCODE_9 = 16 + KEYCODE_STAR = 17 + KEYCODE_POUND = 18 + KEYCODE_DPAD_UP = 19 + KEYCODE_DPAD_DOWN = 20 + KEYCODE_DPAD_LEFT = 21 + KEYCODE_DPAD_RIGHT = 22 + KEYCODE_DPAD_CENTER = 23 + KEYCODE_VOLUME_UP = 24 + KEYCODE_VOLUME_DOWN = 25 + KEYCODE_POWER = 26 + KEYCODE_CAMERA = 27 + KEYCODE_CLEAR = 28 + KEYCODE_A = 29 + KEYCODE_B = 30 + KEYCODE_C = 31 + KEYCODE_D = 32 + KEYCODE_E = 33 + KEYCODE_F = 34 + KEYCODE_G = 35 + KEYCODE_H = 36 + KEYCODE_I = 37 + KEYCODE_J = 38 + KEYCODE_K = 39 + KEYCODE_L = 40 + KEYCODE_M = 41 + KEYCODE_N = 42 + KEYCODE_O = 43 + KEYCODE_P = 44 + KEYCODE_Q = 45 + KEYCODE_R = 46 + KEYCODE_S = 47 + KEYCODE_T = 48 + KEYCODE_U = 49 + KEYCODE_V = 50 + KEYCODE_W = 51 + KEYCODE_X = 52 + KEYCODE_Y = 53 + KEYCODE_Z = 54 + KEYCODE_COMMA = 55 + KEYCODE_PERIOD = 56 + KEYCODE_ALT_LEFT = 57 + KEYCODE_ALT_RIGHT = 58 + KEYCODE_SHIFT_LEFT = 59 + KEYCODE_SHIFT_RIGHT = 60 + KEYCODE_TAB = 61 + KEYCODE_SPACE = 62 + KEYCODE_SYM = 63 + KEYCODE_EXPLORER = 64 + KEYCODE_ENVELOPE = 65 + KEYCODE_ENTER = 66 + KEYCODE_DEL = 67 + KEYCODE_GRAVE = 68 + KEYCODE_MINUS = 69 + KEYCODE_EQUALS = 70 + KEYCODE_LEFT_BRACKET = 71 + KEYCODE_RIGHT_BRACKET = 72 + KEYCODE_BACKSLASH = 73 + KEYCODE_SEMICOLON = 74 + KEYCODE_APOSTROPHE = 75 + KEYCODE_SLASH = 76 + KEYCODE_AT = 77 + KEYCODE_NUM = 78 + KEYCODE_HEADSETHOOK = 79 + KEYCODE_FOCUS = 80 + KEYCODE_PLUS = 81 + KEYCODE_MENU = 82 + KEYCODE_NOTIFICATION = 83 + KEYCODE_SEARCH = 84 + KEYCODE_MEDIA_PLAY_PAUSE = 85 + KEYCODE_MEDIA_STOP = 86 + KEYCODE_MEDIA_NEXT = 87 + KEYCODE_MEDIA_PREVIOUS = 88 + KEYCODE_MEDIA_REWIND = 89 + KEYCODE_MEDIA_FAST_FORWARD = 90 + KEYCODE_MUTE = 91 + KEYCODE_PAGE_UP = 92 + KEYCODE_PAGE_DOWN = 93 + KEYCODE_PICTSYMBOLS = 94 + KEYCODE_SWITCH_CHARSET = 95 + KEYCODE_BUTTON_A = 96 + KEYCODE_BUTTON_B = 97 + KEYCODE_BUTTON_C = 98 + KEYCODE_BUTTON_X = 99 + KEYCODE_BUTTON_Y = 100 + KEYCODE_BUTTON_Z = 101 + KEYCODE_BUTTON_L1 = 102 + KEYCODE_BUTTON_R1 = 103 + KEYCODE_BUTTON_L2 = 104 + KEYCODE_BUTTON_R2 = 105 + KEYCODE_BUTTON_THUMBL = 106 + KEYCODE_BUTTON_THUMBR = 107 + KEYCODE_BUTTON_START = 108 + KEYCODE_BUTTON_SELECT = 109 + KEYCODE_BUTTON_MODE = 110 + KEYCODE_ESCAPE = 111 + KEYCODE_FORWARD_DEL = 112 + KEYCODE_CTRL_LEFT = 113 + KEYCODE_CTRL_RIGHT = 114 + KEYCODE_CAPS_LOCK = 115 + KEYCODE_SCROLL_LOCK = 116 + KEYCODE_META_LEFT = 117 + KEYCODE_META_RIGHT = 118 + KEYCODE_FUNCTION = 119 + KEYCODE_SYSRQ = 120 + KEYCODE_BREAK = 121 + KEYCODE_MOVE_HOME = 122 + KEYCODE_MOVE_END = 123 + KEYCODE_INSERT = 124 + KEYCODE_FORWARD = 125 + KEYCODE_MEDIA_PLAY = 126 + KEYCODE_MEDIA_PAUSE = 127 + KEYCODE_MEDIA_CLOSE = 128 + KEYCODE_MEDIA_EJECT = 129 + KEYCODE_MEDIA_RECORD = 130 + KEYCODE_F1 = 131 + KEYCODE_F2 = 132 + KEYCODE_F3 = 133 + KEYCODE_F4 = 134 + KEYCODE_F5 = 135 + KEYCODE_F6 = 136 + KEYCODE_F7 = 137 + KEYCODE_F8 = 138 + KEYCODE_F9 = 139 + KEYCODE_F10 = 140 + KEYCODE_F11 = 141 + KEYCODE_F12 = 142 + KEYCODE_NUM_LOCK = 143 + KEYCODE_NUMPAD_0 = 144 + KEYCODE_NUMPAD_1 = 145 + KEYCODE_NUMPAD_2 = 146 + KEYCODE_NUMPAD_3 = 147 + KEYCODE_NUMPAD_4 = 148 + KEYCODE_NUMPAD_5 = 149 + KEYCODE_NUMPAD_6 = 150 + KEYCODE_NUMPAD_7 = 151 + KEYCODE_NUMPAD_8 = 152 + KEYCODE_NUMPAD_9 = 153 + KEYCODE_NUMPAD_DIVIDE = 154 + KEYCODE_NUMPAD_MULTIPLY = 155 + KEYCODE_NUMPAD_SUBTRACT = 156 + KEYCODE_NUMPAD_ADD = 157 + KEYCODE_NUMPAD_DOT = 158 + KEYCODE_NUMPAD_COMMA = 159 + KEYCODE_NUMPAD_ENTER = 160 + KEYCODE_NUMPAD_EQUALS = 161 + KEYCODE_NUMPAD_LEFT_PAREN = 162 + KEYCODE_NUMPAD_RIGHT_PAREN = 163 + KEYCODE_META_CTRL_LEFT = 164 + KEYCODE_META_CTRL_RIGHT = 165 + KEYCODE_META_ALT_LEFT = 166 + KEYCODE_META_ALT_RIGHT = 167 + KEYCODE_META_SHIFT_LEFT = 168 + KEYCODE_META_SHIFT_RIGHT = 169 + KEYCODE_META_SYM = 170 + + # 元键掩码 + META_ALT_ON = 0x02 + META_ALT_LEFT_ON = 0x10 + META_ALT_RIGHT_ON = 0x20 + META_SHIFT_ON = 0x01 + META_SHIFT_LEFT_ON = 0x40 + META_SHIFT_RIGHT_ON = 0x80 + META_SYM_ON = 0x04 + META_FUNCTION_ON = 0x08 + META_CTRL_ON = 0x1000 + META_CTRL_LEFT_ON = 0x2000 + META_CTRL_RIGHT_ON = 0x4000 + META_META_ON = 0x10000 + META_META_LEFT_ON = 0x20000 + META_META_RIGHT_ON = 0x40000 + META_CAPS_LOCK_ON = 0x100000 + META_NUM_LOCK_ON = 0x200000 + META_SCROLL_LOCK_ON = 0x400000 + + def __init__(self, action: int = ACTION_DOWN, key_code: int = KEYCODE_UNKNOWN, + repeat_count: int = 0, event_time: int = 0, + meta_state: int = 0): + super().__init__() + self._action = action + self._key_code = key_code + self._repeat_count = repeat_count + self._event_time = event_time if event_time else int(time.time() * 1000) + self._down_time = self._event_time + self._meta_state = meta_state + self._device_id = 0 + self._source = 0x101 # InputDevice.SOURCE_KEYBOARD + self._flags = 0 + self._display_id = 0 + + getAction = lambda self: self._action + getKeyCode = lambda self: self._key_code + getRepeatCount = lambda self: self._repeat_count + getEventTime = lambda self: self._event_time + getDownTime = lambda self: self._down_time + getMetaState = lambda self: self._meta_state + getDeviceId = lambda self: self._device_id + getSource = lambda self: self._source + getFlags = lambda self: self._flags + getDisplayId = lambda self: self._display_id + + def isLongPress(self) -> bool: + return self._repeat_count > 0 + + def dispatch(self, receiver: View, on_receive: Callable) -> bool: + return on_receive(receiver, self) + + def isAltPressed(self) -> bool: + return bool(self._meta_state & KeyEvent.META_ALT_ON) + + def isShiftPressed(self) -> bool: + return bool(self._meta_state & KeyEvent.META_SHIFT_ON) + + def isCtrlPressed(self) -> bool: + return bool(self._meta_state & KeyEvent.META_CTRL_ON) + + def isMetaPressed(self) -> bool: + return bool(self._meta_state & KeyEvent.META_META_ON) + + def isSymPressed(self) -> bool: + return bool(self._meta_state & KeyEvent.META_SYM_ON) + + def isFunctionPressed(self) -> bool: + return bool(self._meta_state & KeyEvent.META_FUNCTION_ON) + + def isCapsLockOn(self) -> bool: + return bool(self._meta_state & KeyEvent.META_CAPS_LOCK_ON) + + def isNumLockOn(self) -> bool: + return bool(self._meta_state & KeyEvent.META_NUM_LOCK_ON) + + def isScrollLockOn(self) -> bool: + return bool(self._meta_state & KeyEvent.META_SCROLL_LOCK_ON) + + @staticmethod + def changeTimeRepeat(event: "KeyEvent", event_time: int, repeat_count: int) -> "KeyEvent": + return KeyEvent( + action=event._action, + key_code=event._key_code, + repeat_count=repeat_count, + event_time=event_time, + meta_state=event._meta_state, + ) + + @staticmethod + def changeAction(event: "KeyEvent", action: int) -> "KeyEvent": + return KeyEvent( + action=action, + key_code=event._key_code, + repeat_count=event._repeat_count, + event_time=event._event_time, + meta_state=event._meta_state, + ) + + +# ═══════════════════════════════════════════════════════════════ +# GestureDetector +# ═══════════════════════════════════════════════════════════════ + +class GestureDetector(Stub): + """GestureDetector — 手势检测器""" + + class SimpleOnGestureListener: + """SimpleOnGestureListener — 简单手势监听器""" + + def onDown(self, e: MotionEvent) -> bool: + return False + + def onShowPress(self, e: MotionEvent): + pass + + def onSingleTapUp(self, e: MotionEvent) -> bool: + return False + + def onScroll(self, e1: MotionEvent, e2: MotionEvent, + distance_x: float, distance_y: float) -> bool: + return False + + def onLongPress(self, e: MotionEvent): + pass + + def onFling(self, e1: MotionEvent, e2: MotionEvent, + velocity_x: float, velocity_y: float) -> bool: + return False + + def onSingleTapConfirmed(self, e: MotionEvent) -> bool: + return False + + def onDoubleTap(self, e: MotionEvent) -> bool: + return False + + def onDoubleTapEvent(self, e: MotionEvent) -> bool: + return False + + def onContextClick(self, e: MotionEvent) -> bool: + return False + + class OnGestureListener: + def onDown(self, e: MotionEvent) -> bool: + return False + def onShowPress(self, e: MotionEvent): + pass + def onSingleTapUp(self, e: MotionEvent) -> bool: + return False + def onScroll(self, e1: MotionEvent, e2: MotionEvent, + distance_x: float, distance_y: float) -> bool: + return False + def onLongPress(self, e: MotionEvent): + pass + def onFling(self, e1: MotionEvent, e2: MotionEvent, + velocity_x: float, velocity_y: float) -> bool: + return False + + class OnDoubleTapListener: + def onSingleTapConfirmed(self, e: MotionEvent) -> bool: + return False + def onDoubleTap(self, e: MotionEvent) -> bool: + return False + def onDoubleTapEvent(self, e: MotionEvent) -> bool: + return False + + def __init__(self, context=None, listener: Optional["GestureDetector.OnGestureListener"] = None, + handler: Any = None): + super().__init__() + self._listener = listener + self._double_tap_listener: Optional["GestureDetector.OnDoubleTapListener"] = None + self._is_longpress_enabled = True + self._context = context + self._current_down_event: Optional[MotionEvent] = None + self._previous_down_event: Optional[MotionEvent] = None + self._in_long_press = False + self._long_press_timeout = 500 + self._touch_slop = 8 + + def setOnDoubleTapListener(self, listener: Optional["GestureDetector.OnDoubleTapListener"]): + self._double_tap_listener = listener + + def setIsLongpressEnabled(self, enabled: bool): + self._is_longpress_enabled = enabled + + def isLongpressEnabled(self) -> bool: + return self._is_longpress_enabled + + def onTouchEvent(self, ev: MotionEvent) -> bool: + action = ev.getAction() + if action == MotionEvent.ACTION_DOWN: + self._in_long_press = False + self._current_down_event = ev + if self._listener: + return self._listener.onDown(ev) + elif action == MotionEvent.ACTION_MOVE: + pass + elif action == MotionEvent.ACTION_UP: + if self._listener: + return self._listener.onSingleTapUp(ev) + elif action == MotionEvent.ACTION_CANCEL: + pass + return False + + def setContextClickListener(self, listener: Callable): + pass + + def setContextClickable(self, context_clickable: bool): + pass + + def isContextClickable(self) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# ScaleGestureDetector +# ═══════════════════════════════════════════════════════════════ + +class ScaleGestureDetector(Stub): + """ScaleGestureDetector — 缩放手势检测器""" + + class SimpleOnScaleGestureListener: + """SimpleOnScaleGestureListener — 简单缩放手势监听器""" + + def onScaleBegin(self, detector: "ScaleGestureDetector") -> bool: + return True + + def onScale(self, detector: "ScaleGestureDetector") -> bool: + return False + + def onScaleEnd(self, detector: "ScaleGestureDetector"): + pass + + class OnScaleGestureListener: + def onScaleBegin(self, detector: "ScaleGestureDetector") -> bool: + return True + def onScale(self, detector: "ScaleGestureDetector") -> bool: + return False + def onScaleEnd(self, detector: "ScaleGestureDetector"): + pass + + def __init__(self, context=None, listener: Optional[OnScaleGestureListener] = None): + super().__init__() + self._listener = listener + self._scale_factor = 1.0 + self._current_span = 0.0 + self._current_span_x = 0.0 + self._current_span_y = 0.0 + self._focus_x = 0.0 + self._focus_y = 0.0 + self._previous_span = 0.0 + self._time_delta = 0 + self._in_progress = False + + def getScaleFactor(self) -> float: + return self._scale_factor + + def getCurrentSpan(self) -> float: + return self._current_span + + def getCurrentSpanX(self) -> float: + return self._current_span_x + + def getCurrentSpanY(self) -> float: + return self._current_span_y + + def getPreviousSpan(self) -> float: + return self._previous_span + + def getPreviousSpanX(self) -> float: + return self._current_span_x + + def getPreviousSpanY(self) -> float: + return self._current_span_y + + def getFocusX(self) -> float: + return self._focus_x + + def getFocusY(self) -> float: + return self._focus_y + + def getTimeDelta(self) -> int: + return self._time_delta + + def getEventTime(self) -> int: + return int(time.time() * 1000) + + def isInProgress(self) -> bool: + return self._in_progress + + def onTouchEvent(self, event: MotionEvent) -> bool: + action = event.getActionMasked() + if action == MotionEvent.ACTION_DOWN: + self._in_progress = False + elif action == MotionEvent.ACTION_POINTER_DOWN: + if event.getPointerCount() >= 2: + self._in_progress = True + x1, y1 = event.getX(0), event.getY(0) + x2, y2 = event.getX(1), event.getY(1) + self._current_span = math.hypot(x2 - x1, y2 - y1) + self._focus_x = (x1 + x2) / 2.0 + self._focus_y = (y1 + y2) / 2.0 + if self._listener: + self._listener.onScaleBegin(self) + return True + elif action == MotionEvent.ACTION_MOVE: + if self._in_progress and event.getPointerCount() >= 2: + x1, y1 = event.getX(0), event.getY(0) + x2, y2 = event.getX(1), event.getY(1) + new_span = math.hypot(x2 - x1, y2 - y1) + self._previous_span = self._current_span + self._current_span = new_span + if self._previous_span > 0: + self._scale_factor = self._current_span / self._previous_span + self._focus_x = (x1 + x2) / 2.0 + self._focus_y = (y1 + y2) / 2.0 + if self._listener: + self._listener.onScale(self) + return True + elif action in (MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL): + if self._in_progress and self._listener: + self._listener.onScaleEnd(self) + self._in_progress = False + return False + + +# ═══════════════════════════════════════════════════════════════ +# VelocityTracker +# ═══════════════════════════════════════════════════════════════ + +class VelocityTracker(Stub): + """VelocityTracker — 速度追踪器""" + + def __init__(self): + super().__init__() + self._movements: List[Tuple[float, float, float]] = [] # (x, y, time) + self._x_velocity: float = 0.0 + self._y_velocity: float = 0.0 + + @staticmethod + def obtain() -> "VelocityTracker": + return VelocityTracker() + + @staticmethod + def obtainUnit(max_units: int, generator_id: int) -> "VelocityTracker": + return VelocityTracker() + + def addMovement(self, event: MotionEvent): + self._movements.append((event.getRawX(), event.getRawY(), + event.getEventTime() / 1000.0)) + if len(self._movements) > 100: + self._movements = self._movements[-50:] + + def computeCurrentVelocity(self, units: int, max_velocity: float = float("inf")): + if len(self._movements) < 2: + self._x_velocity = 0.0 + self._y_velocity = 0.0 + return + recent = self._movements[-4:] if len(self._movements) >= 4 else self._movements + first_x, first_y, first_t = recent[0] + last_x, last_y, last_t = recent[-1] + dt = last_t - first_t + if dt > 0: + unit_scale = units / 1000.0 + self._x_velocity = (last_x - first_x) / dt * unit_scale + self._y_velocity = (last_y - first_y) / dt * unit_scale + self._x_velocity = max(-max_velocity, min(max_velocity, self._x_velocity)) + self._y_velocity = max(-max_velocity, min(max_velocity, self._y_velocity)) + else: + self._x_velocity = 0.0 + self._y_velocity = 0.0 + + def getXVelocity(self, id_: int = -1) -> float: + return self._x_velocity + + def getYVelocity(self, id_: int = -1) -> float: + return self._y_velocity + + def recycle(self): + self._movements.clear() + self._x_velocity = 0.0 + self._y_velocity = 0.0 + + def clear(self): + self._movements.clear() + + def getEstimator(self) -> Any: + return None + + +# ═══════════════════════════════════════════════════════════════ +# DragEvent +# ═══════════════════════════════════════════════════════════════ + +class DragEvent(Stub): + """DragEvent — 拖放事件""" + + ACTION_DRAG_STARTED = 1 + ACTION_DRAG_LOCATION = 2 + ACTION_DROP = 3 + ACTION_DRAG_ENTERED = 4 + ACTION_DRAG_EXITED = 5 + ACTION_DRAG_ENDED = 6 + + def __init__(self, action: int = 0, x: float = 0.0, y: float = 0.0, + clip_data: Any = None, result: bool = False): + super().__init__() + self._action = action + self._x = x + self._y = y + self._clip_data = clip_data + self._result = result + self._local_state: Any = None + + def getAction(self) -> int: + return self._action + + def getX(self) -> float: + return self._x + + def getY(self) -> float: + return self._y + + def getClipData(self) -> Any: + return self._clip_data + + def getResult(self) -> bool: + return self._result + + def getLocalState(self) -> Any: + return self._local_state + + def setLocalState(self, state: Any): + self._local_state = state + + def toString(self) -> str: + action_names = { + 1: "ACTION_DRAG_STARTED", + 2: "ACTION_DRAG_LOCATION", + 3: "ACTION_DROP", + 4: "ACTION_DRAG_ENTERED", + 5: "ACTION_DRAG_EXITED", + 6: "ACTION_DRAG_ENDED", + } + name = action_names.get(self._action, f"UNKNOWN({self._action})") + return f"DragEvent(action={name}, x={self._x}, y={self._y})" + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityDelegate +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityDelegate(Stub): + """AccessibilityDelegate — 辅助功能委托""" + + def sendAccessibilityEvent(self, host: View, event_type: int): + pass + + def sendAccessibilityEventUnchecked(self, host: View, event: Any): + pass + + def dispatchPopulateAccessibilityEvent(self, host: View, event: Any) -> bool: + return False + + def onPopulateAccessibilityEvent(self, host: View, event: Any): + pass + + def onInitializeAccessibilityEvent(self, host: View, event: Any): + pass + + def onInitializeAccessibilityNodeInfo(self, host: View, info: Any): + pass + + def onRequestSendAccessibilityEvent(self, host: ViewGroup, child: View, event: Any) -> bool: + return True + + def getAccessibilityNodeProvider(self, host: View) -> Optional[Any]: + return None + + def performAccessibilityAction(self, host: View, action: int, arguments: Any) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# ViewTreeObserver +# ═══════════════════════════════════════════════════════════════ + +class ViewTreeObserver(Stub): + """ViewTreeObserver — 视图树观察者""" + + class OnGlobalLayoutListener: + def onGlobalLayout(self): + pass + + class OnGlobalFocusChangeListener: + def onGlobalFocusChanged(self, old_focus: View, new_focus: View): + pass + + class OnScrollChangedListener: + def onScrollChanged(self): + pass + + class OnTouchModeChangeListener: + def onTouchModeChanged(self, is_in_touch_mode: bool): + pass + + class OnWindowAttachListener: + def onWindowAttached(self): + pass + def onWindowDetached(self): + pass + + class OnWindowFocusChangeListener: + def onWindowFocusChanged(self, has_focus: bool): + pass + + class OnDrawListener: + def onDraw(self): + pass + + class OnPreDrawListener: + def onPreDraw(self) -> bool: + return True + + class OnWindowShownListener: + def onWindowShown(self): + pass + + def __init__(self): + super().__init__() + self._global_layout_listeners: List[ViewTreeObserver.OnGlobalLayoutListener] = [] + self._scroll_changed_listeners: List[ViewTreeObserver.OnScrollChangedListener] = [] + self._pre_draw_listeners: List[ViewTreeObserver.OnPreDrawListener] = [] + self._draw_listeners: List[ViewTreeObserver.OnDrawListener] = [] + self._touch_mode_listeners: List[ViewTreeObserver.OnTouchModeChangeListener] = [] + self._window_focus_listeners: List[ViewTreeObserver.OnWindowFocusChangeListener] = [] + self._window_attach_listeners: List[ViewTreeObserver.OnWindowAttachListener] = [] + self._alive = True + + def addOnGlobalLayoutListener(self, listener: OnGlobalLayoutListener): + self._global_layout_listeners.append(listener) + + def removeOnGlobalLayoutListener(self, listener: OnGlobalLayoutListener): + if listener in self._global_layout_listeners: + self._global_layout_listeners.remove(listener) + + def addOnScrollChangedListener(self, listener: OnScrollChangedListener): + self._scroll_changed_listeners.append(listener) + + def removeOnScrollChangedListener(self, listener: OnScrollChangedListener): + if listener in self._scroll_changed_listeners: + self._scroll_changed_listeners.remove(listener) + + def addOnPreDrawListener(self, listener: OnPreDrawListener): + self._pre_draw_listeners.append(listener) + + def removeOnPreDrawListener(self, listener: OnPreDrawListener): + if listener in self._pre_draw_listeners: + self._pre_draw_listeners.remove(listener) + + def addOnDrawListener(self, listener: OnDrawListener): + self._draw_listeners.append(listener) + + def removeOnDrawListener(self, listener: OnDrawListener): + if listener in self._draw_listeners: + self._draw_listeners.remove(listener) + + def addOnGlobalFocusChangeListener(self, listener: OnGlobalFocusChangeListener): + pass + + def removeOnGlobalFocusChangeListener(self, listener: OnGlobalFocusChangeListener): + pass + + def addOnTouchModeChangeListener(self, listener: OnTouchModeChangeListener): + self._touch_mode_listeners.append(listener) + + def removeOnTouchModeChangeListener(self, listener: OnTouchModeChangeListener): + if listener in self._touch_mode_listeners: + self._touch_mode_listeners.remove(listener) + + def addOnWindowAttachListener(self, listener: OnWindowAttachListener): + self._window_attach_listeners.append(listener) + + def removeOnWindowAttachListener(self, listener: OnWindowAttachListener): + if listener in self._window_attach_listeners: + self._window_attach_listeners.remove(listener) + + def addOnWindowFocusChangeListener(self, listener: OnWindowFocusChangeListener): + self._window_focus_listeners.append(listener) + + def removeOnWindowFocusChangeListener(self, listener: OnWindowFocusChangeListener): + if listener in self._window_focus_listeners: + self._window_focus_listeners.remove(listener) + + def addOnWindowShownListener(self, listener: OnWindowShownListener): + pass + + def removeOnWindowShownListener(self, listener: OnWindowShownListener): + pass + + def dispatchOnGlobalLayout(self): + for listener in list(self._global_layout_listeners): + listener.onGlobalLayout() + + def dispatchOnScrollChanged(self): + for listener in list(self._scroll_changed_listeners): + listener.onScrollChanged() + + def isAlive(self) -> bool: + return self._alive + + def kill(self): + self._alive = False + self._global_layout_listeners.clear() + self._scroll_changed_listeners.clear() + self._pre_draw_listeners.clear() + self._draw_listeners.clear() + + +# ═══════════════════════════════════════════════════════════════ +# ViewConfiguration +# ═══════════════════════════════════════════════════════════════ + +class ViewConfiguration(Stub): + """ViewConfiguration — 视图配置常量""" + + def __init__(self, context=None): + super().__init__() + if context is None: + context = android_context + self._context = context + + @staticmethod + def get(context=None) -> "ViewConfiguration": + return ViewConfiguration(context) + + @staticmethod + def getLongPressTimeout() -> int: + return 500 + + @staticmethod + def getTapTimeout() -> int: + return 100 + + @staticmethod + def getJumpTapTimeout() -> int: + return 500 + + @staticmethod + def getDoubleTapTimeout() -> int: + return 300 + + @staticmethod + def getEdgeSlop() -> int: + return 12 + + @staticmethod + def getTouchSlop() -> int: + return 8 + + @staticmethod + def getScaledTouchSlop(self) -> int: + return 8 + + @staticmethod + def getScaledEdgeSlop(self) -> int: + return 12 + + @staticmethod + def getScaledFadingEdgeLength(self) -> int: + return 12 + + @staticmethod + def getScaledMinimumFlingVelocity(self) -> int: + return 50 + + @staticmethod + def getScaledMaximumFlingVelocity(self) -> int: + return 8000 + + @staticmethod + def getScaledPagingTouchSlop(self) -> int: + return 16 + + @staticmethod + def getScaledOverflingDistance(self) -> int: + return 32 + + @staticmethod + def getScaledOverscrollDistance(self) -> int: + return 32 + + @staticmethod + def getScaledScrollFactor(self) -> int: + return 64 + + @staticmethod + def getScrollFriction() -> float: + return 0.015 + + @staticmethod + def getMaximumDrawingCacheSize() -> int: + return 480 * 800 * 4 + + @staticmethod + def getWindowTouchSlop() -> int: + return 16 + + @staticmethod + def getGlobalActionKeySlop() -> int: + return 12 + + def getScaledWindowTouchSlop(self) -> int: + return 16 + + +# ═══════════════════════════════════════════════════════════════ +# SurfaceView +# ═══════════════════════════════════════════════════════════════ + +class SurfaceView(View): + """SurfaceView — 表面视图,用于直接绘制""" + + def __init__(self, context=None): + super().__init__(context) + self._surface = Surface() + self._surface_holder = SurfaceHolder() + + def getHolder(self) -> "SurfaceHolder": + return self._surface_holder + + def getSurfaceControl(self) -> Any: + return None + + def setVisibility(self, visibility: int): + super().setVisibility(visibility) + + def setSecure(self, is_secure: bool): + pass + + def setZOrderMediaOverlay(self, is_media_overlay: bool): + pass + + def setZOrderOnTop(self, on_top: bool): + pass + + def gatherTransparentRegion(self, region: Any) -> bool: + return False + + def draw(self, canvas: Any): + pass + + +class SurfaceHolder(Stub): + """SurfaceHolder — Surface 持有者""" + + class Callback: + def surfaceCreated(self, holder: "SurfaceHolder"): + pass + def surfaceChanged(self, holder: "SurfaceHolder", format: int, width: int, height: int): + pass + def surfaceDestroyed(self, holder: "SurfaceHolder"): + pass + + class Callback2(Callback): + def surfaceRedrawNeeded(self, holder: "SurfaceHolder"): + pass + + SURFACE_TYPE_NORMAL = 0 + SURFACE_TYPE_HARDWARE = 1 + SURFACE_TYPE_GPU = 2 + SURFACE_TYPE_PUSH_BUFFERS = 3 + + def __init__(self): + super().__init__() + self._surface = Surface() + self._callbacks: List[SurfaceHolder.Callback] = [] + self._type = SurfaceHolder.SURFACE_TYPE_NORMAL + self._fixed_size: Optional[Tuple[int, int]] = None + self._format = -1 + + def getSurface(self) -> "Surface": + return self._surface + + def addCallback(self, callback: Callback): + self._callbacks.append(callback) + + def removeCallback(self, callback: Callback): + if callback in self._callbacks: + self._callbacks.remove(callback) + + def isCreating(self) -> bool: + return False + + def setType(self, type_: int): + self._type = type_ + + def setFixedSize(self, width: int, height: int): + self._fixed_size = (width, height) + + def setSizeFromLayout(self): + self._fixed_size = None + + def setFormat(self, format: int): + self._format = format + + def lockCanvas(self) -> Any: + return None + + def lockCanvas(self, dirty: Any) -> Any: + return None + + def unlockCanvasAndPost(self, canvas: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# TextureView +# ═══════════════════════════════════════════════════════════════ + +class TextureView(View): + """TextureView — 纹理视图""" + + def __init__(self, context=None): + super().__init__(context) + self._surface_texture = SurfaceTexture() + self._layer = None + self._update_listener: Optional[Callable] = None + self._surface_texture_listener: Optional[Callable] = None + + class SurfaceTextureListener: + def onSurfaceTextureAvailable(self, surface: "SurfaceTexture", width: int, height: int): + pass + def onSurfaceTextureSizeChanged(self, surface: "SurfaceTexture", width: int, height: int): + pass + def onSurfaceTextureDestroyed(self, surface: "SurfaceTexture") -> bool: + return True + def onSurfaceTextureUpdated(self, surface: "SurfaceTexture"): + pass + + def isOpaque(self) -> bool: + return False + + def setOpaque(self, opaque: bool): + pass + + def getSurfaceTexture(self) -> "SurfaceTexture": + return self._surface_texture + + def setSurfaceTexture(self, surface_texture: "SurfaceTexture"): + self._surface_texture = surface_texture + + def setSurfaceTextureListener(self, listener: SurfaceTextureListener): + self._surface_texture_listener = listener + + def getSurfaceTextureListener(self) -> Optional[SurfaceTextureListener]: + return self._surface_texture_listener + + def buildLayer(self): + pass + + def setLayerPaint(self, paint: Any): + pass + + def getLayerPaint(self) -> Any: + return None + + def setTransform(self, transform: Any): + pass + + def getTransform(self, transform: Any) -> Any: + return transform + + def draw(self, canvas: Any): + pass + + def lockCanvas(self) -> Any: + return None + + def unlockCanvasAndPost(self, canvas: Any): + pass + + +class SurfaceTexture(Stub): + """SurfaceTexture — 表面纹理""" + + def __init__(self, tex_name: int = 0, allow_legacy: bool = True): + super().__init__() + self._tex_name = tex_name + self._timestamp = 0 + self._frame_available_listener: Optional[Callable] = None + self._default_buffer_size: Optional[Tuple[int, int]] = None + + def setOnFrameAvailableListener(self, listener: Callable, handler: Any = None): + self._frame_available_listener = listener + + def getTimestamp(self) -> int: + return self._timestamp + + def updateTexImage(self): + pass + + def release(self): + pass + + def setDefaultBufferSize(self, width: int, height: int): + self._default_buffer_size = (width, height) + + def getTransformMatrix(self, mtx: List[float]): + for i in range(16): + mtx[i] = 1.0 if i % 5 == 0 else 0.0 + + +# ═══════════════════════════════════════════════════════════════ +# Window +# ═══════════════════════════════════════════════════════════════ + +class Window(Stub): + """Window — 抽象窗口基类""" + + # 特性ID + FEATURES_DEFAULT = -1 + FEATURE_OPTIONS_PANEL = 0 + FEATURE_NO_TITLE = 1 + FEATURE_PROGRESS = 2 + FEATURE_LEFT_ICON = 3 + FEATURE_RIGHT_ICON = 4 + FEATURE_INDETERMINATE_PROGRESS = 5 + FEATURE_CONTEXT_MENU = 6 + FEATURE_CUSTOM_TITLE = 7 + FEATURE_ACTION_BAR = 8 + FEATURE_ACTION_BAR_OVERLAY = 9 + FEATURE_ACTION_MODE_OVERLAY = 10 + FEATURE_SWIPE_TO_DISMISS = 11 + FEATURE_CONTENT_TRANSITIONS = 12 + FEATURE_WATCH = 13 + + # 本地特性 + ID_ANDROID_CONTENT = 0x01020002 + + DECOR_CAPTION_SHORTCUT_DURATION_MS = 1000 + + # 标志 + NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME = "navigationBarBackground" + STATUS_BAR_BACKGROUND_TRANSITION_NAME = "statusBarBackground" + + def __init__(self, context=None): + super().__init__() + if context is None: + context = android_context + self._context = context + self._features: Dict[int, bool] = {} + self._title: str = "" + self._decor_view: Optional[View] = None + self._callback: Optional["Window.Callback"] = None + self._window_manager: Optional[WindowManager] = None + self._soft_input_mode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED + self._flags: Dict[int, bool] = {} + self._container: Optional[Window] = None + self._local_features: List[int] = [] + + class Callback: + def dispatchKeyEvent(self, event: KeyEvent) -> bool: + return False + def dispatchTouchEvent(self, event: MotionEvent) -> bool: + return False + def dispatchTrackballEvent(self, event: MotionEvent) -> bool: + return False + def dispatchGenericMotionEvent(self, event: MotionEvent) -> bool: + return False + def dispatchPopulateAccessibilityEvent(self, event: Any) -> bool: + return False + def onCreatePanelView(self, feature_id: int) -> Optional[View]: + return None + def onCreatePanelMenu(self, feature_id: int, menu: Any) -> bool: + return False + def onPreparePanel(self, feature_id: int, view: View, menu: Any) -> bool: + return False + def onMenuOpened(self, feature_id: int, menu: Any): + pass + def onMenuItemSelected(self, feature_id: int, item: Any) -> bool: + return False + def onWindowAttributesChanged(self, attrs: Any): + pass + def onContentChanged(self): + pass + def onWindowFocusChanged(self, has_focus: bool): + pass + def onAttachedToWindow(self): + pass + def onDetachedFromWindow(self): + pass + def onPanelClosed(self, feature_id: int, menu: Any): + pass + def onSearchRequested(self) -> bool: + return False + def onSearchRequested(self, search_event: Any) -> bool: + return False + def onActionModeStarted(self, mode: Any): + pass + def onActionModeFinished(self, mode: Any): + pass + def onWindowStartingActionMode(self, callback: Any) -> Optional[Any]: + return None + def onWindowStartingActionMode(self, callback: Any, type_: int) -> Optional[Any]: + return None + + def setContentView(self, layout_res_id: int): + pass + + def setContentView(self, view: View): + pass + + def setContentView(self, view: View, params: ViewGroup.LayoutParams): + pass + + def addContentView(self, view: View, params: ViewGroup.LayoutParams): + pass + + def setTitle(self, title: str): + self._title = title + + def getTitle(self) -> str: + return self._title + + def setFeatureInt(self, feature_id: int, value: int): + self._features[feature_id] = True + + def requestFeature(self, feature_id: int) -> bool: + self._features[feature_id] = True + return True + + def hasFeature(self, feature_id: int) -> bool: + return self._features.get(feature_id, False) + + def setFlags(self, flags: int, mask: int): + for flag_bit in range(32): + if mask & (1 << flag_bit): + self._flags[1 << flag_bit] = bool(flags & (1 << flag_bit)) + + def addFlags(self, flags: int): + for flag_bit in range(32): + if flags & (1 << flag_bit): + self._flags[1 << flag_bit] = True + + def clearFlags(self, flags: int): + for flag_bit in range(32): + if flags & (1 << flag_bit): + self._flags[1 << flag_bit] = False + + def setSoftInputMode(self, soft_input_mode: int): + self._soft_input_mode = soft_input_mode + + def getSoftInputMode(self) -> int: + return self._soft_input_mode + + def setLayout(self, width: int, height: int): + pass + + def findViewById(self, id_: int) -> Optional[View]: + if self._decor_view: + return self._decor_view.findViewById(id_) + return None + + def getDecorView(self) -> Optional[View]: + return self._decor_view + + def setDecorView(self, view: View): + self._decor_view = view + + def getWindowManager(self) -> Optional[WindowManager]: + return self._window_manager + + def setWindowManager(self, wm: WindowManager, app_token: Any = None, app_name: str = ""): + self._window_manager = wm + + def setCallback(self, callback: Callback): + self._callback = callback + + def getCallback(self) -> Optional[Callback]: + return self._callback + + def superDispatchTouchEvent(self, event: MotionEvent) -> bool: + if self._decor_view: + return self._decor_view.dispatchTouchEvent(event) + return False + + def superDispatchKeyEvent(self, event: KeyEvent) -> bool: + if self._decor_view: + return self._decor_view.dispatchKeyEvent(event) + return False + + def superDispatchTrackballEvent(self, event: MotionEvent) -> bool: + return False + + def superDispatchGenericMotionEvent(self, event: MotionEvent) -> bool: + return False + + def takeSurface(self, callback: Any): + pass + + def takeInputQueue(self, callback: Any): + pass + + def isActive(self) -> bool: + return True + + def isFloating(self) -> bool: + return False + + def setBackgroundDrawable(self, drawable: Any): + pass + + def setBackgroundDrawableResource(self, res_id: int): + pass + + def setDimAmount(self, amount: float): + pass + + def getLayoutInflater(self): + return None + + def setWindowAnimations(self, res_id: int): + pass + + def getWindowStyle(self) -> Any: + return None + + def setContainer(self, container: "Window"): + self._container = container + + def getContainer(self) -> Optional["Window"]: + return self._container + + def setLocalFeatures(self, local_features: int): + pass + + def makeActive(self): + pass + + def getContext(self): + return self._context + + def getAttributes(self) -> WindowManager.LayoutParams: + params = WindowManager.LayoutParams() + params.softInputMode = self._soft_input_mode + return params + + def setAttributes(self, attrs: WindowManager.LayoutParams): + pass + + def reconfigure(self, attrs: WindowManager.LayoutParams): + pass + + def onConfigurationChanged(self, new_config: Any): + pass + + def getCurrentFocus(self) -> Optional[View]: + return None + + def getVolumeControlStream(self) -> int: + return -1 + + def setVolumeControlStream(self, stream_type: int): + pass + + def getMediaController(self) -> Any: + return None + + def setMediaController(self, controller: Any): + pass + + def openGlEsVersion(self) -> int: + return 2 + + def isShortcutKey(self, key_code: int, event: KeyEvent) -> bool: + return False + + def setStatusBarColor(self, color: int): + pass + + def setNavigationBarColor(self, color: int): + pass + + def setStatusBarVisibility(self, visible: bool): + pass + + def setNavigationBarVisibility(self, visible: bool): + pass + + def getStatusBarColor(self) -> int: + return 0 + + def getNavigationBarColor(self) -> int: + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# WindowInsets +# ═══════════════════════════════════════════════════════════════ + +class WindowInsets(Stub): + """WindowInsets — 窗口插入区域""" + + def __init__(self, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0): + super().__init__() + self._system_window_insets = Rect(left, top, right, bottom) + self._stable_insets = Rect(0, 0, 0, 0) + self._display_cutout_insets = Rect(0, 0, 0, 0) + self._consumed = False + + def getSystemWindowInsetLeft(self) -> int: + return self._system_window_insets.left + + def getSystemWindowInsetTop(self) -> int: + return self._system_window_insets.top + + def getSystemWindowInsetRight(self) -> int: + return self._system_window_insets.right + + def getSystemWindowInsetBottom(self) -> int: + return self._system_window_insets.bottom + + def getSystemWindowInsets(self) -> Rect: + return self._system_window_insets + + def getStableInsetTop(self) -> int: + return self._stable_insets.top + + def getStableInsetLeft(self) -> int: + return self._stable_insets.left + + def getStableInsetRight(self) -> int: + return self._stable_insets.right + + def getStableInsetBottom(self) -> int: + return self._stable_insets.bottom + + def getDisplayCutoutInsets(self) -> Rect: + return self._display_cutout_insets + + def consumeDisplayCutout(self) -> "WindowInsets": + return WindowInsets() + + def consumeSystemWindowInsets(self) -> "WindowInsets": + return WindowInsets() + + def replaceSystemWindowInsets(self, left: int, top: int, right: int, bottom: int) -> "WindowInsets": + new_ins = WindowInsets(left, top, right, bottom) + new_ins._stable_insets = self._stable_insets + return new_ins + + def isConsumed(self) -> bool: + return self._consumed + + def hasSystemWindowInsets(self) -> bool: + return (self._system_window_insets.left > 0 or self._system_window_insets.top > 0 or + self._system_window_insets.right > 0 or self._system_window_insets.bottom > 0) + + def hasInsets(self) -> bool: + return self.hasSystemWindowInsets() + + def isRound(self) -> bool: + return False + + def getDisplayCutout(self) -> Any: + return None + + def consumeStableInsets(self) -> "WindowInsets": + return WindowInsets() + + def toString(self) -> str: + return f"WindowInsets(system={self._system_window_insets})" + + +# ═══════════════════════════════════════════════════════════════ +# WindowInsetsController +# ═══════════════════════════════════════════════════════════════ + +class WindowInsetsController(Stub): + """WindowInsetsController — 窗口插入控制器""" + + BEHAVIOR_SHOW_BARS_BY_TOUCH = 0 + BEHAVIOR_SHOW_BARS_BY_SWIPE = 1 + BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2 + + APPEARANCE_OPAQUE_STATUS_BARS = 0x00000001 + APPEARANCE_OPAQUE_NAVIGATION_BARS = 0x00000002 + APPEARANCE_LOW_PROFILE_BARS = 0x00000004 + APPEARANCE_LIGHT_STATUS_BARS = 0x00000008 + APPEARANCE_LIGHT_NAVIGATION_BARS = 0x00000010 + + def __init__(self): + super().__init__() + self._appearance = 0 + self._behavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_TOUCH + + def show(self, types: int): + pass + + def hide(self, types: int): + pass + + def setSystemBarsAppearance(self, appearance: int, mask: int): + self._appearance = (self._appearance & ~mask) | (appearance & mask) + + def getSystemBarsAppearance(self) -> int: + return self._appearance + + def setSystemBarsBehavior(self, behavior: int): + self._behavior = behavior + + def getSystemBarsBehavior(self) -> int: + return self._behavior + + def setAnimationsDisabled(self, disabled: bool): + pass + + +# ═══════════════════════════════════════════════════════════════ +# Display +# ═══════════════════════════════════════════════════════════════ + +class Display(Stub): + """Display — 显示信息""" + + DEFAULT_DISPLAY = 0 + + ROTATION_0 = 0 + ROTATION_90 = 1 + ROTATION_180 = 2 + ROTATION_270 = 3 + + FLAG_PRESENTATION = 0x00000008 + FLAG_PRIVATE = 0x00000004 + FLAG_SECURE = 0x00000002 + FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001 + STATE_DOZE = 3 + STATE_DOZE_SUSPEND = 4 + STATE_OFF = 1 + STATE_ON = 2 + STATE_UNKNOWN = 0 + + def __init__(self, display_id: int = DEFAULT_DISPLAY): + super().__init__() + self._display_id = display_id + self._width = 1080 + self._height = 1920 + self._rotation = Display.ROTATION_0 + self._refresh_rate = 60.0 + self._density = 2.0 + self._density_dpi = 320 + self._state = Display.STATE_ON + self._flags = 0 + self._name = "Built-in Screen" + + def getDisplayId(self) -> int: + return self._display_id + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def getRotation(self) -> int: + return self._rotation + + def getRefreshRate(self) -> float: + return self._refresh_rate + + def getName(self) -> str: + return self._name + + def getState(self) -> int: + return self._state + + def getFlags(self) -> int: + return self._flags + + def isHdr(self) -> bool: + return False + + def isWideColorGamut(self) -> bool: + return False + + def getSize(self, out_size: Any = None): + if out_size is not None: + out_size.width = self._width + out_size.height = self._height + return None + + def getRealSize(self, out_size: Any = None): + if out_size is not None: + out_size.width = self._width + out_size.height = self._height + return None + + def getMetrics(self, metrics: Any = None): + if metrics is not None: + metrics.widthPixels = self._width + metrics.heightPixels = self._height + metrics.density = self._density + metrics.densityDpi = self._density_dpi + metrics.scaledDensity = self._density + metrics.xdpi = 320.0 + metrics.ydpi = 320.0 + return None + + def getRealMetrics(self, metrics: Any = None): + return self.getMetrics(metrics) + + def getOrientation(self) -> int: + return self._rotation + + def getDisplayAdjustments(self) -> Any: + return None + + def getPixelFormat(self) -> int: + return 1 # RGBA_8888 + + def isValid(self) -> bool: + return True + + def toString(self) -> str: + return f"Display({self._display_id}, {self._width}x{self._height})" + + +# ═══════════════════════════════════════════════════════════════ +# Surface +# ═══════════════════════════════════════════════════════════════ + +class Surface(Stub): + """Surface — 表面,用于绘制""" + + ROTATION_0 = 0 + ROTATION_90 = 1 + ROTATION_180 = 2 + ROTATION_270 = 3 + + CHANGE_FRAME_RATE_ALWAYS = 1 + CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS = 0 + + def __init__(self, surface_control: Any = None): + super().__init__() + self._is_valid = True + self._is_locked = False + self._width = 1080 + self._height = 1920 + self._hardware_accelerated = False + + def isValid(self) -> bool: + return self._is_valid + + def isConsumerRunningBehind(self) -> bool: + return False + + def lockCanvas(self, dirty_rect: Any = None) -> Any: + if not self._is_valid: + return None + self._is_locked = True + return None # 返回空 Canvas + + def lockHardwareCanvas(self) -> Any: + if not self._is_valid: + return None + self._is_locked = True + self._hardware_accelerated = True + return None + + def unlockCanvasAndPost(self, canvas: Any): + self._is_locked = False + + def getSurfaceControl(self) -> Any: + return None + + def release(self): + self._is_valid = False + self._is_locked = False + + def destroy(self): + self.release() + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def setFrameRate(self, frame_rate: float, compatibility: int): + pass + + def toString(self) -> str: + return f"Surface(valid={self._is_valid}, {self._width}x{self._height})" + + +# ═══════════════════════════════════════════════════════════════ +# Choreographer +# ═══════════════════════════════════════════════════════════════ + +class Choreographer(Stub): + """Choreographer — 帧渲染协调器""" + + CALLBACK_INPUT = 0 + CALLBACK_ANIMATION = 1 + CALLBACK_INSETS_ANIMATION = 2 + CALLBACK_TRAVERSAL = 3 + CALLBACK_COMMIT = 4 + + _instance: Optional["Choreographer"] = None + + class FrameCallback: + def doFrame(self, frame_time_nanos: int): + pass + + class FrameTimeline: + pass + + def __init__(self): + super().__init__() + + @staticmethod + def getInstance() -> "Choreographer": + if Choreographer._instance is None: + Choreographer._instance = Choreographer() + return Choreographer._instance + + def postFrameCallback(self, callback: "FrameCallback"): + callback.doFrame(int(time.time() * 1_000_000_000)) + + def postFrameCallbackDelayed(self, callback: "FrameCallback", delay_millis: int): + callback.doFrame(int((time.time() + delay_millis / 1000.0) * 1_000_000_000)) + + def removeFrameCallback(self, callback: "FrameCallback"): + pass + + def postCallback(self, callback_type: int, action: Callable, token: Any): + action() + + def postCallbackDelayed(self, callback_type: int, action: Callable, token: Any, delay_millis: int): + import threading + timer = threading.Timer(delay_millis / 1000.0, action) + timer.daemon = True + timer.start() + + def removeCallbacks(self, callback_type: int, action: Callable, token: Any): + pass + + def getFrameTimeNanos(self) -> int: + return int(time.time() * 1_000_000_000) + + def getLastFrameTimeNanos(self) -> int: + return int(time.time() * 1_000_000_000) - 16_666_666 + + +# ═══════════════════════════════════════════════════════════════ +# ActionMode +# ═══════════════════════════════════════════════════════════════ + +class ActionMode(Stub): + """ActionMode — 操作模式""" + + TYPE_FLOATING = 1 + TYPE_PRIMARY = 0 + TYPE_TOOLBAR = 2 + + class Callback: + def onCreateActionMode(self, mode: "ActionMode", menu: Any) -> bool: + return False + def onPrepareActionMode(self, mode: "ActionMode", menu: Any) -> bool: + return False + def onActionItemClicked(self, mode: "ActionMode", item: Any) -> bool: + return False + def onDestroyActionMode(self, mode: "ActionMode"): + pass + + class Callback2(Callback): + def onGetContentRect(self, mode: "ActionMode", view: View, out_rect: Any): + pass + + def __init__(self): + super().__init__() + self._tag: Any = None + self._title: str = "" + self._subtitle: str = "" + self._title_optional_hint: str = "" + self._subtitle_optional_hint: str = "" + self._type = ActionMode.TYPE_PRIMARY + self._menu: Any = None + + def setTitle(self, title: str): + self._title = title + + def getTitle(self) -> str: + return self._title + + def setSubtitle(self, subtitle: str): + self._subtitle = subtitle + + def getSubtitle(self) -> str: + return self._subtitle + + def setTitleOptionalHint(self, hint: bool): + self._title_optional_hint = hint + + def getTitleOptionalHint(self) -> bool: + return bool(self._title_optional_hint) + + def setTag(self, tag: Any): + self._tag = tag + + def getTag(self) -> Any: + return self._tag + + def getMenu(self): + return self._menu + + def setType(self, type_: int): + self._type = type_ + + def getType(self) -> int: + return self._type + + def invalidate(self): + pass + + def invalidateContentRect(self): + pass + + def finish(self): + pass + + def getCustomView(self) -> Optional[View]: + return None + + def setCustomView(self, view: View): + pass + + def setImageDrawable(self, drawable: Any): + pass + + def getMenuInflater(self): + return None + + +# ═══════════════════════════════════════════════════════════════ +# ContextThemeWrapper +# ═══════════════════════════════════════════════════════════════ + +class ContextThemeWrapper(Stub): + """ContextThemeWrapper — 上下文主题包装器""" + + def __init__(self, base: Any = None, theme_res_id: int = 0): + super().__init__() + self._base = base if base is not None else android_context + self._theme_res_id = theme_res_id + self._theme: Any = None + + def attachBaseContext(self, new_base: Any): + self._base = new_base + + def getBaseContext(self): + return self._base + + def setTheme(self, resid: int): + self._theme_res_id = resid + + def getThemeResId(self) -> int: + return self._theme_res_id + + def getTheme(self) -> Any: + return self._theme + + def getResources(self): + if hasattr(self._base, "getResources"): + return self._base.getResources() + return None + + def getSystemService(self, name: str) -> Any: + if hasattr(self._base, "getSystemService"): + return self._base.getSystemService(name) + return None + + def getPackageName(self) -> str: + if hasattr(self._base, "getPackageName"): + return self._base.getPackageName() + return "" + + def getApplicationContext(self): + return self._base + + +# ═══════════════════════════════════════════════════════════════ +# ViewOutlineProvider +# ═══════════════════════════════════════════════════════════════ + +class ViewOutlineProvider(Stub): + """ViewOutlineProvider — 视图轮廓提供者""" + + BACKGROUND = None + BOUNDS = None + PADDED_BOUNDS = None + + def getOutline(self, view: View, outline: Any): + pass + + # 预定义实例 + @staticmethod + def _init_constants(): + if ViewOutlineProvider.BACKGROUND is None: + class _Background(ViewOutlineProvider): + def getOutline(self, view, outline): + if hasattr(outline, "setRect"): + outline.setRect(0, 0, view.getWidth(), view.getHeight()) + ViewOutlineProvider.BACKGROUND = _Background() + + if ViewOutlineProvider.BOUNDS is None: + class _Bounds(ViewOutlineProvider): + def getOutline(self, view, outline): + if hasattr(outline, "setRect"): + outline.setRect(0, 0, view.getWidth(), view.getHeight()) + ViewOutlineProvider.BOUNDS = _Bounds() + + if ViewOutlineProvider.PADDED_BOUNDS is None: + class _PaddedBounds(ViewOutlineProvider): + def getOutline(self, view, outline): + if hasattr(outline, "setRect"): + outline.setRect( + view.getPaddingLeft(), view.getPaddingTop(), + view.getWidth() - view.getPaddingRight(), + view.getHeight() - view.getPaddingBottom(), + ) + ViewOutlineProvider.PADDED_BOUNDS = _PaddedBounds() + + +ViewOutlineProvider._init_constants() + +# ═══════════════════════════════════════════════════════════════ +# RenderNode +# ═══════════════════════════════════════════════════════════════ + +class RenderNode(Stub): + """RenderNode — 渲染节点""" + + STATUS_DISPLAY_LIST_INFLATED = 1 + STATUS_DISPLAY_LIST_READY = 2 + STATUS_DISPLAY_LIST_PREPARE = 3 + STATUS_DISPLAY_LIST_UNUSED = 0 + + def __init__(self, name: str = ""): + super().__init__() + self._name = name + self._alpha = 1.0 + self._translation_x = 0.0 + self._translation_y = 0.0 + self._translation_z = 0.0 + self._elevation = 0.0 + self._rotation = 0.0 + self._rotation_x = 0.0 + self._rotation_y = 0.0 + self._scale_x = 1.0 + self._scale_y = 1.0 + self._pivot_x = 0.0 + self._pivot_y = 0.0 + self._left = 0 + self._top = 0 + self._right = 0 + self._bottom = 0 + self._clip_to_bounds = False + self._clip_bounds: Optional[Rect] = None + self._has_display_list = False + self._status = RenderNode.STATUS_DISPLAY_LIST_UNUSED + + def setPosition(self, left: int, top: int, right: int, bottom: int) -> bool: + old_left, old_top, old_right, old_bottom = self._left, self._top, self._right, self._bottom + self._left, self._top, self._right, self._bottom = left, top, right, bottom + return (old_left != left or old_top != top or old_right != right or old_bottom != bottom) + + def getLeft(self) -> int: + return self._left + def getTop(self) -> int: + return self._top + def getRight(self) -> int: + return self._right + def getBottom(self) -> int: + return self._bottom + + def setAlpha(self, alpha: float) -> bool: + changed = self._alpha != alpha + self._alpha = alpha + return changed + + def getAlpha(self) -> float: + return self._alpha + + def setTranslationX(self, tx: float) -> bool: + changed = self._translation_x != tx + self._translation_x = tx + return changed + + def getTranslationX(self) -> float: + return self._translation_x + + def setTranslationY(self, ty: float) -> bool: + changed = self._translation_y != ty + self._translation_y = ty + return changed + + def getTranslationY(self) -> float: + return self._translation_y + + def setElevation(self, elevation: float) -> bool: + changed = self._elevation != elevation + self._elevation = elevation + return changed + + def getElevation(self) -> float: + return self._elevation + + def setRotation(self, rotation: float) -> bool: + changed = self._rotation != rotation + self._rotation = rotation + return changed + + def getRotation(self) -> float: + return self._rotation + + def setScaleX(self, scale_x: float) -> bool: + changed = self._scale_x != scale_x + self._scale_x = scale_x + return changed + + def getScaleX(self) -> float: + return self._scale_x + + def setScaleY(self, scale_y: float) -> bool: + changed = self._scale_y != scale_y + self._scale_y = scale_y + return changed + + def getScaleY(self) -> float: + return self._scale_y + + def setPivotX(self, pivot_x: float) -> bool: + changed = self._pivot_x != pivot_x + self._pivot_x = pivot_x + return changed + + def getPivotX(self) -> float: + return self._pivot_x + + def setPivotY(self, pivot_y: float) -> bool: + changed = self._pivot_y != pivot_y + self._pivot_y = pivot_y + return changed + + def getPivotY(self) -> float: + return self._pivot_y + + def setClipToBounds(self, clip_to_bounds: bool) -> bool: + changed = self._clip_to_bounds != clip_to_bounds + self._clip_to_bounds = clip_to_bounds + return changed + + def getClipToBounds(self) -> bool: + return self._clip_to_bounds + + def setClipBounds(self, clip_rect: Optional[Rect]): + self._clip_bounds = clip_rect + + def getClipBounds(self) -> Optional[Rect]: + return self._clip_bounds + + def setHasDisplayList(self, has_display_list: bool): + self._has_display_list = has_display_list + + def hasDisplayList(self) -> bool: + return self._has_display_list + + def getName(self) -> str: + return self._name + + def discardDisplayList(self): + self._has_display_list = False + self._status = RenderNode.STATUS_DISPLAY_LIST_UNUSED + + def start(self) -> Any: + return None + + def end(self): + pass + + def offsetLeftAndRight(self, offset: int): + self._left += offset + self._right += offset + + def offsetTopAndBottom(self, offset: int): + self._top += offset + self._bottom += offset + + def getMatrix(self, out_matrix: Any): + pass + + def getInverseMatrix(self, out_matrix: Any): + pass + + def useLighting(self) -> bool: + return False + + def setUseLighting(self, use_lighting: bool): + pass + + def setLayerType(self, layer_type: int): + pass + + def getLayerType(self) -> int: + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# ScrollCaptureCallback +# ═══════════════════════════════════════════════════════════════ + +class ScrollCaptureCallback(Stub): + """ScrollCaptureCallback — 滚动捕获回调""" + + def onScrollCaptureSearch(self, l: int, t: int, r: int, b: int) -> Optional[Any]: + return None + + def onScrollCaptureStart(self, session: Any) -> Optional[Any]: + return None + + def onScrollCaptureImageRequest(self, session: Any, request: Any) -> int: + return 0 + + def onScrollCaptureEnd(self, session: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# KeyboardShortcutGroup +# ═══════════════════════════════════════════════════════════════ + +class KeyboardShortcutGroup(Stub): + """KeyboardShortcutGroup — 键盘快捷键分组""" + + def __init__(self, label: str, items: Optional[List[Any]] = None): + super().__init__() + self._label = label + self._items: List[Any] = items if items is not None else [] + self._system_group = False + + def getLabel(self) -> str: + return self._label + + def getItems(self) -> List[Any]: + return list(self._items) + + def addItem(self, item: Any): + self._items.append(item) + + def setSystemGroup(self, is_system: bool): + self._system_group = is_system + + def isSystemGroup(self) -> bool: + return self._system_group + + +# ═══════════════════════════════════════════════════════════════ +# SoundEffectConstants +# ═══════════════════════════════════════════════════════════════ + +class SoundEffectConstants: + """SoundEffectConstants — 音效常量""" + + CLICK = 0 + NAVIGATION_LEFT = 1 + NAVIGATION_UP = 2 + NAVIGATION_RIGHT = 3 + NAVIGATION_DOWN = 4 + NAVIGATION_REPEAT_LEFT = 5 + NAVIGATION_REPEAT_UP = 6 + NAVIGATION_REPEAT_RIGHT = 7 + NAVIGATION_REPEAT_DOWN = 8 + + @staticmethod + def getDirectionForNavigation(gravity: int) -> int: + return SoundEffectConstants.NAVIGATION_UP + + +# ═══════════════════════════════════════════════════════════════ +# FocusFinder +# ═══════════════════════════════════════════════════════════════ + +class FocusFinder(Stub): + """FocusFinder — 焦点查找器""" + + _instance: Optional["FocusFinder"] = None + + def __init__(self): + super().__init__() + + @staticmethod + def getInstance() -> "FocusFinder": + if FocusFinder._instance is None: + FocusFinder._instance = FocusFinder() + return FocusFinder._instance + + def findNextFocus(self, root: ViewGroup, focused: View, direction: int) -> Optional[View]: + return None + + def findNearestTouchable(self, root: ViewGroup, x: int, y: int, direction: int, + deltas: List[int]) -> Optional[View]: + return None + + +# ═══════════════════════════════════════════════════════════════ +# ViewGroup override for children list +# ═══════════════════════════════════════════════════════════════ + +# Ensure ViewGroup._children is separate from View._children +ViewGroup._children = [] # Will be overridden in __init__ + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "View", + "ViewGroup", + "ViewParent", + "ViewManager", + "WindowManager", + "MotionEvent", + "KeyEvent", + "GestureDetector", + "ScaleGestureDetector", + "VelocityTracker", + "DragEvent", + "AccessibilityDelegate", + "ViewTreeObserver", + "ViewConfiguration", + "SurfaceView", + "TextureView", + "Window", + "WindowInsets", + "WindowInsetsController", + "Display", + "Surface", + "Choreographer", + "ActionMode", + "ContextThemeWrapper", + "Gravity", + "ViewOutlineProvider", + "RenderNode", + "ScrollCaptureCallback", + "KeyboardShortcutGroup", + "MeasureSpec", + "ViewPropertyAnimator", + "SurfaceHolder", + "SurfaceTexture", + "FocusFinder", + "SoundEffectConstants", + "Rect", +] \ No newline at end of file diff --git a/android_pyapi/android/view_accessibility.py b/android_pyapi/android/view_accessibility.py new file mode 100644 index 0000000..7e306bc --- /dev/null +++ b/android_pyapi/android/view_accessibility.py @@ -0,0 +1,1488 @@ +""" +Android View Accessibility 组件 — AccessibilityEvent, AccessibilityNodeInfo, +AccessibilityService, AccessibilityManager 等 +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError, android_context + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityEvent +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityEvent(Stub): + """AccessibilityEvent — 无障碍事件""" + + # 事件类型 + TYPE_VIEW_CLICKED = 0x00000001 + TYPE_VIEW_LONG_CLICKED = 0x00000002 + TYPE_VIEW_SELECTED = 0x00000004 + TYPE_VIEW_FOCUSED = 0x00000008 + TYPE_VIEW_TEXT_CHANGED = 0x00000010 + TYPE_WINDOW_STATE_CHANGED = 0x00000020 + TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040 + TYPE_VIEW_HOVER_ENTER = 0x00000080 + TYPE_VIEW_HOVER_EXIT = 0x00000100 + TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200 + TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400 + TYPE_WINDOW_CONTENT_CHANGED = 0x00000800 + TYPE_VIEW_SCROLLED = 0x00001000 + TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000 + TYPE_ANNOUNCEMENT = 0x00004000 + TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000 + TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000 + TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000 + TYPE_GESTURE_DETECTION_START = 0x00040000 + TYPE_GESTURE_DETECTION_END = 0x00080000 + TYPE_TOUCH_INTERACTION_START = 0x00100000 + TYPE_TOUCH_INTERACTION_END = 0x00200000 + TYPE_WINDOWS_CHANGED = 0x00400000 + TYPE_VIEW_CONTEXT_CLICKED = 0x00800000 + TYPE_ASSIST_READING_CONTENT = 0x01000000 + + # 内容变化类型 + CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000001 + CONTENT_CHANGE_TYPE_STATE_DESCRIPTION = 0x00000002 + CONTENT_CHANGE_TYPE_SUBTREE = 0x00000004 + CONTENT_CHANGE_TYPE_TEXT = 0x00000010 + CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000 + CONTENT_CHANGE_TYPE_PANE_TITLE = 0x00000020 + CONTENT_CHANGE_TYPE_DRAG_STARTED = 0x00000040 + CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 0x00000080 + CONTENT_CHANGE_TYPE_DRAG_DROPPED = 0x00000100 + CONTENT_CHANGE_TYPE_CLEAR_FOCUS = 0x00000200 + CONTENT_CHANGE_TYPE_ADDED = 0x00000400 + CONTENT_CHANGE_TYPE_REMOVED = 0x00000800 + CONTENT_CHANGE_TYPE_VISIBILITY_CHANGED = 0x00001000 + CONTENT_CHANGE_TYPE_PANE_APPEARED = 0x00002000 + CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 0x00004000 + CONTENT_CHANGE_TYPE_ERROR = 0x00008000 + + # 窗口变化类型 + WINDOWS_CHANGE_ADDED = 0x00000001 + WINDOWS_CHANGE_REMOVED = 0x00000002 + WINDOWS_CHANGE_TITLE = 0x00000004 + WINDOWS_CHANGE_BOUNDS = 0x00000008 + WINDOWS_CHANGE_LAYER = 0x00000010 + WINDOWS_CHANGE_ACTIVE = 0x00000020 + WINDOWS_CHANGE_FOCUSED = 0x00000040 + WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED = 0x00000080 + WINDOWS_CHANGE_PARENT = 0x00000100 + WINDOWS_CHANGE_CHILDREN = 0x00000200 + WINDOWS_CHANGE_PIP = 0x00000400 + + MAX_TEXT_LENGTH = 500 + + # 预留回收池 + _pool: List["AccessibilityEvent"] = [] + _pool_max = 10 + + def __init__(self): + super().__init__() + self._event_type: int = 0 + self._source: Optional[Any] = None + self._source_node_id: int = -1 + self._class_name: Optional[str] = None + self._package_name: Optional[str] = None + self._content_description: Optional[str] = None + self._text: List[str] = [] + self._action: int = 0 + self._from_index: int = -1 + self._to_index: int = -1 + self._item_count: int = 0 + self._current_item_index: int = 0 + self._enabled: bool = True + self._password: bool = False + self._checked: bool = False + self._full_package_name: Optional[str] = None + self._before_text: Optional[str] = None + self._movement_granularity: int = 0 + self._added_count: int = 0 + self._removed_count: int = 0 + self._content_change_types: int = 0 + self._scroll_delta_x: float = 0.0 + self._scroll_delta_y: float = 0.0 + self._max_scroll_x: float = 0.0 + self._max_scroll_y: float = 0.0 + self._event_time: int = 0 + self._parcelable_data: Dict[str, Any] = {} + self._record_count: int = 0 + + @staticmethod + def obtain(event: Optional["AccessibilityEvent"] = None) -> "AccessibilityEvent": + if AccessibilityEvent._pool: + result = AccessibilityEvent._pool.pop() + else: + result = AccessibilityEvent() + if event is not None: + result._event_type = event._event_type + result._source = event._source + result._class_name = event._class_name + result._package_name = event._package_name + result._content_description = event._content_description + result._text = list(event._text) + result._action = event._action + return result + + def recycle(self): + self._event_type = 0 + self._source = None + self._source_node_id = -1 + self._class_name = None + self._package_name = None + self._content_description = None + self._text.clear() + self._action = 0 + self._from_index = -1 + self._to_index = -1 + self._item_count = 0 + if len(AccessibilityEvent._pool) < AccessibilityEvent._pool_max: + AccessibilityEvent._pool.append(self) + + def getEventType(self) -> int: + return self._event_type + + def setEventType(self, event_type: int): + self._event_type = event_type + + def getSource(self) -> Optional[Any]: + return self._source + + def setSource(self, source: Optional[Any]): + self._source = source + + def getClassName(self) -> Optional[str]: + return self._class_name + + def setClassName(self, class_name: Optional[str]): + self._class_name = class_name + + def getPackageName(self) -> Optional[str]: + return self._package_name + + def setPackageName(self, package_name: Optional[str]): + self._package_name = package_name + + def getContentDescription(self) -> Optional[str]: + return self._content_description + + def setContentDescription(self, content_description: Optional[str]): + self._content_description = content_description + + def getText(self) -> List[str]: + return list(self._text) + + def getAction(self) -> int: + return self._action + + def setAction(self, action: int): + self._action = action + + def setFromIndex(self, from_index: int): + self._from_index = from_index + + def getFromIndex(self) -> int: + return self._from_index + + def setToIndex(self, to_index: int): + self._to_index = to_index + + def getToIndex(self) -> int: + return self._to_index + + def setItemCount(self, item_count: int): + self._item_count = item_count + + def getItemCount(self) -> int: + return self._item_count + + def setCurrentItemIndex(self, index: int): + self._current_item_index = index + + def getCurrentItemIndex(self) -> int: + return self._current_item_index + + def isEnabled(self) -> bool: + return self._enabled + + def setEnabled(self, enabled: bool): + self._enabled = enabled + + def isPassword(self) -> bool: + return self._password + + def setPassword(self, password: bool): + self._password = password + + def isChecked(self) -> bool: + return self._checked + + def setChecked(self, checked: bool): + self._checked = checked + + def getBeforeText(self) -> Optional[str]: + return self._before_text + + def setBeforeText(self, before_text: Optional[str]): + self._before_text = before_text + + def getMovementGranularity(self) -> int: + return self._movement_granularity + + def setMovementGranularity(self, granularity: int): + self._movement_granularity = granularity + + def getAddedCount(self) -> int: + return self._added_count + + def setAddedCount(self, added_count: int): + self._added_count = added_count + + def getRemovedCount(self) -> int: + return self._removed_count + + def setRemovedCount(self, removed_count: int): + self._removed_count = removed_count + + def getContentChangeTypes(self) -> int: + return self._content_change_types + + def setContentChangeTypes(self, change_types: int): + self._content_change_types = change_types + + def getScrollDeltaX(self) -> float: + return self._scroll_delta_x + + def setScrollDeltaX(self, delta_x: float): + self._scroll_delta_x = delta_x + + def getScrollDeltaY(self) -> float: + return self._scroll_delta_y + + def setScrollDeltaY(self, delta_y: float): + self._scroll_delta_y = delta_y + + def getMaxScrollX(self) -> float: + return self._max_scroll_x + + def setMaxScrollX(self, max_x: float): + self._max_scroll_x = max_x + + def getMaxScrollY(self) -> float: + return self._max_scroll_y + + def setMaxScrollY(self, max_y: float): + self._max_scroll_y = max_y + + def getEventTime(self) -> int: + return self._event_time + + def setEventTime(self, event_time: int): + self._event_time = event_time + + def getParcelableData(self) -> Any: + return self._parcelable_data + + def setParcelableData(self, data: Any): + if isinstance(data, dict): + self._parcelable_data = data + else: + self._parcelable_data = {"data": data} + + def getRecordCount(self) -> int: + return self._record_count + + def appendRecord(self, record: "AccessibilityRecord"): + self._record_count += 1 + + def getRecord(self, index: int) -> Optional["AccessibilityRecord"]: + return None + + def toString(self) -> str: + return (f"AccessibilityEvent(type={hex(self._event_type)}, " + f"className={self._class_name}, " + f"packageName={self._package_name}, " + f"text={self._text})") + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityRecord +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityRecord(Stub): + """AccessibilityRecord — 无障碍记录""" + + def __init__(self): + super().__init__() + self._source: Optional[Any] = None + self._source_node_id: int = -1 + self._class_name: Optional[str] = None + self._package_name: Optional[str] = None + self._content_description: Optional[str] = None + self._text: List[str] = [] + self._from_index: int = -1 + self._to_index: int = -1 + self._item_count: int = 0 + self._current_item_index: int = 0 + self._scrollable: bool = False + self._scroll_x: int = 0 + self._scroll_y: int = 0 + self._max_scroll_x: int = 0 + self._max_scroll_y: int = 0 + self._enabled: bool = True + self._password: bool = False + self._checked: bool = False + self._full_package_name: Optional[str] = None + self._before_text: Optional[str] = None + self._removed_count: int = 0 + self._added_count: int = 0 + + def setSource(self, source: Optional[Any]): + self._source = source + + def getSource(self) -> Optional[Any]: + return self._source + + def setClassName(self, class_name: Optional[str]): + self._class_name = class_name + + def getClassName(self) -> Optional[str]: + return self._class_name + + def setContentDescription(self, content_description: Optional[str]): + self._content_description = content_description + + def getContentDescription(self) -> Optional[str]: + return self._content_description + + def setPackageName(self, package_name: Optional[str]): + self._package_name = package_name + + def getPackageName(self) -> Optional[str]: + return self._package_name + + def getText(self) -> List[str]: + return list(self._text) + + def setFromIndex(self, from_index: int): + self._from_index = from_index + + def getFromIndex(self) -> int: + return self._from_index + + def setToIndex(self, to_index: int): + self._to_index = to_index + + def getToIndex(self) -> int: + return self._to_index + + def setItemCount(self, item_count: int): + self._item_count = item_count + + def getItemCount(self) -> int: + return self._item_count + + def setScrollable(self, scrollable: bool): + self._scrollable = scrollable + + def getScrollable(self) -> bool: + return self._scrollable + + def setScrollX(self, scroll_x: int): + self._scroll_x = scroll_x + + def getScrollX(self) -> int: + return self._scroll_x + + def setScrollY(self, scroll_y: int): + self._scroll_y = scroll_y + + def getScrollY(self) -> int: + return self._scroll_y + + def setMaxScrollX(self, max_scroll_x: int): + self._max_scroll_x = max_scroll_x + + def getMaxScrollX(self) -> int: + return self._max_scroll_x + + def setMaxScrollY(self, max_scroll_y: int): + self._max_scroll_y = max_scroll_y + + def getMaxScrollY(self) -> int: + return self._max_scroll_y + + def isEnabled(self) -> bool: + return self._enabled + + def setEnabled(self, enabled: bool): + self._enabled = enabled + + def isPassword(self) -> bool: + return self._password + + def setPassword(self, password: bool): + self._password = password + + def isChecked(self) -> bool: + return self._checked + + def setChecked(self, checked: bool): + self._checked = checked + + def getBeforeText(self) -> Optional[str]: + return self._before_text + + def setBeforeText(self, before_text: Optional[str]): + self._before_text = before_text + + def getAddedCount(self) -> int: + return self._added_count + + def setAddedCount(self, added_count: int): + self._added_count = added_count + + def getRemovedCount(self) -> int: + return self._removed_count + + def setRemovedCount(self, removed_count: int): + self._removed_count = removed_count + + def toString(self) -> str: + return (f"AccessibilityRecord(className={self._class_name}, " + f"packageName={self._package_name})") + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityNodeInfo +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityNodeInfo(Stub): + """AccessibilityNodeInfo — 无障碍节点信息""" + + FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000002 + FLAG_REPORT_VIEW_IDS = 0x00000010 + FLAG_PREFETCH_PREDECESSORS = 0x00000001 + FLAG_PREFETCH_SIBLINGS = 0x00000002 + FLAG_PREFETCH_DESCENDANTS = 0x00000004 + + ACTION_CLICK = 0x00000010 + ACTION_LONG_CLICK = 0x00000020 + ACTION_FOCUS = 0x00000040 + ACTION_CLEAR_FOCUS = 0x00000080 + ACTION_SELECT = 0x00000100 + ACTION_CLEAR_SELECTION = 0x00000200 + ACTION_ACCESSIBILITY_FOCUS = 0x00000400 + ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000800 + ACTION_SCROLL_FORWARD = 0x00001000 + ACTION_SCROLL_BACKWARD = 0x00002000 + ACTION_SCROLL_TO_POSITION = 0x00004000 + ACTION_SET_SELECTION = 0x00008000 + ACTION_COPY = 0x00010000 + ACTION_PASTE = 0x00020000 + ACTION_CUT = 0x00030000 + ACTION_SET_TEXT = 0x00040000 + ACTION_EXPAND = 0x00080000 + ACTION_COLLAPSE = 0x00100000 + ACTION_DISMISS = 0x00200000 + ACTION_MOVE_WINDOW = 0x00400000 + ACTION_SHOW_ON_SCREEN = 0x00800000 + ACTION_CONTEXT_CLICK = 0x01000000 + ACTION_SCROLL_UP = 0x02000000 + ACTION_SCROLL_DOWN = 0x04000000 + ACTION_SCROLL_LEFT = 0x08000000 + ACTION_SCROLL_RIGHT = 0x10000000 + ACTION_PAGE_UP = 0x20000000 + ACTION_PAGE_DOWN = 0x40000000 + ACTION_PAGE_LEFT = 0x80000000 + ACTION_PAGE_RIGHT = 0x100000000 + + # 移动粒度 + MOVEMENT_GRANULARITY_CHARACTER = 1 + MOVEMENT_GRANULARITY_WORD = 2 + MOVEMENT_GRANULARITY_LINE = 4 + MOVEMENT_GRANULARITY_PARAGRAPH = 8 + MOVEMENT_GRANULARITY_PAGE = 16 + + def __init__(self): + super().__init__() + self._children: List["AccessibilityNodeInfo"] = [] + self._actions: int = 0 + self._clickable: bool = False + self._focusable: bool = False + self._long_clickable: bool = False + self._scrollable: bool = False + self._content_description: Optional[str] = None + self._text: Optional[str] = None + self._class_name: Optional[str] = None + self._package_name: Optional[str] = None + self._parent: Optional["AccessibilityNodeInfo"] = None + self._view_id: Optional[str] = None + self._enabled: bool = True + self._password: bool = False + self._checked: bool = False + self._checkable: bool = False + self._selected: bool = False + self._focused: bool = False + self._visible_to_user: bool = True + self._accessibility_focused: bool = False + self._can_open_popup: bool = False + self._dismissable: bool = False + self._editable: bool = False + self._multi_line: bool = False + self._important_for_accessibility: bool = True + self._pane_title: Optional[str] = None + self._hint_text: Optional[str] = None + self._tooltip_text: Optional[str] = None + self._error: Optional[str] = None + self._min_text_length: int = -1 + self._max_text_length: int = -1 + self._child_count: int = 0 + self._bounds_in_parent: Optional[Tuple[int, int, int, int]] = None + self._bounds_in_screen: Optional[Tuple[int, int, int, int]] = None + + def findAccessibilityNodeInfosByText(self, text: str) -> List["AccessibilityNodeInfo"]: + result: List["AccessibilityNodeInfo"] = [] + if self._text and text and text.lower() in self._text.lower(): + result.append(self) + for child in self._children: + result.extend(child.findAccessibilityNodeInfosByText(text)) + return result + + def findAccessibilityNodeInfosByViewId(self, view_id: str) -> List["AccessibilityNodeInfo"]: + result: List["AccessibilityNodeInfo"] = [] + if self._view_id == view_id: + result.append(self) + for child in self._children: + result.extend(child.findAccessibilityNodeInfosByViewId(view_id)) + return result + + def performAction(self, action: int) -> bool: + if action & self._actions: + sys.stderr.write(f"[AccessibilityNodeInfo] performAction: {hex(action)}\n") + return True + return False + + def setClickable(self, clickable: bool): + self._clickable = clickable + + def isClickable(self) -> bool: + return self._clickable + + def setFocusable(self, focusable: bool): + self._focusable = focusable + + def isFocusable(self) -> bool: + return self._focusable + + def setLongClickable(self, long_clickable: bool): + self._long_clickable = long_clickable + + def isLongClickable(self) -> bool: + return self._long_clickable + + def setScrollable(self, scrollable: bool): + self._scrollable = scrollable + + def isScrollable(self) -> bool: + return self._scrollable + + def setContentDescription(self, content_description: Optional[str]): + self._content_description = content_description + + def getContentDescription(self) -> Optional[str]: + return self._content_description + + def setText(self, text: Optional[str]): + self._text = text + + def getText(self) -> Optional[str]: + return self._text + + def setClassName(self, class_name: Optional[str]): + self._class_name = class_name + + def getClassName(self) -> Optional[str]: + return self._class_name + + def setPackageName(self, package_name: Optional[str]): + self._package_name = package_name + + def getPackageName(self) -> Optional[str]: + return self._package_name + + def getChild(self, index: int) -> Optional["AccessibilityNodeInfo"]: + if 0 <= index < len(self._children): + return self._children[index] + return None + + def setParent(self, parent: Optional["AccessibilityNodeInfo"]): + self._parent = parent + + def getParent(self) -> Optional["AccessibilityNodeInfo"]: + return self._parent + + def getChildCount(self) -> int: + return len(self._children) + + def addChild(self, child: "AccessibilityNodeInfo"): + self._children.append(child) + child._parent = self + + def addAction(self, action: int): + self._actions |= action + + def setActions(self, actions: int): + self._actions = actions + + def getActions(self) -> int: + return self._actions + + def setViewIdResourceName(self, view_id: str): + self._view_id = view_id + + def getViewIdResourceName(self) -> Optional[str]: + return self._view_id + + def isEnabled(self) -> bool: + return self._enabled + + def setEnabled(self, enabled: bool): + self._enabled = enabled + + def isPassword(self) -> bool: + return self._password + + def setPassword(self, password: bool): + self._password = password + + def isChecked(self) -> bool: + return self._checked + + def setChecked(self, checked: bool): + self._checked = checked + + def isCheckable(self) -> bool: + return self._checkable + + def setCheckable(self, checkable: bool): + self._checkable = checkable + + def isSelected(self) -> bool: + return self._selected + + def setSelected(self, selected: bool): + self._selected = selected + + def isFocused(self) -> bool: + return self._focused + + def setFocused(self, focused: bool): + self._focused = focused + + def isVisibleToUser(self) -> bool: + return self._visible_to_user + + def setVisibleToUser(self, visible: bool): + self._visible_to_user = visible + + def isAccessibilityFocused(self) -> bool: + return self._accessibility_focused + + def setAccessibilityFocused(self, focused: bool): + self._accessibility_focused = focused + + def isEditable(self) -> bool: + return self._editable + + def setEditable(self, editable: bool): + self._editable = editable + + def isMultiLine(self) -> bool: + return self._multi_line + + def setMultiLine(self, multi_line: bool): + self._multi_line = multi_line + + def isImportantForAccessibility(self) -> bool: + return self._important_for_accessibility + + def setImportantForAccessibility(self, important: bool): + self._important_for_accessibility = important + + def setPaneTitle(self, pane_title: Optional[str]): + self._pane_title = pane_title + + def getPaneTitle(self) -> Optional[str]: + return self._pane_title + + def setHintText(self, hint_text: Optional[str]): + self._hint_text = hint_text + + def getHintText(self) -> Optional[str]: + return self._hint_text + + def setTooltipText(self, tooltip_text: Optional[str]): + self._tooltip_text = tooltip_text + + def getTooltipText(self) -> Optional[str]: + return self._tooltip_text + + def setError(self, error: Optional[str]): + self._error = error + + def getError(self) -> Optional[str]: + return self._error + + def setBoundsInParent(self, left: int, top: int, right: int, bottom: int): + self._bounds_in_parent = (left, top, right, bottom) + + def getBoundsInParent(self) -> Optional[Tuple[int, int, int, int]]: + return self._bounds_in_parent + + def setBoundsInScreen(self, left: int, top: int, right: int, bottom: int): + self._bounds_in_screen = (left, top, right, bottom) + + def getBoundsInScreen(self) -> Optional[Tuple[int, int, int, int]]: + return self._bounds_in_screen + + def getWindowId(self) -> int: + return -1 + + def recycle(self): + self._children.clear() + self._parent = None + self._actions = 0 + + def toString(self) -> str: + return (f"AccessibilityNodeInfo(className={self._class_name}, " + f"text={self._text}, " + f"contentDescription={self._content_description})") + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityNodeProvider +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityNodeProvider(Stub): + """AccessibilityNodeProvider — 无障碍节点提供者""" + + HOST_VIEW_ID = -1 + + def __init__(self): + super().__init__() + + def createAccessibilityNodeInfo(self, virtual_view_id: int) -> AccessibilityNodeInfo: + raise AndroidOnlyError("AccessibilityNodeProvider.createAccessibilityNodeInfo") + + def addExtraDataToAccessibilityNodeInfo(self, virtual_view_id: int, + info: AccessibilityNodeInfo, + extra_data_key: str, + arguments: Optional[Any]): + pass + + def performAction(self, virtual_view_id: int, action: int, + arguments: Optional[Any]) -> bool: + return False + + def findFocus(self, focus: int) -> Optional[AccessibilityNodeInfo]: + return None + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityServiceInfo +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityServiceInfo(Stub): + """AccessibilityServiceInfo — 无障碍服务信息""" + + FEEDBACK_GENERIC = 16 + FEEDBACK_SPOKEN = 1 + FEEDBACK_HAPTIC = 2 + FEEDBACK_AUDIBLE = 4 + FEEDBACK_VISUAL = 8 + FEEDBACK_BRAILLE = 32 + FEEDBACK_ALL_MASK = -1 + + FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 0x00000001 + FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000002 + FLAG_REPORT_VIEW_IDS = 0x00000010 + FLAG_REQUEST_FILTER_KEY_EVENTS = 0x00000020 + FLAG_RETRIEVE_INTERACTIVE_WINDOWS = 0x00000040 + FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000080 + FLAG_REQUEST_ACCESSIBILITY_BUTTON = 0x00000100 + FLAG_REQUEST_SHORTCUT_WARNING_DIALOG = 0x00000200 + + CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT = 1 + CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 2 + CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 4 + CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS = 8 + CAPABILITY_CAN_CONTROL_MAGNIFICATION = 16 + CAPABILITY_CAN_PERFORM_GESTURES = 32 + + def __init__(self): + super().__init__() + self.eventTypes: int = 0 + self.feedbackType: int = 0 + self.flags: int = 0 + self.notificationTimeout: int = 0 + self.filterTouchExploration: bool = False + self.packageNames: Optional[List[str]] = None + self.descriptionResId: int = 0 + self.description: Optional[str] = None + self.settingsActivityName: Optional[str] = None + self.capabilities: int = 0 + self.interactiveUiTimeout: int = 0 + self.nonInteractiveUiTimeout: int = 0 + + def getCapabilityInfo(self, capability: int) -> Optional[str]: + cap_map = { + self.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT: "CAN_RETRIEVE_WINDOW_CONTENT", + self.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION: "CAN_REQUEST_TOUCH_EXPLORATION", + self.CAPABILITY_CAN_CONTROL_MAGNIFICATION: "CAN_CONTROL_MAGNIFICATION", + self.CAPABILITY_CAN_PERFORM_GESTURES: "CAN_PERFORM_GESTURES", + } + return cap_map.get(capability) + + def toString(self) -> str: + return (f"AccessibilityServiceInfo(eventTypes={hex(self.eventTypes)}, " + f"feedbackType={self.feedbackType}, " + f"flags={hex(self.flags)})") + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityWindowInfo +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityWindowInfo(Stub): + """AccessibilityWindowInfo — 无障碍窗口信息""" + + TYPE_APPLICATION = 1 + TYPE_INPUT_METHOD = 2 + TYPE_SYSTEM = 3 + TYPE_ACCESSIBILITY_OVERLAY = 4 + TYPE_SPLIT_SCREEN_DIVIDER = 5 + + UNDEFINED = -1 + ACTIVE = 0x00000001 + FOCUSED = 0x00000002 + ACCESSIBILITY_FOCUSED = 0x00000004 + + def __init__(self): + super().__init__() + self._type: int = TYPE_SYSTEM + self._bounds_in_screen: Tuple[int, int, int, int] = (0, 0, 0, 0) + self._root: Optional[AccessibilityNodeInfo] = None + self._title: Optional[str] = None + self._anchor: Optional[Any] = None + self._parent: Optional["AccessibilityWindowInfo"] = None + self._children: List["AccessibilityWindowInfo"] = [] + self._id: int = -1 + self._layer: int = 0 + self._active: bool = False + self._focused: bool = False + self._accessibility_focused: bool = False + self._picture_in_picture: bool = False + + @staticmethod + def obtain() -> "AccessibilityWindowInfo": + return AccessibilityWindowInfo() + + def recycle(self): + self._root = None + self._children.clear() + self._parent = None + + def getType(self) -> int: + return self._type + + def setType(self, window_type: int): + self._type = window_type + + def getBoundsInScreen(self) -> Tuple[int, int, int, int]: + return self._bounds_in_screen + + def setBoundsInScreen(self, left: int, top: int, right: int, bottom: int): + self._bounds_in_screen = (left, top, right, bottom) + + def getRoot(self) -> Optional[AccessibilityNodeInfo]: + return self._root + + def setRoot(self, root: Optional[AccessibilityNodeInfo]): + self._root = root + + def getTitle(self) -> Optional[str]: + return self._title + + def setTitle(self, title: Optional[str]): + self._title = title + + def getAnchor(self) -> Optional[Any]: + return self._anchor + + def setAnchor(self, anchor: Optional[Any]): + self._anchor = anchor + + def getId(self) -> int: + return self._id + + def setId(self, window_id: int): + self._id = window_id + + def getLayer(self) -> int: + return self._layer + + def setLayer(self, layer: int): + self._layer = layer + + def getParent(self) -> Optional["AccessibilityWindowInfo"]: + return self._parent + + def setParent(self, parent: Optional["AccessibilityWindowInfo"]): + self._parent = parent + + def getChild(self, index: int) -> Optional["AccessibilityWindowInfo"]: + if 0 <= index < len(self._children): + return self._children[index] + return None + + def getChildCount(self) -> int: + return len(self._children) + + def addChild(self, child: "AccessibilityWindowInfo"): + self._children.append(child) + + def isActive(self) -> bool: + return self._active + + def setActive(self, active: bool): + self._active = active + + def isFocused(self) -> bool: + return self._focused + + def setFocused(self, focused: bool): + self._focused = focused + + def isAccessibilityFocused(self) -> bool: + return self._accessibility_focused + + def setAccessibilityFocused(self, focused: bool): + self._accessibility_focused = focused + + def isInPictureInPictureMode(self) -> bool: + return self._picture_in_picture + + def setPictureInPictureMode(self, pip: bool): + self._picture_in_picture = pip + + def toString(self) -> str: + type_names = {1: "APPLICATION", 2: "INPUT_METHOD", 3: "SYSTEM", + 4: "ACCESSIBILITY_OVERLAY", 5: "SPLIT_SCREEN_DIVIDER"} + type_name = type_names.get(self._type, "UNKNOWN") + return f"AccessibilityWindowInfo(id={self._id}, type={type_name}, title={self._title})" + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityService +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityService(Stub): + """AccessibilityService — 无障碍服务基类""" + + GLOBAL_ACTION_BACK = 1 + GLOBAL_ACTION_HOME = 2 + GLOBAL_ACTION_RECENTS = 3 + GLOBAL_ACTION_NOTIFICATIONS = 4 + GLOBAL_ACTION_QUICK_SETTINGS = 5 + GLOBAL_ACTION_POWER_DIALOG = 6 + GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN = 7 + GLOBAL_ACTION_LOCK_SCREEN = 8 + GLOBAL_ACTION_TAKE_SCREENSHOT = 9 + GLOBAL_ACTION_KEYCODE_HEADSETHOOK = 10 + GLOBAL_ACTION_ACCESSIBILITY_SHORTCUT = 11 + GLOBAL_ACTION_ACCESSIBILITY_BUTTON = 12 + GLOBAL_ACTION_ACCESSIBILITY_BUTTON_CHOOSER = 13 + GLOBAL_ACTION_ACCESSIBILITY_SHORTCUT_CHOOSER = 14 + GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS = 15 + + def __init__(self): + super().__init__() + self._service_info: Optional[AccessibilityServiceInfo] = None + self._root_node: Optional[AccessibilityNodeInfo] = None + self._windows: List[AccessibilityWindowInfo] = [] + + def onAccessibilityEvent(self, event: AccessibilityEvent): + raise AndroidOnlyError("AccessibilityService.onAccessibilityEvent: not implemented") + + def onInterrupt(self): + raise AndroidOnlyError("AccessibilityService.onInterrupt: not implemented") + + def onServiceConnected(self): + pass + + def performGlobalAction(self, action: int) -> bool: + action_names = { + self.GLOBAL_ACTION_BACK: "BACK", + self.GLOBAL_ACTION_HOME: "HOME", + self.GLOBAL_ACTION_RECENTS: "RECENTS", + self.GLOBAL_ACTION_NOTIFICATIONS: "NOTIFICATIONS", + self.GLOBAL_ACTION_QUICK_SETTINGS: "QUICK_SETTINGS", + } + sys.stderr.write(f"[AccessibilityService] performGlobalAction: " + f"{action_names.get(action, hex(action))}\n") + return True + + def getRootInActiveWindow(self) -> Optional[AccessibilityNodeInfo]: + return self._root_node + + def findFocus(self, focus_type: int) -> Optional[AccessibilityNodeInfo]: + return self._root_node + + def getServiceInfo(self) -> Optional[AccessibilityServiceInfo]: + return self._service_info + + def setServiceInfo(self, info: AccessibilityServiceInfo): + self._service_info = info + + def getWindows(self) -> List[AccessibilityWindowInfo]: + return list(self._windows) + + def getWindowsOnScreen(self) -> List[AccessibilityWindowInfo]: + return [w for w in self._windows if w.isActive()] + + def disableSelf(self): + sys.stderr.write("[AccessibilityService] disableSelf\n") + + def onKeyEvent(self, event: Any) -> bool: + return False + + def onGesture(self, gesture_id: int) -> bool: + return False + + def onMotionEvent(self, event: Any) -> bool: + return False + + def onTouchExplorationStateChanged(self, enabled: bool): + pass + + def onMagnificationChanged(self, region: Any, scale: float): + pass + + def onSoftKeyboardShowModeChanged(self, show_mode: int): + pass + + def dispatchKeyEventToInputMethod(self, event: Any) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityManager +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityManager(Stub): + """AccessibilityManager — 无障碍管理器""" + + def __init__(self): + super().__init__() + self._enabled: bool = False + self._touch_exploration_enabled: bool = False + self._state_listeners: List[Callable] = [] + self._touch_exploration_listeners: List[Callable] = [] + self._accessibility_service_list: List[AccessibilityServiceInfo] = [] + self._request_preparers: List[Any] = [] + + def isEnabled(self) -> bool: + return self._enabled + + def setEnabled(self, enabled: bool): + old = self._enabled + self._enabled = enabled + if old != enabled: + for listener in list(self._state_listeners): + try: + listener.onAccessibilityStateChanged(enabled) + except Exception: + pass + + def isTouchExplorationEnabled(self) -> bool: + return self._touch_exploration_enabled + + def addAccessibilityStateChangeListener(self, listener: Callable) -> bool: + if listener not in self._state_listeners: + self._state_listeners.append(listener) + return True + return False + + def removeAccessibilityStateChangeListener(self, listener: Callable) -> bool: + if listener in self._state_listeners: + self._state_listeners.remove(listener) + return True + return False + + def addTouchExplorationStateChangeListener(self, listener: Callable) -> bool: + if listener not in self._touch_exploration_listeners: + self._touch_exploration_listeners.append(listener) + return True + return False + + def removeTouchExplorationStateChangeListener(self, listener: Callable) -> bool: + if listener in self._touch_exploration_listeners: + self._touch_exploration_listeners.remove(listener) + return True + return False + + def getAccessibilityServiceList(self) -> List[AccessibilityServiceInfo]: + return list(self._accessibility_service_list) + + def addAccessibilityRequestPreparer(self, preparer: Any): + if preparer not in self._request_preparers: + self._request_preparers.append(preparer) + + def removeAccessibilityRequestPreparer(self, preparer: Any): + if preparer in self._request_preparers: + self._request_preparers.remove(preparer) + + def sendAccessibilityEvent(self, event: AccessibilityEvent): + sys.stderr.write(f"[AccessibilityManager] sendEvent: type={hex(event.getEventType())}\n") + + def interrupt(self): + sys.stderr.write("[AccessibilityManager] interrupt\n") + + def getEnabledAccessibilityServiceList(self, feedback_type_flags: int) -> List[AccessibilityServiceInfo]: + return [s for s in self._accessibility_service_list + if s.feedbackType & feedback_type_flags] + + def getInstalledAccessibilityServiceList(self) -> List[AccessibilityServiceInfo]: + return list(self._accessibility_service_list) + + def addAccessibilityStateChangeListener(self, listener: Callable) -> bool: + if listener not in self._state_listeners: + self._state_listeners.append(listener) + return True + return False + + def removeAccessibilityStateChangeListener(self, listener: Callable) -> bool: + if listener in self._state_listeners: + self._state_listeners.remove(listener) + return True + return False + + def toString(self) -> str: + return f"AccessibilityManager(enabled={self._enabled})" + + +# ═══════════════════════════════════════════════════════════════ +# CaptioningManager +# ═══════════════════════════════════════════════════════════════ + +class CaptioningManager(Stub): + """CaptioningManager — 字幕管理器""" + + class CaptionStyle(Stub): + """CaptionStyle — 字幕样式""" + + EDGE_TYPE_NONE = 0 + EDGE_TYPE_OUTLINE = 1 + EDGE_TYPE_DROP_SHADOW = 2 + EDGE_TYPE_RAISED = 3 + EDGE_TYPE_DEPRESSED = 4 + + def __init__(self): + super().__init__() + self._foreground_color: int = 0xFFFFFFFF + self._background_color: int = 0xFF000000 + self._edge_color: int = 0xFF000000 + self._edge_type: int = EDGE_TYPE_NONE + self._typeface: Optional[Any] = None + self._font_scale: float = 1.0 + self._window_color: int = 0xFF000000 + + @staticmethod + def getCustomStyle(css: Any) -> "CaptioningManager.CaptionStyle": + return CaptioningManager.CaptionStyle() + + def getForegroundColor(self) -> int: + return self._foreground_color + + def getBackgroundColor(self) -> int: + return self._background_color + + def getEdgeColor(self) -> int: + return self._edge_color + + def getEdgeType(self) -> int: + return self._edge_type + + def getTypeface(self) -> Optional[Any]: + return self._typeface + + def hasForegroundColor(self) -> bool: + return True + + def hasBackgroundColor(self) -> bool: + return True + + def hasEdgeColor(self) -> bool: + return True + + def hasEdgeType(self) -> bool: + return True + + def hasWindowColor(self) -> bool: + return True + + def getWindowColor(self) -> int: + return self._window_color + + def __init__(self): + super().__init__() + self._enabled: bool = False + self._locale: Optional[str] = None + self._font_scale: float = 1.0 + self._user_style: Optional["CaptioningManager.CaptionStyle"] = None + + def isCaptioningEnabled(self) -> bool: + return self._enabled + + def getLocale(self) -> Optional[str]: + return self._locale + + def getFontScale(self) -> float: + return self._font_scale + + def getUserStyle(self) -> "CaptioningManager.CaptionStyle": + if self._user_style is None: + self._user_style = CaptioningManager.CaptionStyle() + return self._user_style + + def addCaptioningChangeListener(self, listener: Any): + pass + + def removeCaptioningChangeListener(self, listener: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityServiceGestureResult +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityServiceGestureResult(Stub): + """AccessibilityServiceGestureResult — 手势结果""" + + def __init__(self, completed: bool): + super().__init__() + self._completed: bool = completed + + def isCompleted(self) -> bool: + return self._completed + + +# ═══════════════════════════════════════════════════════════════ +# TouchExplorationStateChangeListener +# ═══════════════════════════════════════════════════════════════ + +class TouchExplorationStateChangeListener(Stub): + """TouchExplorationStateChangeListener — 触摸探索状态变更监听器""" + + def onTouchExplorationStateChanged(self, enabled: bool): + pass + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityButtonController +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityButtonController(Stub): + """AccessibilityButtonController — 无障碍按钮控制器""" + + class AccessibilityButtonCallback(Stub): + """AccessibilityButtonCallback — 无障碍按钮回调""" + + def onAvailabilityChanged(self, available: bool): + pass + + def onClicked(self, controller: "AccessibilityButtonController"): + pass + + def __init__(self): + super().__init__() + self._callback: Optional["AccessibilityButtonController.AccessibilityButtonCallback"] = None + + def isAccessibilityButtonAvailable(self) -> bool: + return False + + def registerAccessibilityButtonCallback(self, callback: "AccessibilityButtonController.AccessibilityButtonCallback"): + self._callback = callback + + def unregisterAccessibilityButtonCallback(self, callback: "AccessibilityButtonController.AccessibilityButtonCallback"): + if self._callback == callback: + self._callback = None + + def performAccessibilityButtonAction(self) -> bool: + if self._callback: + self._callback.onClicked(self) + return True + return False + + +# ═══════════════════════════════════════════════════════════════ +# AccessibilityButtonControllerCallback +# ═══════════════════════════════════════════════════════════════ + +class AccessibilityButtonControllerCallback(Stub): + """AccessibilityButtonControllerCallback — 别名""" + + def onAvailabilityChanged(self, available: bool): + pass + + def onClicked(self, controller: AccessibilityButtonController): + pass + + +# ═══════════════════════════════════════════════════════════════ +# MagnificationConfig +# ═══════════════════════════════════════════════════════════════ + +class MagnificationConfig(Stub): + """MagnificationConfig — 放大配置""" + + SCALE = 0 + CENTER_X = 1 + CENTER_Y = 2 + ALL = -1 + + def __init__(self): + super().__init__() + self._scale: float = 1.0 + self._center_x: float = 0.0 + self._center_y: float = 0.0 + self._mode: int = 0 + self._frame_left: float = 0.0 + self._frame_top: float = 0.0 + self._frame_right: float = 0.0 + self._frame_bottom: float = 0.0 + + def getScale(self) -> float: + return self._scale + + def setScale(self, scale: float): + self._scale = scale + + def getCenterX(self) -> float: + return self._center_x + + def setCenterX(self, center_x: float): + self._center_x = center_x + + def getCenterY(self) -> float: + return self._center_y + + def setCenterY(self, center_y: float): + self._center_y = center_y + + def getMode(self) -> int: + return self._mode + + def setMode(self, mode: int): + self._mode = mode + + def toString(self) -> str: + return (f"MagnificationConfig(scale={self._scale}, " + f"center=({self._center_x}, {self._center_y}))") + + +# ═══════════════════════════════════════════════════════════════ +# MagnificationController +# ═══════════════════════════════════════════════════════════════ + +class MagnificationController(Stub): + """MagnificationController — 放大控制器""" + + class OnMagnificationChangedListener(Stub): + def onMagnificationChanged(self, controller: "MagnificationController", + region: Any, scale: float, center_x: float, center_y: float): + pass + + def __init__(self): + super().__init__() + self._listeners: List["MagnificationController.OnMagnificationChangedListener"] = [] + self._scale: float = 1.0 + self._center_x: float = 0.0 + self._center_y: float = 0.0 + self._magnification_region: Any = None + + def getScale(self) -> float: + return self._scale + + def getCenterX(self) -> float: + return self._center_x + + def getCenterY(self) -> float: + return self._center_y + + def getMagnificationRegion(self) -> Any: + return self._magnification_region + + def setScale(self, scale: float, animate: bool = False) -> bool: + self._scale = scale + self._notifyListeners() + return True + + def setCenter(self, center_x: float, center_y: float, animate: bool = False) -> bool: + self._center_x = center_x + self._center_y = center_y + self._notifyListeners() + return True + + def addListener(self, listener: "MagnificationController.OnMagnificationChangedListener"): + if listener not in self._listeners: + self._listeners.append(listener) + + def removeListener(self, listener: "MagnificationController.OnMagnificationChangedListener"): + if listener in self._listeners: + self._listeners.remove(listener) + + def _notifyListeners(self): + for listener in list(self._listeners): + try: + listener.onMagnificationChanged(self, self._magnification_region, + self._scale, self._center_x, self._center_y) + except Exception: + pass + + def isMagnifying(self) -> bool: + return abs(self._scale - 1.0) > 0.01 + + def reset(self, animate: bool = False) -> bool: + if self.isMagnifying(): + self._scale = 1.0 + self._center_x = 0.0 + self._center_y = 0.0 + self._notifyListeners() + return True + return False + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "AccessibilityEvent", "AccessibilityRecord", + "AccessibilityNodeInfo", "AccessibilityNodeProvider", + "AccessibilityService", "AccessibilityServiceInfo", + "AccessibilityWindowInfo", + "CaptioningManager", + "AccessibilityManager", + "AccessibilityServiceGestureResult", + "TouchExplorationStateChangeListener", + "AccessibilityButtonController", + "AccessibilityButtonControllerCallback", + "MagnificationConfig", + "MagnificationController", +] \ No newline at end of file diff --git a/android_pyapi/android/view_animation.py b/android_pyapi/android/view_animation.py new file mode 100644 index 0000000..1c703ea --- /dev/null +++ b/android_pyapi/android/view_animation.py @@ -0,0 +1,935 @@ +""" +Android View Animation 组件 — Animation, AnimationSet, Interpolators, +AnimationUtils, Transformation 等 +方法签名完全对齐 Android SDK +""" + +import math +import sys +import time +from typing import Any, Callable, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError, android_context + + +# ═══════════════════════════════════════════════════════════════ +# Interpolator +# ═══════════════════════════════════════════════════════════════ + +class Interpolator(Stub): + """Interpolator — 插值器接口""" + + def getInterpolation(self, input: float) -> float: + return input + + +# ═══════════════════════════════════════════════════════════════ +# AccelerateInterpolator +# ═══════════════════════════════════════════════════════════════ + +class AccelerateInterpolator(Interpolator): + """AccelerateInterpolator — 加速插值器(先慢后快)""" + + def __init__(self, factor: float = 1.0): + super().__init__() + self._factor = factor + self._double_factor = 2.0 * factor + + def getInterpolation(self, input: float) -> float: + if self._factor == 1.0: + return input * input + return input ** (self._double_factor) + + +# ═══════════════════════════════════════════════════════════════ +# DecelerateInterpolator +# ═══════════════════════════════════════════════════════════════ + +class DecelerateInterpolator(Interpolator): + """DecelerateInterpolator — 减速插值器(先快后慢)""" + + def __init__(self, factor: float = 1.0): + super().__init__() + self._factor = factor + + def getInterpolation(self, input: float) -> float: + if self._factor == 1.0: + return 1.0 - (1.0 - input) * (1.0 - input) + return 1.0 - (1.0 - input) ** (2.0 * self._factor) + + +# ═══════════════════════════════════════════════════════════════ +# AccelerateDecelerateInterpolator +# ═══════════════════════════════════════════════════════════════ + +class AccelerateDecelerateInterpolator(Interpolator): + """AccelerateDecelerateInterpolator — 加速减速插值器""" + + def getInterpolation(self, input: float) -> float: + return float(math.cos((input + 1) * math.pi) / 2.0 + 0.5) + + +# ═══════════════════════════════════════════════════════════════ +# LinearInterpolator +# ═══════════════════════════════════════════════════════════════ + +class LinearInterpolator(Interpolator): + """LinearInterpolator — 线性插值器""" + + def getInterpolation(self, input: float) -> float: + return input + + +# ═══════════════════════════════════════════════════════════════ +# OvershootInterpolator +# ═══════════════════════════════════════════════════════════════ + +class OvershootInterpolator(Interpolator): + """OvershootInterpolator — 超出插值器(越过目标再回弹)""" + + def __init__(self, tension: float = 2.0): + super().__init__() + self._tension = tension + + def getInterpolation(self, input: float) -> float: + input -= 1.0 + return input * input * ((self._tension + 1) * input + self._tension) + 1.0 + + +# ═══════════════════════════════════════════════════════════════ +# AnticipateInterpolator +# ═══════════════════════════════════════════════════════════════ + +class AnticipateInterpolator(Interpolator): + """AnticipateInterpolator — 预判插值器(先反向后正向前进)""" + + def __init__(self, tension: float = 2.0): + super().__init__() + self._tension = tension + + def getInterpolation(self, input: float) -> float: + return input * input * ((self._tension + 1) * input - self._tension) + + +# ═══════════════════════════════════════════════════════════════ +# AnticipateOvershootInterpolator +# ═══════════════════════════════════════════════════════════════ + +class AnticipateOvershootInterpolator(Interpolator): + """AnticipateOvershootInterpolator — 预判超出插值器""" + + def __init__(self, tension: float = 2.0, extra_tension: float = 1.5): + super().__init__() + self._tension = tension + self._extra_tension = extra_tension + + def getInterpolation(self, input: float) -> float: + if input < 0.5: + t = input * 2.0 + return 0.5 * t * t * ((self._tension + 1) * t - self._tension) + else: + t = input * 2.0 - 2.0 + return 0.5 * (t * t * ((self._tension + 1) * t + self._tension) + 2.0) + + +# ═══════════════════════════════════════════════════════════════ +# BounceInterpolator +# ═══════════════════════════════════════════════════════════════ + +class BounceInterpolator(Interpolator): + """BounceInterpolator — 弹跳插值器""" + + @staticmethod + def _bounce(t: float) -> float: + t2 = t * t + return t2 * 8.0 + + def getInterpolation(self, input: float) -> float: + if input < 0.3535: + return BounceInterpolator._bounce(input) + elif input < 0.7408: + return BounceInterpolator._bounce(input - 0.54719) + 0.7 + elif input < 0.9644: + return BounceInterpolator._bounce(input - 0.8526) + 0.9 + else: + return BounceInterpolator._bounce(input - 1.0435) + 0.95 + + +# ═══════════════════════════════════════════════════════════════ +# CycleInterpolator +# ═══════════════════════════════════════════════════════════════ + +class CycleInterpolator(Interpolator): + """CycleInterpolator — 周期插值器(正弦循环)""" + + def __init__(self, cycles: float = 1.0): + super().__init__() + self._cycles = cycles + + def getInterpolation(self, input: float) -> float: + return float(math.sin(2.0 * math.pi * self._cycles * input)) + + +# ═══════════════════════════════════════════════════════════════ +# PathInterpolator +# ═══════════════════════════════════════════════════════════════ + +class PathInterpolator(Interpolator): + """PathInterpolator — 路径插值器""" + + def __init__(self, control_x1: float = 0.0, control_y1: float = 0.0, + control_x2: float = 1.0, control_y2: float = 1.0): + super().__init__() + self._control_x1 = control_x1 + self._control_y1 = control_y1 + self._control_x2 = control_x2 + self._control_y2 = control_y2 + + def getInterpolation(self, input: float) -> float: + return self._cubic_bezier(input) + + def _cubic_bezier(self, t: float) -> float: + u = 1.0 - t + return (3.0 * u * u * t * self._control_y1 + + 3.0 * u * t * t * self._control_y2 + + t * t * t) + + +# ═══════════════════════════════════════════════════════════════ +# Transformation +# ═══════════════════════════════════════════════════════════════ + +class Transformation(Stub): + """Transformation — 动画变换矩阵""" + + TYPE_IDENTITY = 0 + TYPE_ALPHA = 1 + TYPE_BOTH = 2 + + def __init__(self): + super().__init__() + self._alpha: float = 1.0 + self._matrix: Any = None # 占位,实际 Android 为 Matrix 对象 + self._transformation_type: int = Transformation.TYPE_IDENTITY + + def getAlpha(self) -> float: + return self._alpha + + def setAlpha(self, alpha: float): + self._alpha = alpha + + def getMatrix(self) -> Any: + return self._matrix + + def setMatrix(self, matrix: Any): + self._matrix = matrix + + def setTransformationType(self, transformation_type: int): + self._transformation_type = transformation_type + + def getTransformationType(self) -> int: + return self._transformation_type + + def clear(self): + self._alpha = 1.0 + self._matrix = None + self._transformation_type = Transformation.TYPE_IDENTITY + + def compose(self, other: "Transformation"): + if other is None: + return + self._alpha *= other._alpha + if self._matrix is None: + self._matrix = other._matrix + elif other._matrix is not None: + pass # 占位:矩阵乘法 + self._transformation_type = max(self._transformation_type, + other._transformation_type) + + def toString(self) -> str: + return (f"Transformation(alpha={self._alpha}, " + f"type={self._transformation_type})") + + +# ═══════════════════════════════════════════════════════════════ +# Animation +# ═══════════════════════════════════════════════════════════════ + +class Animation(Stub): + """Animation — 动画基类""" + + INFINITE = -1 + RESTART = 1 + REVERSE = 2 + + ABSOLUTE = 0 + RELATIVE_TO_SELF = 1 + RELATIVE_TO_PARENT = 2 + + START_ON_FIRST_FRAME = -1 + + class AnimationListener(Stub): + """AnimationListener — 动画监听器""" + + def onAnimationStart(self, animation: "Animation"): + pass + + def onAnimationEnd(self, animation: "Animation"): + pass + + def onAnimationRepeat(self, animation: "Animation"): + pass + + def __init__(self): + super().__init__() + self._duration: int = 0 + self._start_offset: int = 0 + self._repeat_count: int = 0 + self._repeat_mode: int = Animation.RESTART + self._fill_before: bool = False + self._fill_after: bool = False + self._fill_enabled: bool = False + self._z_adjustment: int = 0 + self._interpolator: Optional[Interpolator] = None + self._listener: Optional["Animation.AnimationListener"] = None + self._start_time: Optional[int] = None + self._started: bool = False + self._ended: bool = False + self._cycle: float = 0.0 + self._current_repeat: int = 0 + self._initialized: bool = False + self._scale_factor: float = 1.0 + self._paused: bool = False + self._more: bool = True + + def start(self): + self._start_time = self._current_time_millis() + self._started = True + self._ended = False + self._current_repeat = 0 + self._more = True + if self._listener: + self._listener.onAnimationStart(self) + + def startNow(self): + self.start() + + def cancel(self): + self._ended = True + self._more = False + + def reset(self): + self._start_time = None + self._started = False + self._ended = False + self._current_repeat = 0 + self._cycle = 0.0 + self._more = True + self._initialized = False + + def setDuration(self, duration_millis: int): + self._duration = duration_millis + + def getDuration(self) -> int: + return self._duration + + def setStartOffset(self, start_offset: int): + self._start_offset = start_offset + + def getStartOffset(self) -> int: + return self._start_offset + + def setRepeatCount(self, repeat_count: int): + self._repeat_count = repeat_count + + def getRepeatCount(self) -> int: + return self._repeat_count + + def setRepeatMode(self, repeat_mode: int): + self._repeat_mode = repeat_mode + + def getRepeatMode(self) -> int: + return self._repeat_mode + + def setInterpolator(self, interpolator: Optional[Interpolator]): + self._interpolator = interpolator + + def getInterpolator(self) -> Optional[Interpolator]: + return self._interpolator + + def setAnimationListener(self, listener: "Animation.AnimationListener"): + self._listener = listener + + def getAnimationListener(self) -> Optional["Animation.AnimationListener"]: + return self._listener + + def getTransformation(self, current_time: int, + out_transformation: Transformation) -> bool: + if self._start_time is None: + self._start_time = current_time + + elapsed = current_time - self._start_time - self._start_offset + if elapsed < 0: + out_transformation.clear() + return True + + duration = self._duration + if duration == 0: + duration = 1 + + normalized = elapsed / duration + + if normalized >= 1.0: + if self._current_repeat < self._repeat_count or self._repeat_count == self.INFINITE: + if self._repeat_mode == Animation.REVERSE: + normalized = 0.0 + else: + normalized = 0.0 + self._current_repeat += 1 + if self._listener: + self._listener.onAnimationRepeat(self) + else: + normalized = 1.0 + self._ended = True + self._more = False + self._apply_transformation(1.0, out_transformation) + if self._listener: + self._listener.onAnimationEnd(self) + return False + + if normalized > 1.0: + normalized = 1.0 + + if self._interpolator: + interpolated = self._interpolator.getInterpolation(normalized) + else: + interpolated = normalized + + self._cycle = normalized + self._apply_transformation(interpolated, out_transformation) + return self._more + + def _apply_transformation(self, interpolated_time: float, t: Transformation): + pass + + def hasEnded(self) -> bool: + return self._ended + + def hasStarted(self) -> bool: + return self._started + + def setFillBefore(self, fill_before: bool): + self._fill_before = fill_before + + def getFillBefore(self) -> bool: + return self._fill_before + + def setFillAfter(self, fill_after: bool): + self._fill_after = fill_after + + def getFillAfter(self) -> bool: + return self._fill_after + + def setFillEnabled(self, fill_enabled: bool): + self._fill_enabled = fill_enabled + + def getFillEnabled(self) -> bool: + return self._fill_enabled + + def setZAdjustment(self, z_adjustment: int): + self._z_adjustment = z_adjustment + + def getZAdjustment(self) -> int: + return self._z_adjustment + + def willChangeBounds(self) -> bool: + return True + + def willChangeTransformationMatrix(self) -> bool: + return True + + def initialize(self, width: int, height: int, parent_width: int, parent_height: int): + self._initialized = True + + def restrictDuration(self, duration_millis: int): + if self._duration > duration_millis: + self._duration = duration_millis + + def scaleCurrentDuration(self, scale: float): + self._duration = int(self._duration * scale) + + @staticmethod + def _current_time_millis() -> int: + return int(time.time() * 1000) + + def toString(self) -> str: + return (f"Animation(duration={self._duration}, " + f"repeatCount={self._repeat_count}, " + f"started={self._started})") + + +# ═══════════════════════════════════════════════════════════════ +# AlphaAnimation +# ═══════════════════════════════════════════════════════════════ + +class AlphaAnimation(Animation): + """AlphaAnimation — 透明度动画""" + + def __init__(self, from_alpha: float, to_alpha: float): + super().__init__() + self._from_alpha = from_alpha + self._to_alpha = to_alpha + + def _apply_transformation(self, interpolated_time: float, t: Transformation): + alpha = self._from_alpha + (self._to_alpha - self._from_alpha) * interpolated_time + t.setAlpha(alpha) + + def getFromAlpha(self) -> float: + return self._from_alpha + + def getToAlpha(self) -> float: + return self._to_alpha + + +# ═══════════════════════════════════════════════════════════════ +# ScaleAnimation +# ═══════════════════════════════════════════════════════════════ + +class ScaleAnimation(Animation): + """ScaleAnimation — 缩放动画""" + + def __init__(self, from_x: float, to_x: float, from_y: float, to_y: float, + pivot_x: float = 0.0, pivot_y: float = 0.0, + pivot_x_type: int = Animation.ABSOLUTE, + pivot_y_type: int = Animation.ABSOLUTE): + super().__init__() + self._from_x = from_x + self._to_x = to_x + self._from_y = from_y + self._to_y = to_y + self._pivot_x = pivot_x + self._pivot_y = pivot_y + self._pivot_x_type = pivot_x_type + self._pivot_y_type = pivot_y_type + + def _apply_transformation(self, interpolated_time: float, t: Transformation): + sx = self._from_x + (self._to_x - self._from_x) * interpolated_time + sy = self._from_y + (self._to_y - self._from_y) * interpolated_time + # 占位:Matrix 的缩放操作 + if t.getMatrix() is not None: + pass + + def getFromX(self) -> float: + return self._from_x + + def getToX(self) -> float: + return self._to_x + + def getFromY(self) -> float: + return self._from_y + + def getToY(self) -> float: + return self._to_y + + def getPivotX(self) -> float: + return self._pivot_x + + def getPivotY(self) -> float: + return self._pivot_y + + +# ═══════════════════════════════════════════════════════════════ +# TranslateAnimation +# ═══════════════════════════════════════════════════════════════ + +class TranslateAnimation(Animation): + """TranslateAnimation — 平移动画""" + + def __init__(self, from_x: float, to_x: float, from_y: float, to_y: float, + from_x_type: int = Animation.ABSOLUTE, + to_x_type: int = Animation.ABSOLUTE, + from_y_type: int = Animation.ABSOLUTE, + to_y_type: int = Animation.ABSOLUTE): + super().__init__() + self._from_x = from_x + self._to_x = to_x + self._from_y = from_y + self._to_y = to_y + self._from_x_type = from_x_type + self._to_x_type = to_x_type + self._from_y_type = from_y_type + self._to_y_type = to_y_type + + def _apply_transformation(self, interpolated_time: float, t: Transformation): + dx = self._from_x + (self._to_x - self._from_x) * interpolated_time + dy = self._from_y + (self._to_y - self._from_y) * interpolated_time + if t.getMatrix() is not None: + pass + + def getFromX(self) -> float: + return self._from_x + + def getToX(self) -> float: + return self._to_x + + def getFromY(self) -> float: + return self._from_y + + def getToY(self) -> float: + return self._to_y + + +# ═══════════════════════════════════════════════════════════════ +# RotateAnimation +# ═══════════════════════════════════════════════════════════════ + +class RotateAnimation(Animation): + """RotateAnimation — 旋转动画""" + + def __init__(self, from_degrees: float, to_degrees: float, + pivot_x: float = 0.0, pivot_y: float = 0.0, + pivot_x_type: int = Animation.ABSOLUTE, + pivot_y_type: int = Animation.ABSOLUTE): + super().__init__() + self._from_degrees = from_degrees + self._to_degrees = to_degrees + self._pivot_x = pivot_x + self._pivot_y = pivot_y + self._pivot_x_type = pivot_x_type + self._pivot_y_type = pivot_y_type + + def _apply_transformation(self, interpolated_time: float, t: Transformation): + degrees = self._from_degrees + (self._to_degrees - self._from_degrees) * interpolated_time + if t.getMatrix() is not None: + pass + + def getFromDegrees(self) -> float: + return self._from_degrees + + def getToDegrees(self) -> float: + return self._to_degrees + + def getPivotX(self) -> float: + return self._pivot_x + + def getPivotY(self) -> float: + return self._pivot_y + + +# ═══════════════════════════════════════════════════════════════ +# AnimationSet +# ═══════════════════════════════════════════════════════════════ + +class AnimationSet(Animation): + """AnimationSet — 动画集合""" + + def __init__(self, share_interpolator: bool = True): + super().__init__() + self._share_interpolator = share_interpolator + self._animations: List[Animation] = [] + + def addAnimation(self, animation: Animation): + self._animations.append(animation) + if self._share_interpolator and self._interpolator: + animation.setInterpolator(self._interpolator) + + def getAnimations(self) -> List[Animation]: + return list(self._animations) + + def setDuration(self, duration_millis: int): + super().setDuration(duration_millis) + for anim in self._animations: + anim.setDuration(duration_millis) + + def setFillAfter(self, fill_after: bool): + super().setFillAfter(fill_after) + for anim in self._animations: + anim.setFillAfter(fill_after) + + def setFillBefore(self, fill_before: bool): + super().setFillBefore(fill_before) + for anim in self._animations: + anim.setFillBefore(fill_before) + + def setRepeatMode(self, repeat_mode: int): + super().setRepeatMode(repeat_mode) + for anim in self._animations: + anim.setRepeatMode(repeat_mode) + + def setRepeatCount(self, repeat_count: int): + super().setRepeatCount(repeat_count) + for anim in self._animations: + anim.setRepeatCount(repeat_count) + + def setStartOffset(self, start_offset: int): + super().setStartOffset(start_offset) + for anim in self._animations: + anim.setStartOffset(start_offset) + + def setInterpolator(self, interpolator: Optional[Interpolator]): + super().setInterpolator(interpolator) + if self._share_interpolator: + for anim in self._animations: + anim.setInterpolator(interpolator) + + def start(self): + super().start() + for anim in self._animations: + anim.start() + + def reset(self): + super().reset() + for anim in self._animations: + anim.reset() + + def cancel(self): + super().cancel() + for anim in self._animations: + anim.cancel() + + def getTransformation(self, current_time: int, + out_transformation: Transformation) -> bool: + more = False + for anim in self._animations: + local_t = Transformation() + if anim.getTransformation(current_time, local_t): + more = True + out_transformation.compose(local_t) + return more + + def initialize(self, width: int, height: int, parent_width: int, parent_height: int): + super().initialize(width, height, parent_width, parent_height) + for anim in self._animations: + anim.initialize(width, height, parent_width, parent_height) + + def willChangeBounds(self) -> bool: + for anim in self._animations: + if anim.willChangeBounds(): + return True + return False + + def willChangeTransformationMatrix(self) -> bool: + for anim in self._animations: + if anim.willChangeTransformationMatrix(): + return True + return False + + def getAnimations(self) -> List[Animation]: + return list(self._animations) + + def restrictDuration(self, duration_millis: int): + super().restrictDuration(duration_millis) + for anim in self._animations: + anim.restrictDuration(duration_millis) + + def scaleCurrentDuration(self, scale: float): + super().scaleCurrentDuration(scale) + for anim in self._animations: + anim.scaleCurrentDuration(scale) + + +# ═══════════════════════════════════════════════════════════════ +# Animation.AnimationListener (已定义在 Animation 类中) +# ═══════════════════════════════════════════════════════════════ + +# Animation.AnimationListener is defined inside the Animation class above + + +# ═══════════════════════════════════════════════════════════════ +# AnimationUtils +# ═══════════════════════════════════════════════════════════════ + +class AnimationUtils(Stub): + """AnimationUtils — 动画工具类""" + + @staticmethod + def loadAnimation(context: Any, id: int) -> Animation: + """加载动画资源(占位实现)""" + sys.stderr.write(f"[AnimationUtils] loadAnimation: id={id}\n") + return Animation() + + @staticmethod + def makeInAnimation(context: Any, from_left: bool) -> Animation: + """创建进入动画""" + anim = AlphaAnimation(0.0, 1.0) + anim.setDuration(300) + return anim + + @staticmethod + def makeOutAnimation(context: Any, to_left: bool) -> Animation: + """创建退出动画""" + anim = AlphaAnimation(1.0, 0.0) + anim.setDuration(300) + return anim + + @staticmethod + def makeInChildBottomAnimation(context: Any) -> Animation: + """创建子视图从底部进入动画""" + anim = TranslateAnimation(0, 0, 1.0, 0.0, + from_x_type=Animation.RELATIVE_TO_SELF, + to_x_type=Animation.RELATIVE_TO_SELF, + from_y_type=Animation.RELATIVE_TO_SELF, + to_y_type=Animation.RELATIVE_TO_SELF) + anim.setDuration(300) + return anim + + @staticmethod + def currentAnimationTimeMillis() -> int: + return int(time.time() * 1000) + + +# ═══════════════════════════════════════════════════════════════ +# LayoutAnimationController +# ═══════════════════════════════════════════════════════════════ + +class LayoutAnimationController(Stub): + """LayoutAnimationController — 布局动画控制器""" + + ORDER_NORMAL = 0 + ORDER_REVERSE = 1 + ORDER_RANDOM = 2 + + def __init__(self, animation: Optional[Animation] = None): + super().__init__() + self._animation: Optional[Animation] = animation + self._delay: float = 0.5 + self._order: int = LayoutAnimationController.ORDER_NORMAL + self._initialized: bool = False + self._max_delay: int = 0 + + def setAnimation(self, animation: Animation): + self._animation = animation + + def getAnimation(self) -> Optional[Animation]: + return self._animation + + def setOrder(self, order: int): + self._order = order + + def getOrder(self) -> int: + return self._order + + def setDelay(self, delay: float): + self._delay = delay + + def getDelay(self) -> float: + return self._delay + + def getDelayForView(self, view: Any) -> int: + if self._order == LayoutAnimationController.ORDER_NORMAL: + return 0 + elif self._order == LayoutAnimationController.ORDER_REVERSE: + return 0 + elif self._order == LayoutAnimationController.ORDER_RANDOM: + import random + return int(random.random() * self._max_delay) + return 0 + + def willOverlap(self) -> bool: + return self._delay < 1.0 + + def start(self): + self._initialized = True + + def isDone(self) -> bool: + return True + + def getAnimationForView(self, view: Any) -> Optional[Animation]: + return self._animation + + def setInitialized(self, initialized: bool): + self._initialized = initialized + + +# ═══════════════════════════════════════════════════════════════ +# GridLayoutAnimationController +# ═══════════════════════════════════════════════════════════════ + +class GridLayoutAnimationController(LayoutAnimationController): + """GridLayoutAnimationController — 网格布局动画控制器""" + + DIRECTION_LEFT_TO_RIGHT = 0x0 + DIRECTION_RIGHT_TO_LEFT = 0x1 + DIRECTION_TOP_TO_BOTTOM = 0x2 + DIRECTION_BOTTOM_TO_TOP = 0x3 + DIRECTION_HORIZONTAL_MASK = 0x1 + DIRECTION_VERTICAL_MASK = 0x2 + + def __init__(self, animation: Optional[Animation] = None): + super().__init__(animation) + self._column_delay: float = 0.5 + self._row_delay: float = 0.5 + self._direction: int = GridLayoutAnimationController.DIRECTION_LEFT_TO_RIGHT + self._columns: int = 1 + self._rows: int = 1 + + def setColumnDelay(self, column_delay: float): + self._column_delay = column_delay + + def getColumnDelay(self) -> float: + return self._column_delay + + def setRowDelay(self, row_delay: float): + self._row_delay = row_delay + + def getRowDelay(self) -> float: + return self._row_delay + + def setDirection(self, direction: int): + self._direction = direction + + def getDirection(self) -> int: + return self._direction + + def setColumns(self, columns: int): + self._columns = columns + + def getColumns(self) -> int: + return self._columns + + def setRows(self, rows: int): + self._rows = rows + + def getRows(self) -> int: + return self._rows + + def getDelayForView(self, view: Any) -> int: + duration = 0 + if self._animation: + duration = self._animation.getDuration() + return int(duration * self._column_delay) + + def willOverlap(self) -> bool: + return self._column_delay < 1.0 or self._row_delay < 1.0 + + def getAnimationForView(self, view: Any) -> Optional[Animation]: + return self._animation + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "Animation", + "AnimationSet", + "AlphaAnimation", + "ScaleAnimation", + "TranslateAnimation", + "RotateAnimation", + "AnimationUtils", + "Interpolator", + "AccelerateInterpolator", + "DecelerateInterpolator", + "AccelerateDecelerateInterpolator", + "LinearInterpolator", + "OvershootInterpolator", + "AnticipateInterpolator", + "AnticipateOvershootInterpolator", + "BounceInterpolator", + "CycleInterpolator", + "PathInterpolator", + "Transformation", + "LayoutAnimationController", + "GridLayoutAnimationController", +] \ No newline at end of file diff --git a/android_pyapi/android/view_inputmethod.py b/android_pyapi/android/view_inputmethod.py new file mode 100644 index 0000000..4da8dab --- /dev/null +++ b/android_pyapi/android/view_inputmethod.py @@ -0,0 +1,859 @@ +""" +Android View InputMethod 组件 — InputMethodManager, EditorInfo, +InputConnection, ExtractedText 等 +方法签名完全对齐 Android SDK +""" + +import sys +from typing import Any, Dict, List, Optional, Tuple + +from .._core import Stub, AndroidOnlyError, android_context + + +# ═══════════════════════════════════════════════════════════════ +# InputMethodManager +# ═══════════════════════════════════════════════════════════════ + +class InputMethodManager(Stub): + """InputMethodManager — 输入法管理器""" + + SHOW_IMPLICIT = 0x0001 + SHOW_FORCED = 0x0002 + HIDE_IMPLICIT_ONLY = 0x0001 + HIDE_NOT_ALWAYS = 0x0002 + RESULT_SHOWN = 2 + RESULT_HIDDEN = 3 + RESULT_UNCHANGED_SHOWN = 4 + RESULT_UNCHANGED_HIDDEN = 5 + + def __init__(self): + super().__init__() + self._active: bool = False + self._current_input_connection: Optional["InputConnection"] = None + self._is_accepting_text: bool = True + self._fullscreen_mode: bool = False + self._cursor_x: int = 0 + self._cursor_y: int = 0 + self._selection_start: int = -1 + self._selection_end: int = -1 + self._composing_start: int = -1 + self._composing_end: int = -1 + + def showSoftInput(self, view: Any, flags: int, + result_receiver: Any = None) -> bool: + self._active = True + sys.stderr.write(f"[InputMethodManager] showSoftInput: flags={flags}\n") + return True + + def hideSoftInputFromWindow(self, window_token: Any, flags: int, + result_receiver: Any = None) -> bool: + self._active = False + sys.stderr.write(f"[InputMethodManager] hideSoftInputFromWindow: flags={flags}\n") + return True + + def toggleSoftInput(self, show_flags: int, hide_flags: int): + self._active = not self._active + sys.stderr.write(f"[InputMethodManager] toggleSoftInput: " + f"show={show_flags}, hide={hide_flags}\n") + + def restartInput(self, view: Any): + sys.stderr.write("[InputMethodManager] restartInput\n") + + def startInput(self, view: Any) -> bool: + sys.stderr.write("[InputMethodManager] startInput\n") + return True + + def updateSelection(self, view: Any, sel_start: int, sel_end: int, + composing_start: int, composing_end: int): + self._selection_start = sel_start + self._selection_end = sel_end + self._composing_start = composing_start + self._composing_end = composing_end + + def updateCursorAnchorInfo(self, view: Any, cursor_anchor_info: "CursorAnchorInfo"): + self._cursor_x = cursor_anchor_info.getInsertionMarkerBaseline() + self._cursor_y = cursor_anchor_info.getInsertionMarkerTop() + + def displayCompletions(self, view: Any, completions: List["CompletionInfo"]): + sys.stderr.write(f"[InputMethodManager] displayCompletions: " + f"{len(completions) if completions else 0} items\n") + + def sendKeyChar(self, key_char: str): + sys.stderr.write(f"[InputMethodManager] sendKeyChar: '{key_char}'\n") + + def clearMetaKeyStates(self, view: Any, states: int): + sys.stderr.write(f"[InputMethodManager] clearMetaKeyStates: {states}\n") + + def isAcceptingText(self) -> bool: + return self._is_accepting_text + + def isActive(self) -> bool: + return self._active + + def isFullscreenMode(self) -> bool: + return self._fullscreen_mode + + def setFullscreenMode(self, fullscreen: bool): + self._fullscreen_mode = fullscreen + + def getCurrentInputMethodSubtype(self) -> Optional["InputMethodSubtype"]: + return None + + def getShortcutInputMethodsAndSubtypes(self) -> List[Any]: + return [] + + def showStatusIcon(self, icon: Any): + pass + + def hideStatusIcon(self): + pass + + def registerSuggestionSpansForNotification(self, spans: List[Any], sender: Any): + pass + + def notifySuggestionPicked(self, suggestion: Any): + pass + + def windowDismissed(self, window_token: Any): + self._active = False + + def toString(self) -> str: + return f"InputMethodManager(active={self._active})" + + +# ═══════════════════════════════════════════════════════════════ +# EditorInfo +# ═══════════════════════════════════════════════════════════════ + +class EditorInfo(Stub): + """EditorInfo — 编辑器信息""" + + TYPE_CLASS_TEXT = 0x0001 + TYPE_CLASS_NUMBER = 0x0002 + TYPE_CLASS_PHONE = 0x0004 + TYPE_CLASS_DATETIME = 0x0008 + + TYPE_TEXT_VARIATION_NORMAL = 0x0000 + TYPE_TEXT_VARIATION_URI = 0x0010 + TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 0x0020 + TYPE_TEXT_VARIATION_EMAIL_SUBJECT = 0x0030 + TYPE_TEXT_VARIATION_SHORT_MESSAGE = 0x0040 + TYPE_TEXT_VARIATION_LONG_MESSAGE = 0x0050 + TYPE_TEXT_VARIATION_PERSON_NAME = 0x0060 + TYPE_TEXT_VARIATION_POSTAL_ADDRESS = 0x0070 + TYPE_TEXT_VARIATION_PASSWORD = 0x0080 + TYPE_TEXT_VARIATION_VISIBLE_PASSWORD = 0x0090 + TYPE_TEXT_VARIATION_WEB_EDIT_TEXT = 0x00A0 + TYPE_TEXT_VARIATION_FILTER = 0x00B0 + TYPE_TEXT_VARIATION_PHONETIC = 0x00C0 + TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS = 0x00D0 + TYPE_TEXT_VARIATION_WEB_PASSWORD = 0x00E0 + + TYPE_NUMBER_VARIATION_NORMAL = 0x0000 + TYPE_NUMBER_VARIATION_PASSWORD = 0x0010 + + TYPE_DATETIME_VARIATION_NORMAL = 0x0000 + TYPE_DATETIME_VARIATION_DATE = 0x0010 + TYPE_DATETIME_VARIATION_TIME = 0x0020 + + TYPE_NULL = 0x0000 + + IME_ACTION_NONE = 0x0001 + IME_ACTION_GO = 0x0002 + IME_ACTION_SEARCH = 0x0003 + IME_ACTION_SEND = 0x0004 + IME_ACTION_NEXT = 0x0005 + IME_ACTION_DONE = 0x0006 + IME_ACTION_PREVIOUS = 0x0007 + + IME_FLAG_NO_EXTRACT_UI = 0x10000001 + IME_FLAG_NO_ACCESSORY_ACTION = 0x20000000 + IME_FLAG_NO_ENTER_ACTION = 0x40000000 + IME_FLAG_NAVIGATE_NEXT = 0x08000000 + IME_FLAG_NAVIGATE_PREVIOUS = 0x04000000 + IME_FLAG_NO_FULLSCREEN = 0x02000000 + IME_FLAG_FORCE_ASCII = 0x80000000 + + IME_MASK_ACTION = 0x000000FF + + def __init__(self): + super().__init__() + self.inputType: int = EditorInfo.TYPE_CLASS_TEXT + self.imeOptions: int = EditorInfo.IME_ACTION_NONE + self.actionLabel: Optional[str] = None + self.actionId: int = 0 + self.initialSelStart: int = -1 + self.initialSelEnd: int = -1 + self.initialCapsMode: int = 0 + self.hintText: Optional[str] = None + self.label: Optional[str] = None + self.packageName: Optional[str] = None + self.fieldId: int = 0 + self.fieldName: Optional[str] = None + self.extras: Optional[Any] = None + + def toString(self) -> str: + return (f"EditorInfo(inputType={hex(self.inputType)}, " + f"imeOptions={hex(self.imeOptions)}, " + f"actionLabel={self.actionLabel})") + + def dump(self, printer: Any, prefix: str): + sys.stderr.write(f"{prefix}EditorInfo: inputType={hex(self.inputType)}\n") + + +# ═══════════════════════════════════════════════════════════════ +# InputConnection +# ═══════════════════════════════════════════════════════════════ + +class InputConnection(Stub): + """InputConnection — 输入连接接口""" + + def __init__(self): + super().__init__() + self._text: str = "" + self._selection_start: int = 0 + self._selection_end: int = 0 + self._composing_start: int = -1 + self._composing_end: int = -1 + self._batch_edit_count: int = 0 + + def getTextBeforeCursor(self, length: int, flags: int) -> Optional[str]: + end = self._selection_start + start = max(0, end - length) + return self._text[start:end] + + def getTextAfterCursor(self, length: int, flags: int) -> Optional[str]: + start = self._selection_end + end = min(len(self._text), start + length) + return self._text[start:end] + + def getExtractedText(self, request: "ExtractedTextRequest", + flags: int) -> Optional["ExtractedText"]: + result = ExtractedText() + result.text = self._text + result.startOffset = 0 + result.selectionStart = self._selection_start + result.selectionEnd = self._selection_end + result.flags = 0 + return result + + def commitText(self, text: Any, new_cursor_position: int) -> bool: + if isinstance(text, str): + committed = text + else: + committed = str(text) if text else "" + + before = self._text[:self._selection_start] + after = self._text[self._selection_end:] + self._text = before + committed + after + + if new_cursor_position > 0: + self._selection_start = len(before) + new_cursor_position - 1 + else: + self._selection_start = len(before) + len(committed) + new_cursor_position + self._selection_end = self._selection_start + self._composing_start = -1 + self._composing_end = -1 + return True + + def setComposingText(self, text: Any, new_cursor_position: int) -> bool: + if self._composing_start < 0: + self._composing_start = self._selection_start + self._composing_end = self._selection_end + + before = self._text[:self._composing_start] + after = self._text[self._composing_end:] + + if isinstance(text, str): + composing = text + else: + composing = str(text) if text else "" + + self._text = before + composing + after + self._composing_end = self._composing_start + len(composing) + self._selection_start = self._composing_end + new_cursor_position + self._selection_end = self._selection_start + return True + + def setSelection(self, start: int, end: int) -> bool: + self._selection_start = max(0, min(start, len(self._text))) + self._selection_end = max(0, min(end, len(self._text))) + return True + + def deleteSurroundingText(self, before_length: int, after_length: int) -> bool: + start = max(0, self._selection_start - before_length) + end = min(len(self._text), self._selection_end + after_length) + self._text = self._text[:start] + self._text[end:] + new_cursor = max(0, self._selection_start - before_length) + self._selection_start = new_cursor + self._selection_end = new_cursor + return True + + def commitCompletion(self, completion: "CompletionInfo") -> bool: + sys.stderr.write(f"[InputConnection] commitCompletion: " + f"{completion.getText()}\n") + return True + + def commitCorrection(self, correction: "CorrectionInfo") -> bool: + sys.stderr.write(f"[InputConnection] commitCorrection: " + f"{correction.getNewText()}\n") + return True + + def getSelectedText(self, flags: int) -> Optional[str]: + if self._selection_start == self._selection_end: + return "" + return self._text[self._selection_start:self._selection_end] + + def finishComposingText(self) -> bool: + self._composing_start = -1 + self._composing_end = -1 + return True + + def beginBatchEdit(self) -> bool: + self._batch_edit_count += 1 + return True + + def endBatchEdit(self) -> bool: + if self._batch_edit_count > 0: + self._batch_edit_count -= 1 + return True + + def sendKeyEvent(self, event: Any) -> bool: + sys.stderr.write(f"[InputConnection] sendKeyEvent\n") + return True + + def clearMetaKeyStates(self, states: int) -> bool: + return True + + def performEditorAction(self, action_code: int) -> bool: + sys.stderr.write(f"[InputConnection] performEditorAction: {action_code}\n") + return True + + def performContextMenuAction(self, id: int) -> bool: + sys.stderr.write(f"[InputConnection] performContextMenuAction: {id}\n") + return True + + def commitContent(self, input_content: "InputContentInfo", + flags: int, opts: Optional[Any] = None) -> bool: + sys.stderr.write("[InputConnection] commitContent\n") + return True + + def requestCursorUpdates(self, cursor_update_mode: int) -> bool: + return True + + def closeConnection(self): + pass + + def reportFullscreenMode(self, enabled: bool): + pass + + def getSurroundingText(self, before_length: int, after_length: int, + flags: int) -> Optional["SurroundingText"]: + start = max(0, self._selection_start - before_length) + end = min(len(self._text), self._selection_end + after_length) + return SurroundingText( + text=self._text[start:end], + selection_start=self._selection_start - start, + selection_end=self._selection_end - start, + offset=start, + ) + + def takeSnapshot(self) -> Optional[Any]: + """获取输入快照""" + return None + + def setComposingRegion(self, start: int, end: int) -> bool: + self._composing_start = start + self._composing_end = end + return True + + def getCursorCapsMode(self, req_modes: int) -> int: + return 0 + + def getTextBeforeCursor(self, length: int, flags: int) -> Optional[str]: + end = self._selection_start + start = max(0, end - length) + return self._text[start:end] + + def getTextAfterCursor(self, length: int, flags: int) -> Optional[str]: + start = self._selection_end + end = min(len(self._text), start + length) + return self._text[start:end] + + +# ═══════════════════════════════════════════════════════════════ +# ExtractedText +# ═══════════════════════════════════════════════════════════════ + +class ExtractedText(Stub): + """ExtractedText — 提取的文本""" + + FLAG_SINGLE_LINE = 0x0001 + FLAG_SELECTING = 0x0002 + + def __init__(self): + super().__init__() + self.text: Optional[str] = None + self.startOffset: int = 0 + self.partialStartOffset: int = -1 + self.partialEndOffset: int = -1 + self.selectionStart: int = -1 + self.selectionEnd: int = -1 + self.flags: int = 0 + + def toString(self) -> str: + return (f"ExtractedText(startOffset={self.startOffset}, " + f"text={self.text}, " + f"selection=({self.selectionStart}, {self.selectionEnd}))") + + +# ═══════════════════════════════════════════════════════════════ +# ExtractedTextRequest +# ═══════════════════════════════════════════════════════════════ + +class ExtractedTextRequest(Stub): + """ExtractedTextRequest — 提取文本请求""" + + def __init__(self): + super().__init__() + self.token: int = 0 + self.flags: int = 0 + self.hintMaxLines: int = 0 + self.hintMaxChars: int = 0 + + +# ═══════════════════════════════════════════════════════════════ +# InputBinding +# ═══════════════════════════════════════════════════════════════ + +class InputBinding(Stub): + """InputBinding — 输入绑定""" + + def __init__(self, connection: Optional["InputConnection"] = None, + uid: int = -1, pid: int = -1, + private_ime_options: str = ""): + super().__init__() + self._connection: Optional["InputConnection"] = connection + self._uid: int = uid + self._pid: int = pid + self._private_ime_options: str = private_ime_options + + def getConnection(self) -> Optional["InputConnection"]: + return self._connection + + def getUid(self) -> int: + return self._uid + + def getPid(self) -> int: + return self._pid + + def getPrivateImeOptions(self) -> str: + return self._private_ime_options + + def toString(self) -> str: + return (f"InputBinding(uid={self._uid}, pid={self._pid}, " + f"connection={self._connection})") + + +# ═══════════════════════════════════════════════════════════════ +# InputMethodSubtype +# ═══════════════════════════════════════════════════════════════ + +class InputMethodSubtype(Stub): + """InputMethodSubtype — 输入法子类型""" + + SUBTYPE_ID_NONE = -1 + + def __init__(self, locale: str = "", mode: str = "", + extra_value: str = "", is_auxiliary: bool = False, + overrides_ime_theme: bool = False, + subtype_id: int = SUBTYPE_ID_NONE): + super().__init__() + self._locale: str = locale + self._mode: str = mode + self._extra_value: str = extra_value + self._is_auxiliary: bool = is_auxiliary + self._overrides_ime_theme: bool = overrides_ime_theme + self._subtype_id: int = subtype_id + self._display_name_res: int = 0 + self._icon_res: int = 0 + + def getLocale(self) -> str: + return self._locale + + def getMode(self) -> str: + return self._mode + + def getExtraValue(self) -> str: + return self._extra_value + + def getSubtypeId(self) -> int: + return self._subtype_id + + def containsExtraValueKey(self, key: str) -> bool: + if not self._extra_value: + return False + pairs = self._extra_value.split(",") + for pair in pairs: + kv = pair.split("=", 1) + if len(kv) == 2 and kv[0].strip() == key: + return True + return False + + def getDisplayName(self, context: Any, package_name: str, + app_info: Any) -> str: + return self._locale or self._mode + + def isAuxiliary(self) -> bool: + return self._is_auxiliary + + def overridesImeTheme(self) -> bool: + return self._overrides_ime_theme + + def hashCode(self) -> int: + return hash((self._locale, self._mode, self._extra_value)) & 0x7FFFFFFF + + def equals(self, other: Any) -> bool: + if isinstance(other, InputMethodSubtype): + return (self._locale == other._locale and + self._mode == other._mode and + self._extra_value == other._extra_value) + return False + + def toString(self) -> str: + return (f"InputMethodSubtype(locale={self._locale}, " + f"mode={self._mode})") + + +# ═══════════════════════════════════════════════════════════════ +# CursorAnchorInfo +# ═══════════════════════════════════════════════════════════════ + +class CursorAnchorInfo(Stub): + """CursorAnchorInfo — 光标锚点信息""" + + FLAG_HAS_VISIBLE_REGION = 0x01 + FLAG_IS_RTL = 0x02 + + def __init__(self): + super().__init__() + self._insertion_marker_baseline: float = 0.0 + self._insertion_marker_top: float = 0.0 + self._insertion_marker_bottom: float = 0.0 + self._matrix: Optional[Any] = None + self._character_bounds: Dict[int, Tuple[float, float, float, float]] = {} + self._character_bounds_flags: Dict[int, int] = {} + self._composing_text: Optional[str] = None + self._composing_text_start: int = -1 + + def getInsertionMarkerBaseline(self) -> float: + return self._insertion_marker_baseline + + def setInsertionMarkerBaseline(self, baseline: float): + self._insertion_marker_baseline = baseline + + def getInsertionMarkerTop(self) -> float: + return self._insertion_marker_top + + def setInsertionMarkerTop(self, top: float): + self._insertion_marker_top = top + + def getInsertionMarkerBottom(self) -> float: + return self._insertion_marker_bottom + + def setInsertionMarkerBottom(self, bottom: float): + self._insertion_marker_bottom = bottom + + def getMatrix(self) -> Optional[Any]: + return self._matrix + + def setMatrix(self, matrix: Any): + self._matrix = matrix + + def getCharacterBounds(self, index: int) -> Optional[Tuple[float, float, float, float]]: + return self._character_bounds.get(index) + + def setCharacterBounds(self, index: int, left: float, top: float, + right: float, bottom: float, flags: int): + self._character_bounds[index] = (left, top, right, bottom) + self._character_bounds_flags[index] = flags + + def getCharacterBoundsFlags(self, index: int) -> int: + return self._character_bounds_flags.get(index, 0) + + def getComposingText(self) -> Optional[str]: + return self._composing_text + + def getComposingTextStart(self) -> int: + return self._composing_text_start + + def toString(self) -> str: + return (f"CursorAnchorInfo(baseline={self._insertion_marker_baseline}, " + f"top={self._insertion_marker_top})") + + +# ═══════════════════════════════════════════════════════════════ +# CorrectionInfo +# ═══════════════════════════════════════════════════════════════ + +class CorrectionInfo(Stub): + """CorrectionInfo — 校正信息""" + + def __init__(self, old_text: str = "", new_text: str = ""): + super().__init__() + self._old_text: str = old_text + self._new_text: str = new_text + + def getOldText(self) -> str: + return self._old_text + + def getNewText(self) -> str: + return self._new_text + + def toString(self) -> str: + return f"CorrectionInfo('{self._old_text}' -> '{self._new_text}')" + + +# ═══════════════════════════════════════════════════════════════ +# CompletionInfo +# ═══════════════════════════════════════════════════════════════ + +class CompletionInfo(Stub): + """CompletionInfo — 补全信息""" + + def __init__(self, id: int = 0, position: int = 0, + text: str = "", label: str = ""): + super().__init__() + self._id: int = id + self._position: int = position + self._text: str = text + self._label: str = label + + def getId(self) -> int: + return self._id + + def getPosition(self) -> int: + return self._position + + def getText(self) -> str: + return self._text + + def getLabel(self) -> str: + return self._label + + def toString(self) -> str: + return f"CompletionInfo(id={self._id}, text='{self._text}')" + + +# ═══════════════════════════════════════════════════════════════ +# SurroundingText +# ═══════════════════════════════════════════════════════════════ + +class SurroundingText(Stub): + """SurroundingText — 周围文本""" + + def __init__(self, text: str = "", selection_start: int = 0, + selection_end: int = 0, offset: int = 0): + super().__init__() + self._text: str = text + self._selection_start: int = selection_start + self._selection_end: int = selection_end + self._offset: int = offset + + def getText(self) -> str: + return self._text + + def getSelectionStart(self) -> int: + return self._selection_start + + def getSelectionEnd(self) -> int: + return self._selection_end + + def getOffset(self) -> int: + return self._offset + + def toString(self) -> str: + return (f"SurroundingText(text='{self._text}', " + f"selection=({self._selection_start}, {self._selection_end}), " + f"offset={self._offset})") + + +# ═══════════════════════════════════════════════════════════════ +# InputContentInfo +# ═══════════════════════════════════════════════════════════════ + +class InputContentInfo(Stub): + """InputContentInfo — 输入内容信息""" + + def __init__(self, uri: Any = None, description: Any = None, + link_uri: Any = None): + super().__init__() + self._uri: Any = uri + self._description: Any = description + self._link_uri: Any = link_uri + + def getUri(self) -> Any: + return self._uri + + def getDescription(self) -> Any: + return self._description + + def getLinkUri(self) -> Any: + return self._link_uri + + def requestPermission(self): + pass + + def releasePermission(self): + pass + + def toString(self) -> str: + return (f"InputContentInfo(uri={self._uri}, " + f"description={self._description})") + + +# ═══════════════════════════════════════════════════════════════ +# InputConnectionWrapper +# ═══════════════════════════════════════════════════════════════ + +class InputConnectionWrapper(InputConnection): + """InputConnectionWrapper — 输入连接包装器""" + + def __init__(self, target: InputConnection, mutable: bool = True): + super().__init__() + self._target: InputConnection = target + self._mutable: bool = mutable + + def setTarget(self, target: InputConnection): + if self._mutable: + self._target = target + + def getTarget(self) -> InputConnection: + return self._target + + def getTextBeforeCursor(self, length: int, flags: int) -> Optional[str]: + return self._target.getTextBeforeCursor(length, flags) + + def getTextAfterCursor(self, length: int, flags: int) -> Optional[str]: + return self._target.getTextAfterCursor(length, flags) + + def getSelectedText(self, flags: int) -> Optional[str]: + return self._target.getSelectedText(flags) + + def getSurroundingText(self, before_length: int, after_length: int, + flags: int) -> Optional["SurroundingText"]: + return self._target.getSurroundingText(before_length, after_length, flags) + + def getCursorCapsMode(self, req_modes: int) -> int: + return self._target.getCursorCapsMode(req_modes) + + def getExtractedText(self, request: "ExtractedTextRequest", + flags: int) -> Optional["ExtractedText"]: + return self._target.getExtractedText(request, flags) + + def commitText(self, text: Any, new_cursor_position: int) -> bool: + return self._target.commitText(text, new_cursor_position) + + def commitCompletion(self, completion: "CompletionInfo") -> bool: + return self._target.commitCompletion(completion) + + def commitCorrection(self, correction: "CorrectionInfo") -> bool: + return self._target.commitCorrection(correction) + + def commitContent(self, input_content: "InputContentInfo", + flags: int, opts: Optional[Any] = None) -> bool: + return self._target.commitContent(input_content, flags, opts) + + def setComposingText(self, text: Any, new_cursor_position: int) -> bool: + return self._target.setComposingText(text, new_cursor_position) + + def setComposingRegion(self, start: int, end: int) -> bool: + return self._target.setComposingRegion(start, end) + + def finishComposingText(self) -> bool: + return self._target.finishComposingText() + + def setSelection(self, start: int, end: int) -> bool: + return self._target.setSelection(start, end) + + def performEditorAction(self, action_code: int) -> bool: + return self._target.performEditorAction(action_code) + + def performContextMenuAction(self, id: int) -> bool: + return self._target.performContextMenuAction(id) + + def beginBatchEdit(self) -> bool: + return self._target.beginBatchEdit() + + def endBatchEdit(self) -> bool: + return self._target.endBatchEdit() + + def sendKeyEvent(self, event: Any) -> bool: + return self._target.sendKeyEvent(event) + + def clearMetaKeyStates(self, states: int) -> bool: + return self._target.clearMetaKeyStates(states) + + def deleteSurroundingText(self, before_length: int, after_length: int) -> bool: + return self._target.deleteSurroundingText(before_length, after_length) + + def requestCursorUpdates(self, cursor_update_mode: int) -> bool: + return self._target.requestCursorUpdates(cursor_update_mode) + + def closeConnection(self): + self._target.closeConnection() + + def reportFullscreenMode(self, enabled: bool): + self._target.reportFullscreenMode(enabled) + + +# ═══════════════════════════════════════════════════════════════ +# BaseInputConnection +# ═══════════════════════════════════════════════════════════════ + +class BaseInputConnection(InputConnection): + """BaseInputConnection — 输入连接基类""" + + def __init__(self, view: Any, full_editor: bool = True): + super().__init__() + self._view = view + self._full_editor = full_editor + self._text: str = "" + self._selection_start: int = 0 + self._selection_end: int = 0 + self._composing_start: int = -1 + self._composing_end: int = -1 + + def getEditable(self) -> Optional[Any]: + return None + + def reportFullscreenMode(self, enabled: bool): + pass + + def requestCursorUpdates(self, cursor_update_mode: int) -> bool: + return self._full_editor + + def closeConnection(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "InputMethodManager", + "EditorInfo", + "InputConnection", + "ExtractedText", + "ExtractedTextRequest", + "InputBinding", + "InputMethodSubtype", + "CursorAnchorInfo", + "CorrectionInfo", + "CompletionInfo", + "SurroundingText", + "InputContentInfo", + "InputConnectionWrapper", + "BaseInputConnection", +] \ No newline at end of file diff --git a/android_pyapi/android/webkit.py b/android_pyapi/android/webkit.py new file mode 100644 index 0000000..e8000c7 --- /dev/null +++ b/android_pyapi/android/webkit.py @@ -0,0 +1,992 @@ +""" +Android WebKit 组件 — WebView, WebSettings, WebViewClient, WebChromeClient 等 +方法签名完全对齐 Android SDK +""" + +import os +import sys +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .._core import ( + Stub, AndroidOnlyError, android_context, + resolve_android_path, to_android_path, + _sdcard_root, _app_data_dir, _package_name, +) + + +# ═══════════════════════════════════════════════════════════════ +# WebResourceRequest / WebResourceResponse / WebResourceError +# ═══════════════════════════════════════════════════════════════ + +class WebResourceRequest(Stub): + """WebResourceRequest — 资源请求""" + + def __init__(self, url: str = "", is_for_main_frame: bool = False, + is_redirect: bool = False, method: str = "GET", + request_headers: Optional[Dict[str, str]] = None): + super().__init__() + self._url = url + self._is_for_main_frame = is_for_main_frame + self._is_redirect = is_redirect + self._method = method + self._request_headers = request_headers or {} + + def getUrl(self) -> str: + return self._url + + def isForMainFrame(self) -> bool: + return self._is_for_main_frame + + def isRedirect(self) -> bool: + return self._is_redirect + + def getMethod(self) -> str: + return self._method + + def getRequestHeaders(self) -> Dict[str, str]: + return dict(self._request_headers) + + def toString(self) -> str: + return f"WebResourceRequest(url={self._url})" + + +class WebResourceResponse(Stub): + """WebResourceResponse — 资源响应""" + + def __init__(self, mime_type: str = "", encoding: str = "", + data: Any = None, status_code: int = 200, + reason_phrase: str = "OK", + response_headers: Optional[Dict[str, str]] = None): + super().__init__() + self._mime_type = mime_type + self._encoding = encoding + self._data = data + self._status_code = status_code + self._reason_phrase = reason_phrase + self._response_headers = response_headers or {} + + def getMimeType(self) -> str: + return self._mime_type + + def getEncoding(self) -> str: + return self._encoding + + def getData(self) -> Any: + return self._data + + def getStatusCode(self) -> int: + return self._status_code + + def getReasonPhrase(self) -> str: + return self._reason_phrase + + def getResponseHeaders(self) -> Dict[str, str]: + return dict(self._response_headers) + + +class WebResourceError(Stub): + """WebResourceError — 资源错误""" + + ERROR_UNKNOWN = -1 + ERROR_HOST_LOOKUP = -2 + ERROR_UNSUPPORTED_AUTH_SCHEME = -3 + ERROR_AUTHENTICATION = -4 + ERROR_PROXY_AUTHENTICATION = -5 + ERROR_CONNECT = -6 + ERROR_IO = -7 + ERROR_TIMEOUT = -8 + ERROR_REDIRECT_LOOP = -9 + ERROR_UNSUPPORTED_SCHEME = -10 + ERROR_FAILED_SSL_HANDSHAKE = -11 + ERROR_BAD_URL = -12 + ERROR_FILE = -13 + ERROR_FILE_NOT_FOUND = -14 + ERROR_TOO_MANY_REQUESTS = -15 + + def __init__(self, error_code: int = ERROR_UNKNOWN, description: str = ""): + super().__init__() + self._error_code = error_code + self._description = description + + def getErrorCode(self) -> int: + return self._error_code + + def getDescription(self) -> str: + return self._description + + +# ═══════════════════════════════════════════════════════════════ +# WebMessage / WebMessagePort +# ═══════════════════════════════════════════════════════════════ + +class WebMessage(Stub): + """WebMessage — Web 消息""" + + def __init__(self, data: str = ""): + super().__init__() + self._data = data + + def getData(self) -> str: + return self._data + + +class WebMessagePort(Stub): + """WebMessagePort — Web 消息端口""" + + def postMessage(self, message: WebMessage): + pass + + def close(self): + pass + + def setWebMessageCallback(self, callback: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# JavascriptInterface +# ═══════════════════════════════════════════════════════════════ + +class JavascriptInterface(Stub): + """JavascriptInterface — JavaScript 接口注解""" + + def __init__(self, value: str = ""): + super().__init__() + self._value = value + + def getValue(self) -> str: + return self._value + + +# ═══════════════════════════════════════════════════════════════ +# JsResult / JsPromptResult +# ═══════════════════════════════════════════════════════════════ + +class JsResult(Stub): + """JsResult — JS 对话框结果""" + + def __init__(self): + super().__init__() + self._confirmed = False + + def confirm(self): + self._confirmed = True + + def cancel(self): + self._confirmed = False + + +class JsPromptResult(JsResult): + """JsPromptResult — JS 提示结果""" + + def __init__(self): + super().__init__() + self._string_result: str = "" + + def confirm(self, result: str = ""): + self._confirmed = True + self._string_result = result + + def getStringResult(self) -> str: + return self._string_result + + +# ═══════════════════════════════════════════════════════════════ +# HttpAuthHandler / SslErrorHandler +# ═══════════════════════════════════════════════════════════════ + +class HttpAuthHandler(Stub): + """HttpAuthHandler — HTTP 认证处理器""" + + def __init__(self): + super().__init__() + self._proceeded = False + self._cancelled = False + + def proceed(self, username: str, password: str): + self._proceeded = True + + def cancel(self): + self._cancelled = True + + def useHttpAuthUsernamePassword(self) -> bool: + return False + + +class SslErrorHandler(Stub): + """SslErrorHandler — SSL 错误处理器""" + + def __init__(self): + super().__init__() + self._proceeded = False + self._cancelled = False + + def proceed(self): + self._proceeded = True + + def cancel(self): + self._cancelled = True + + +# ═══════════════════════════════════════════════════════════════ +# PermissionRequest +# ═══════════════════════════════════════════════════════════════ + +class PermissionRequest(Stub): + """PermissionRequest — 权限请求""" + + RESOURCE_VIDEO_CAPTURE = "android.webkit.resource.VIDEO_CAPTURE" + RESOURCE_AUDIO_CAPTURE = "android.webkit.resource.AUDIO_CAPTURE" + RESOURCE_PROTECTED_MEDIA_ID = "android.webkit.resource.PROTECTED_MEDIA_ID" + RESOURCE_MIDI_SYSEX = "android.webkit.resource.MIDI_SYSEX" + + def __init__(self): + super().__init__() + self._origin: Any = None + self._resources: List[str] = [] + self._granted_resources: List[str] = [] + + def getOrigin(self) -> Any: + return self._origin + + def getResources(self) -> List[str]: + return list(self._resources) + + def getGrantedResources(self) -> List[str]: + return list(self._granted_resources) + + def grant(self, resources: List[str]): + self._granted_resources = list(resources) + + def deny(self): + self._granted_resources.clear() + + +# ═══════════════════════════════════════════════════════════════ +# GeolocationPermissions / WebStorage +# ═══════════════════════════════════════════════════════════════ + +class GeolocationPermissions(Stub): + """GeolocationPermissions — 地理位置权限""" + + @staticmethod + def getInstance() -> "GeolocationPermissions": + return GeolocationPermissions() + + def allow(self, origin: str): + pass + + def clear(self, origin: str): + pass + + def clearAll(self): + pass + + def getAllowed(self, origin: str, callback: Any): + pass + + def getOrigins(self, callback: Any): + pass + + +class WebStorage(Stub): + """WebStorage — Web 存储""" + + @staticmethod + def getInstance() -> "WebStorage": + return WebStorage() + + def deleteAllData(self): + pass + + def deleteOrigin(self, origin: str): + pass + + def getOrigins(self, callback: Any): + pass + + def getQuotaForOrigin(self, origin: str, callback: Any): + pass + + def getUsageForOrigin(self, origin: str, callback: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# WebBackForwardList / WebHistoryItem +# ═══════════════════════════════════════════════════════════════ + +class WebHistoryItem(Stub): + """WebHistoryItem — 历史项""" + + def __init__(self): + super().__init__() + self._url: str = "" + self._original_url: str = "" + self._title: str = "" + self._favicon: Any = None + + def getUrl(self) -> str: + return self._url + + def getOriginalUrl(self) -> str: + return self._original_url + + def getTitle(self) -> str: + return self._title + + def getFavicon(self) -> Any: + return self._favicon + + def toString(self) -> str: + return f"WebHistoryItem(url={self._url})" + + +class WebBackForwardList(Stub): + """WebBackForwardList — 前进后退列表""" + + def __init__(self): + super().__init__() + self._items: List[WebHistoryItem] = [] + self._current_index: int = -1 + + def getCurrentItem(self) -> Optional[WebHistoryItem]: + if 0 <= self._current_index < len(self._items): + return self._items[self._current_index] + return None + + def getCurrentIndex(self) -> int: + return self._current_index + + def getItemAtIndex(self, index: int) -> Optional[WebHistoryItem]: + if 0 <= index < len(self._items): + return self._items[index] + return None + + def getSize(self) -> int: + return len(self._items) + + +# ═══════════════════════════════════════════════════════════════ +# CookieManager / CookieSyncManager +# ═══════════════════════════════════════════════════════════════ + +class CookieManager(Stub): + """CookieManager — Cookie 管理器""" + + _instance: Optional["CookieManager"] = None + + def __init__(self): + super().__init__() + self._accept_cookies: bool = True + self._accept_third_party: bool = True + self._cookies: Dict[str, str] = {} + + @staticmethod + def getInstance() -> "CookieManager": + if CookieManager._instance is None: + CookieManager._instance = CookieManager() + return CookieManager._instance + + def setAcceptCookies(self, accept: bool): + self._accept_cookies = accept + + def acceptCookies(self) -> bool: + return self._accept_cookies + + def setCookie(self, url: str, value: str): + if self._accept_cookies: + self._cookies[url] = value + + def getCookie(self, url: str) -> Optional[str]: + return self._cookies.get(url) + + def setAcceptThirdPartyCookies(self, webview: Any, accept: bool): + self._accept_third_party = accept + + def acceptThirdPartyCookies(self) -> bool: + return self._accept_third_party + + def removeSessionCookie(self): + pass + + def removeAllCookies(self): + self._cookies.clear() + + def removeAllCookiesSync(self): + self._cookies.clear() + + def removeSessionCookies(self): + pass + + def flush(self): + pass + + def hasCookies(self) -> bool: + return bool(self._cookies) + + def removeExpiredCookie(self): + pass + + +class CookieSyncManager(Stub): + """CookieSyncManager — Cookie 同步管理器""" + + _instance: Optional["CookieSyncManager"] = None + + @staticmethod + def createInstance(context: Any) -> "CookieSyncManager": + if CookieSyncManager._instance is None: + CookieSyncManager._instance = CookieSyncManager() + return CookieSyncManager._instance + + @staticmethod + def getInstance() -> "CookieSyncManager": + if CookieSyncManager._instance is None: + CookieSyncManager._instance = CookieSyncManager() + return CookieSyncManager._instance + + def sync(self): + pass + + def startSync(self): + pass + + def stopSync(self): + pass + + def resetSync(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# WebSettings +# ═══════════════════════════════════════════════════════════════ + +class WebSettings(Stub): + """WebSettings — WebView 设置""" + + LOAD_DEFAULT = -1 + LOAD_CACHE_ELSE_NETWORK = 1 + LOAD_NO_CACHE = 2 + LOAD_CACHE_ONLY = 3 + + MIXED_CONTENT_ALWAYS_ALLOW = 0 + MIXED_CONTENT_NEVER_ALLOW = 1 + MIXED_CONTENT_COMPATIBILITY_MODE = 2 + + class LayoutAlgorithm(Stub): + NORMAL = 0 + SINGLE_COLUMN = 1 + NARROW_COLUMNS = 2 + TEXT_AUTOSIZING = 3 + + def __init__(self): + super().__init__() + self._js_enabled: bool = True + self._dom_storage_enabled: bool = True + self._allow_file_access: bool = True + self._allow_content_access: bool = True + self._allow_file_access_from_urls: bool = False + self._allow_universal_access_from_urls: bool = False + self._built_in_zoom_controls: bool = True + self._display_zoom_controls: bool = True + self._support_zoom: bool = True + self._load_with_overview_mode: bool = False + self._use_wide_viewport: bool = False + self._user_agent_string: str = "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36" + self._default_text_encoding: str = "UTF-8" + self._cache_mode: int = self.LOAD_DEFAULT + self._app_cache_enabled: bool = False + self._app_cache_path: str = "" + self._database_enabled: bool = False + self._geolocation_enabled: bool = False + self._js_can_open_windows: bool = False + self._loads_images_automatically: bool = True + self._block_network_image: bool = False + self._block_network_loads: bool = False + self._minimum_font_size: int = 8 + self._default_font_size: int = 16 + self._text_zoom: int = 100 + self._layout_algorithm: int = WebSettings.LayoutAlgorithm.NORMAL + self._mixed_content_mode: int = self.MIXED_CONTENT_NEVER_ALLOW + self._offscreen_pre_raster: bool = False + self._safe_browsing_enabled: bool = True + self._javascript_can_open_windows_automatically: bool = False + + def setJavaScriptEnabled(self, enabled: bool): + self._js_enabled = enabled + + def setDomStorageEnabled(self, enabled: bool): + self._dom_storage_enabled = enabled + + def setAllowFileAccess(self, allow: bool): + self._allow_file_access = allow + + def setAllowContentAccess(self, allow: bool): + self._allow_content_access = allow + + def setAllowFileAccessFromFileURLs(self, allow: bool): + self._allow_file_access_from_urls = allow + + def setAllowUniversalAccessFromFileURLs(self, allow: bool): + self._allow_universal_access_from_urls = allow + + def setBuiltInZoomControls(self, enabled: bool): + self._built_in_zoom_controls = enabled + + def setDisplayZoomControls(self, enabled: bool): + self._display_zoom_controls = enabled + + def setSupportZoom(self, support: bool): + self._support_zoom = support + + def setLoadWithOverviewMode(self, overview: bool): + self._load_with_overview_mode = overview + + def setUseWideViewPort(self, use: bool): + self._use_wide_viewport = use + + def setUserAgentString(self, ua: str): + self._user_agent_string = ua + + def getUserAgentString(self) -> str: + return self._user_agent_string + + def setDefaultTextEncodingName(self, encoding: str): + self._default_text_encoding = encoding + + def getDefaultTextEncodingName(self) -> str: + return self._default_text_encoding + + def setCacheMode(self, mode: int): + self._cache_mode = mode + + def getCacheMode(self) -> int: + return self._cache_mode + + def setAppCacheEnabled(self, enabled: bool): + self._app_cache_enabled = enabled + + def setAppCachePath(self, path: str): + self._app_cache_path = path + + def setDatabaseEnabled(self, enabled: bool): + self._database_enabled = enabled + + def setGeolocationEnabled(self, enabled: bool): + self._geolocation_enabled = enabled + + def setJavaScriptCanOpenWindowsAutomatically(self, auto: bool): + self._javascript_can_open_windows_automatically = auto + + def setLoadsImagesAutomatically(self, auto: bool): + self._loads_images_automatically = auto + + def setBlockNetworkImage(self, block: bool): + self._block_network_image = block + + def setBlockNetworkLoads(self, block: bool): + self._block_network_loads = block + + def setMinimumFontSize(self, size: int): + self._minimum_font_size = size + + def getMinimumFontSize(self) -> int: + return self._minimum_font_size + + def setDefaultFontSize(self, size: int): + self._default_font_size = size + + def getDefaultFontSize(self) -> int: + return self._default_font_size + + def setTextZoom(self, zoom: int): + self._text_zoom = zoom + + def getTextZoom(self) -> int: + return self._text_zoom + + def setLayoutAlgorithm(self, algo: int): + self._layout_algorithm = algo + + def getLayoutAlgorithm(self) -> int: + return self._layout_algorithm + + def setMixedContentMode(self, mode: int): + self._mixed_content_mode = mode + + def getMixedContentMode(self) -> int: + return self._mixed_content_mode + + def setOffscreenPreRaster(self, enabled: bool): + self._offscreen_pre_raster = enabled + + def getOffscreenPreRaster(self) -> bool: + return self._offscreen_pre_raster + + def setSafeBrowsingEnabled(self, enabled: bool): + self._safe_browsing_enabled = enabled + + def getSafeBrowsingEnabled(self) -> bool: + return self._safe_browsing_enabled + + def toString(self) -> str: + return f"WebSettings(js={self._js_enabled})" + + +# ═══════════════════════════════════════════════════════════════ +# WebViewClient +# ═══════════════════════════════════════════════════════════════ + +class WebViewClient(Stub): + """WebViewClient — WebView 客户端""" + + def onPageStarted(self, view: "WebView", url: str, favicon: Any): + pass + + def onPageFinished(self, view: "WebView", url: str): + pass + + def onLoadResource(self, view: "WebView", url: str): + pass + + def shouldOverrideUrlLoading(self, view: "WebView", request: Any) -> bool: + return False + + def shouldOverrideUrlLoading(self, view: "WebView", url: str) -> bool: + return False + + def shouldInterceptRequest(self, view: "WebView", request: WebResourceRequest) -> Optional[WebResourceResponse]: + return None + + def shouldInterceptRequest(self, view: "WebView", url: str) -> Optional[WebResourceResponse]: + return None + + def onReceivedHttpError(self, view: "WebView", request: WebResourceRequest, + error_response: WebResourceResponse): + pass + + def onReceivedSslError(self, view: "WebView", handler: SslErrorHandler, error: Any): + pass + + def onReceivedClientCertRequest(self, view: "WebView", request: Any): + pass + + def onReceivedHttpAuthRequest(self, view: "WebView", handler: HttpAuthHandler, + host: str, realm: str): + pass + + def onFormResubmission(self, view: "WebView", dont_resend: Any, resend: Any): + pass + + def doUpdateVisitedHistory(self, view: "WebView", url: str, is_reload: bool): + pass + + def onScaleChanged(self, view: "WebView", old_scale: float, new_scale: float): + pass + + def onUnhandledKeyEvent(self, view: "WebView", event: Any): + pass + + def shouldOverrideKeyEvent(self, view: "WebView", event: Any) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# WebChromeClient +# ═══════════════════════════════════════════════════════════════ + +class WebChromeClient(Stub): + """WebChromeClient — WebView Chrome 客户端""" + + def onProgressChanged(self, view: "WebView", new_progress: int): + pass + + def onReceivedTitle(self, view: "WebView", title: str): + pass + + def onReceivedIcon(self, view: "WebView", icon: Any): + pass + + def onReceivedTouchIconUrl(self, view: "WebView", url: str, precomposed: bool): + pass + + def onShowCustomView(self, view: Any, callback: Any): + pass + + def onHideCustomView(self): + pass + + def onCreateWindow(self, view: "WebView", is_dialog: bool, + is_user_gesture: bool, result_msg: Any) -> bool: + return False + + def onRequestFocus(self, view: "WebView"): + pass + + def onCloseWindow(self, window: "WebView"): + pass + + def onJsAlert(self, view: "WebView", url: str, message: str, + result: JsResult) -> bool: + return False + + def onJsConfirm(self, view: "WebView", url: str, message: str, + result: JsResult) -> bool: + return False + + def onJsPrompt(self, view: "WebView", url: str, message: str, + default_value: str, result: JsPromptResult) -> bool: + return False + + def onJsBeforeUnload(self, view: "WebView", url: str, message: str, + result: JsResult) -> bool: + return False + + def onExceededDatabaseQuota(self, url: str, database_identifier: str, + quota: int, estimated_database_size: int, + total_quota: int, quota_updater: Any): + pass + + def onReachedMaxAppCacheSize(self, space_needed: int, total_quota: int, + quota_updater: Any): + pass + + def onGeolocationPermissionsShowPrompt(self, origin: str, + callback: Any): + pass + + def onGeolocationPermissionsHidePrompt(self): + pass + + def onPermissionRequest(self, request: PermissionRequest): + pass + + def onPermissionRequestCanceled(self, request: PermissionRequest): + pass + + def onConsoleMessage(self, message: Any) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# WebView +# ═══════════════════════════════════════════════════════════════ + +class WebView(Stub): + """WebView — Web 视图""" + + _instance_count = 0 + + def __init__(self, context: Any = None, attrs: Any = None): + super().__init__() + WebView._instance_count += 1 + self._context = context + self._settings = WebSettings() + self._webview_client: Optional[WebViewClient] = None + self._chrome_client: Optional[WebChromeClient] = None + self._current_url: str = "" + self._original_url: str = "" + self._title: str = "" + self._progress: int = 100 + self._content_height: int = 0 + self._scale: float = 1.0 + self._background_color: int = 0xFFFFFFFF + self._back_forward_list = WebBackForwardList() + self._javascript_interfaces: Dict[str, Any] = {} + self._paused: bool = False + self._timers_paused: bool = False + self._network_available: bool = True + + def loadUrl(self, url: str, additional_headers: Optional[Dict[str, str]] = None): + self._current_url = url + self._original_url = url + self._title = url + self._progress = 0 + if self._webview_client: + try: + self._webview_client.onPageStarted(self, url, None) + self._webview_client.onPageFinished(self, url) + except Exception: + pass + self._progress = 100 + + def loadData(self, data: str, mime_type: str = "text/html", + encoding: str = "utf-8"): + self._current_url = "data:text/html;charset=utf-8," + self._progress = 100 + + def loadDataWithBaseURL(self, base_url: str, data: str, + mime_type: str, encoding: str, + history_url: str): + self._current_url = history_url or base_url + self._progress = 100 + + def setWebViewClient(self, client: WebViewClient): + self._webview_client = client + + def setWebChromeClient(self, client: WebChromeClient): + self._chrome_client = client + + def getSettings(self) -> WebSettings: + return self._settings + + def goBack(self): + pass + + def goForward(self): + pass + + def canGoBack(self) -> bool: + return False + + def canGoForward(self) -> bool: + return False + + def goBackOrForward(self, steps: int): + pass + + def reload(self): + pass + + def stopLoading(self): + pass + + def clearHistory(self): + pass + + def clearCache(self, include_disk_files: bool): + pass + + def clearView(self): + pass + + def destroy(self): + WebView._instance_count -= 1 + + def getUrl(self) -> Optional[str]: + return self._current_url or None + + def getTitle(self) -> Optional[str]: + return self._title or None + + def getProgress(self) -> int: + return self._progress + + def getContentHeight(self) -> int: + return self._content_height + + def getScale(self) -> float: + return self._scale + + def addJavascriptInterface(self, obj: Any, interface_name: str): + self._javascript_interfaces[interface_name] = obj + + def removeJavascriptInterface(self, interface_name: str): + if interface_name in self._javascript_interfaces: + del self._javascript_interfaces[interface_name] + + def evaluateJavascript(self, script: str, callback: Any = None): + pass + + def setInitialScale(self, scale_in_percent: int): + self._scale = scale_in_percent / 100.0 + + def setBackgroundColor(self, color: int): + self._background_color = color + + def scrollTo(self, x: int, y: int): + pass + + def scrollBy(self, x: int, y: int): + pass + + def pageUp(self, top: bool) -> bool: + return True + + def pageDown(self, bottom: bool) -> bool: + return True + + def findNext(self, forward: bool): + pass + + def findAll(self, find: str) -> int: + return 0 + + def findAllAsync(self, find: str): + pass + + def clearMatches(self): + pass + + def zoomIn(self): + self._scale *= 1.25 + + def zoomOut(self): + self._scale /= 1.25 + + def setNetworkAvailable(self, available: bool): + self._network_available = available + + def pauseTimers(self): + self._timers_paused = True + + def resumeTimers(self): + self._timers_paused = False + + def onPause(self): + self._paused = True + + def onResume(self): + self._paused = False + + def onWindowFocusChanged(self, has_focus: bool): + pass + + def setLayoutParams(self, params: Any): + pass + + def requestFocus(self, direction: int = 0): + pass + + def getOriginalUrl(self) -> Optional[str]: + return self._original_url or None + + def canZoomIn(self) -> bool: + return True + + def canZoomOut(self) -> bool: + return self._scale > 0.25 + + def toString(self) -> str: + return f"WebView(url={self._current_url})" + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + "WebView", + "WebSettings", + "WebViewClient", + "WebChromeClient", + "WebResourceRequest", + "WebResourceResponse", + "WebResourceError", + "WebMessage", + "WebMessagePort", + "JavascriptInterface", + "CookieManager", + "CookieSyncManager", + "JsResult", + "JsPromptResult", + "HttpAuthHandler", + "SslErrorHandler", + "PermissionRequest", + "GeolocationPermissions", + "WebStorage", + "WebBackForwardList", + "WebHistoryItem", +] \ No newline at end of file diff --git a/android_pyapi/android/widget.py b/android_pyapi/android/widget.py new file mode 100644 index 0000000..98c2544 --- /dev/null +++ b/android_pyapi/android/widget.py @@ -0,0 +1,4777 @@ +""" +Android Widget 组件 — TextView, Button, RecyclerView, ListView, WebView 等 +方法签名完全对齐 Android SDK +""" + +import math +import time +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .._core import Stub, AndroidOnlyError, android_context +from .view import ( + View, ViewGroup, ViewTreeObserver, + MotionEvent, KeyEvent, Gravity, SurfaceView, + ViewGroup as VG, +) + + +# ═══════════════════════════════════════════════════════════════ +# TextView +# ═══════════════════════════════════════════════════════════════ + +class TextView(View): + """TextView — 文本显示视图""" + + # 自动链接 + AUTO_LINK_NONE = 0x00 + AUTO_LINK_WEB_URLS = 0x01 + AUTO_LINK_EMAIL_ADDRESSES = 0x02 + AUTO_LINK_PHONE_NUMBERS = 0x04 + AUTO_LINK_MAP_ADDRESSES = 0x08 + AUTO_LINK_ALL = 0x0F + + # Buffer类型 + BUFFER_TYPE_NORMAL = 0 + BUFFER_TYPE_SPANNABLE = 1 + BUFFER_TYPE_EDITABLE = 2 + + # 截断位置 + TRUNCATE_AT_END = 0 + TRUNCATE_AT_MIDDLE = 1 + TRUNCATE_AT_START = 2 + + # 文本方向 + TEXT_DIRECTION_INHERIT = 0 + TEXT_DIRECTION_FIRST_STRONG = 1 + TEXT_DIRECTION_ANY_RTL = 2 + TEXT_DIRECTION_LTR = 3 + TEXT_DIRECTION_RTL = 4 + TEXT_DIRECTION_LOCALE = 5 + TEXT_DIRECTION_FIRST_STRONG_LTR = 6 + TEXT_DIRECTION_FIRST_STRONG_RTL = 7 + + # 文本对齐 + TEXT_ALIGNMENT_INHERIT = 0 + TEXT_ALIGNMENT_GRAVITY = 1 + TEXT_ALIGNMENT_TEXT_START = 2 + TEXT_ALIGNMENT_TEXT_END = 3 + TEXT_ALIGNMENT_CENTER = 4 + TEXT_ALIGNMENT_VIEW_START = 5 + TEXT_ALIGNMENT_VIEW_END = 6 + + def __init__(self, context=None): + super().__init__(context) + self._text: str = "" + self._hint: str = "" + self._text_size: float = 14.0 + self._text_color: int = 0xFF000000 + self._hint_text_color: int = 0xFF808080 + self._link_text_color: int = 0xFFFF0000 + self._highlight_color: int = 0x556633CC + self._gravity = Gravity.NO_GRAVITY + self._typeface: Any = None + self._input_type: int = 0 + self._lines: int = 0 + self._max_lines: int = 0 + self._min_lines: int = 0 + self._single_line: bool = False + self._ellipsize: Optional[str] = None # "end", "start", "middle", "marquee" + self._movement_method: Any = None + self._auto_link_mask: int = TextView.AUTO_LINK_NONE + self._transformation_method: Optional[Callable] = None + self._shadow_radius: float = 0.0 + self._shadow_dx: float = 0.0 + self._shadow_dy: float = 0.0 + self._shadow_color: int = 0 + self._line_spacing_extra: float = 0.0 + self._line_spacing_mult: float = 1.0 + self._compound_drawables: List[Any] = [None, None, None, None] + self._compound_drawable_padding: int = 0 + self._include_font_padding: bool = True + self._elegant_text_height: bool = False + self._letter_spacing: float = 0.0 + self._font_feature_settings: str = "" + self._key_listener: Any = None + self._filters: List[Any] = [] + self._select_all_on_focus: bool = False + self._horizontally_scrolling: bool = False + self._on_editor_action_listener: Optional[Callable] = None + self._paint: Any = None + self._line_count: int = 1 + self._layout: Any = None + self._links_clickable: bool = False + self._cursor_visible: bool = True + self._editable: bool = False + self._selection_start: int = 0 + self._selection_end: int = 0 + self._scroll_horizontally: bool = False + + # ─── text ────────────────────────────────────────────────── + + def setText(self, text: str, buffer_type: int = BUFFER_TYPE_NORMAL): + self._text = str(text) if text is not None else "" + self._line_count = self._text.count("\n") + 1 if self._text else 0 + + def getText(self) -> str: + return self._text + + def append(self, text: str): + self._text += str(text) if text is not None else "" + self._line_count = self._text.count("\n") + 1 if self._text else 0 + + # ─── text size / color ───────────────────────────────────── + + def setTextSize(self, size: float, unit: int = 2): # unit=2 means SP + self._text_size = size + + def getTextSize(self) -> float: + return self._text_size + + def setTextColor(self, color: int): + self._text_color = color + + def getCurrentTextColor(self) -> int: + return self._text_color + + def setHintTextColor(self, color: int): + self._hint_text_color = color + + def getHintTextColor(self) -> int: + return self._hint_text_color + + def setLinkTextColor(self, color: int): + self._link_text_color = color + + def getLinkTextColor(self) -> int: + return self._link_text_color + + def setHighlightColor(self, color: int): + self._highlight_color = color + + def getHighlightColor(self) -> int: + return self._highlight_color + + # ─── gravity ────────────────────────────────────────────── + + def setGravity(self, gravity: int): + self._gravity = gravity + + def getGravity(self) -> int: + return self._gravity + + # ─── typeface ────────────────────────────────────────────── + + def setTypeface(self, typeface: Any, style: int = 0): + self._typeface = typeface + + def getTypeface(self) -> Any: + return self._typeface + + # ─── hint ────────────────────────────────────────────────── + + def setHint(self, hint: str): + self._hint = str(hint) if hint is not None else "" + + def getHint(self) -> str: + return self._hint + + # ─── input type ──────────────────────────────────────────── + + def setInputType(self, input_type: int): + self._input_type = input_type + + def getInputType(self) -> int: + return self._input_type + + # ─── lines ───────────────────────────────────────────────── + + def setLines(self, lines: int): + self._lines = lines + + def setMaxLines(self, max_lines: int): + self._max_lines = max_lines + + def getMaxLines(self) -> int: + return self._max_lines + + def setMinLines(self, min_lines: int): + self._min_lines = min_lines + + def getMinLines(self) -> int: + return self._min_lines + + def getLineCount(self) -> int: + return self._line_count + + def setSingleLine(self, single_line: bool = True): + self._single_line = single_line + + # ─── ellipsize ───────────────────────────────────────────── + + def setEllipsize(self, where: Optional[str]): + self._ellipsize = where + + def getEllipsize(self) -> Optional[str]: + return self._ellipsize + + # ─── movement method ────────────────────────────────────── + + def setMovementMethod(self, movement: Any): + self._movement_method = movement + + def getMovementMethod(self) -> Any: + return self._movement_method + + # ─── auto link ───────────────────────────────────────────── + + def setAutoLinkMask(self, mask: int): + self._auto_link_mask = mask + + def getAutoLinkMask(self) -> int: + return self._auto_link_mask + + # ─── transformation ─────────────────────────────────────── + + def setTransformationMethod(self, method: Optional[Callable]): + self._transformation_method = method + + def getTransformationMethod(self) -> Optional[Callable]: + return self._transformation_method + + # ─── shadow ──────────────────────────────────────────────── + + def setShadowLayer(self, radius: float, dx: float, dy: float, color: int): + self._shadow_radius = radius + self._shadow_dx = dx + self._shadow_dy = dy + self._shadow_color = color + + # ─── line spacing ───────────────────────────────────────── + + def setLineSpacing(self, extra: float, multiplier: float): + self._line_spacing_extra = extra + self._line_spacing_mult = multiplier + + # ─── compound drawables ─────────────────────────────────── + + def setCompoundDrawables(self, left: Any, top: Any, right: Any, bottom: Any): + self._compound_drawables = [left, top, right, bottom] + + def setCompoundDrawablesWithIntrinsicBounds(self, left: Any, top: Any, + right: Any, bottom: Any): + self._compound_drawables = [left, top, right, bottom] + + def setCompoundDrawablePadding(self, pad: int): + self._compound_drawable_padding = pad + + def getCompoundDrawablePadding(self) -> int: + return self._compound_drawable_padding + + def getCompoundDrawables(self) -> List[Any]: + return list(self._compound_drawables) + + # ─── editor action ──────────────────────────────────────── + + def setOnEditorActionListener(self, listener: Optional[Callable]): + self._on_editor_action_listener = listener + + # ─── key listener ───────────────────────────────────────── + + def setKeyListener(self, key_listener: Any): + self._key_listener = key_listener + + def getKeyListener(self) -> Any: + return self._key_listener + + # ─── filters ─────────────────────────────────────────────── + + def setFilters(self, filters: List[Any]): + self._filters = list(filters) + + def getFilters(self) -> List[Any]: + return list(self._filters) + + # ─── misc ────────────────────────────────────────────────── + + def setSelectAllOnFocus(self, select_all_on_focus: bool): + self._select_all_on_focus = select_all_on_focus + + def setHorizontallyScrolling(self, horizontally_scrolling: bool): + self._horizontally_scrolling = horizontally_scrolling + + def setIncludeFontPadding(self, include_font_padding: bool): + self._include_font_padding = include_font_padding + + def setElegantTextHeight(self, elegant: bool): + self._elegant_text_height = elegant + + def setLetterSpacing(self, letter_spacing: float): + self._letter_spacing = letter_spacing + + def getLetterSpacing(self) -> float: + return self._letter_spacing + + def setFontFeatureSettings(self, settings: str): + self._font_feature_settings = settings + + def getFontFeatureSettings(self) -> str: + return self._font_feature_settings + + def getPaint(self) -> Any: + return self._paint + + def getLayout(self) -> Any: + return self._layout + + def setLinksClickable(self, links_clickable: bool): + self._links_clickable = links_clickable + + def getLinksClickable(self) -> bool: + return self._links_clickable + + def setCursorVisible(self, visible: bool): + self._cursor_visible = visible + + def isCursorVisible(self) -> bool: + return self._cursor_visible + + def setTextCursorDrawable(self, drawable: Any): + pass + + def setFreezesText(self, freezes_text: bool): + pass + + def getFreezesText(self) -> bool: + return False + + def setEditableFactory(self, factory: Any): + pass + + def setSpannableFactory(self, factory: Any): + pass + + def getTextSize(self) -> float: + return self._text_size + + def setBackgroundColor(self, color: int): + super().setBackgroundColor(color) + + def setPadding(self, left: int, top: int, right: int, bottom: int): + super().setPadding(left, top, right, bottom) + + def onMeasure(self, width_measure_spec: int, height_measure_spec: int): + from .view import MeasureSpec + w = MeasureSpec.getSize(width_measure_spec) + h = MeasureSpec.getSize(height_measure_spec) + if MeasureSpec.getMode(width_measure_spec) == MeasureSpec.AT_MOST: + text_len = len(self._text) * 4 + w = min(w, max(100, text_len)) + if MeasureSpec.getMode(height_measure_spec) == MeasureSpec.AT_MOST: + text_lines = self._line_count + h = min(h, max(20, text_lines * int(self._text_size) + 10)) + self.setMeasuredDimension(w, h) + + def onDraw(self, canvas: Any): + pass + + +# ═══════════════════════════════════════════════════════════════ +# EditText +# ═══════════════════════════════════════════════════════════════ + +class EditText(TextView): + """EditText — 可编辑文本""" + + def __init__(self, context=None): + super().__init__(context) + self._editable = True + self._selection_start = 0 + self._selection_end = 0 + + def setSelection(self, start: int, stop: Optional[int] = None): + if stop is None: + self._selection_start = start + self._selection_end = start + else: + self._selection_start = start + self._selection_end = stop + + def getSelectionStart(self) -> int: + return self._selection_start + + def getSelectionEnd(self) -> int: + return self._selection_end + + def selectAll(self): + self._selection_start = 0 + self._selection_end = len(self._text) + + def extendSelection(self, index: int): + self._selection_end = index + + def setText(self, text: str, buffer_type: int = TextView.BUFFER_TYPE_EDITABLE): + super().setText(text, buffer_type) + + def getEditableText(self): + return self._text + + +# ═══════════════════════════════════════════════════════════════ +# Button +# ═══════════════════════════════════════════════════════════════ + +class Button(TextView): + """Button — 按钮""" + + def __init__(self, context=None): + super().__init__(context) + self._clickable = True + self._focusable = True + + def onTouchEvent(self, event: MotionEvent) -> bool: + if event.getAction() == MotionEvent.ACTION_UP: + self.performClick() + return super().onTouchEvent(event) + + +class CheckBox(Button): + """CheckBox — 复选框""" + + def __init__(self, context=None): + super().__init__(context) + self._checked = False + + def setChecked(self, checked: bool): + self._checked = checked + + def isChecked(self) -> bool: + return self._checked + + def toggle(self): + self._checked = not self._checked + + def performClick(self) -> bool: + self.toggle() + return super().performClick() + + +class RadioButton(Button): + """RadioButton — 单选按钮""" + + def __init__(self, context=None): + super().__init__(context) + self._checked = False + + def setChecked(self, checked: bool): + self._checked = checked + + def isChecked(self) -> bool: + return self._checked + + def toggle(self): + self._checked = not self._checked + + def performClick(self) -> bool: + self.toggle() + return super().performClick() + + +class ToggleButton(Button): + """ToggleButton — 开关按钮""" + + def __init__(self, context=None): + super().__init__(context) + self._checked = False + self._text_on = "ON" + self._text_off = "OFF" + self._disabled_alpha = 0.5 + + def setChecked(self, checked: bool): + self._checked = checked + if self._checked: + self.setText(self._text_on) + else: + self.setText(self._text_off) + + def isChecked(self) -> bool: + return self._checked + + def toggle(self): + self.setChecked(not self._checked) + + def setTextOn(self, text_on: str): + self._text_on = text_on + if self._checked: + self.setText(text_on) + + def getTextOn(self) -> str: + return self._text_on + + def setTextOff(self, text_off: str): + self._text_off = text_off + if not self._checked: + self.setText(text_off) + + def getTextOff(self) -> str: + return self._text_off + + def performClick(self) -> bool: + self.toggle() + return super().performClick() + + +class Switch(Button): + """Switch — 开关组件""" + + def __init__(self, context=None): + super().__init__(context) + self._checked = False + self._show_text = True + self._split_track = False + self._thumb_text_padding: int = 0 + self._switch_min_width: int = 0 + self._switch_padding: int = 0 + self._thumb_drawable: Any = None + self._track_drawable: Any = None + self._text_on = "ON" + self._text_off = "OFF" + self._thumb_width = 40 + self._switch_width = 100 + + def isChecked(self) -> bool: + return self._checked + + def setChecked(self, checked: bool): + self._checked = checked + + def toggle(self): + self._checked = not self._checked + + def setShowText(self, show_text: bool): + self._show_text = show_text + + def getShowText(self) -> bool: + return self._show_text + + def setSplitTrack(self, split_track: bool): + self._split_track = split_track + + def getSplitTrack(self) -> bool: + return self._split_track + + def setThumbTextPadding(self, padding: int): + self._thumb_text_padding = padding + + def getThumbTextPadding(self) -> int: + return self._thumb_text_padding + + def setSwitchMinWidth(self, min_width: int): + self._switch_min_width = min_width + + def getSwitchMinWidth(self) -> int: + return self._switch_min_width + + def setSwitchPadding(self, padding: int): + self._switch_padding = padding + + def getSwitchPadding(self) -> int: + return self._switch_padding + + def getThumbDrawable(self) -> Any: + return self._thumb_drawable + + def setThumbDrawable(self, drawable: Any): + self._thumb_drawable = drawable + + def getTrackDrawable(self) -> Any: + return self._track_drawable + + def setTrackDrawable(self, drawable: Any): + self._track_drawable = drawable + + def setTextOn(self, text_on: str): + self._text_on = text_on + + def getTextOn(self) -> str: + return self._text_on + + def setTextOff(self, text_off: str): + self._text_off = text_off + + def getTextOff(self) -> str: + return self._text_off + + def performClick(self) -> bool: + self.toggle() + return super().performClick() + + def onMeasure(self, width_measure_spec: int, height_measure_spec: int): + from .view import MeasureSpec + w = MeasureSpec.getSize(width_measure_spec) + h = MeasureSpec.getSize(height_measure_spec) + self.setMeasuredDimension(w, max(30, h)) + + +# ═══════════════════════════════════════════════════════════════ +# ImageView +# ═══════════════════════════════════════════════════════════════ + +class ImageView(View): + """ImageView — 图片视图""" + + # ScaleType常量 + ScaleType = type("ScaleType", (), { + "MATRIX": 0, + "FIT_XY": 1, + "FIT_START": 2, + "FIT_CENTER": 3, + "FIT_END": 4, + "CENTER": 5, + "CENTER_CROP": 6, + "CENTER_INSIDE": 7, + }) + + def __init__(self, context=None): + super().__init__(context) + self._drawable: Any = None + self._image_resource: int = 0 + self._uri: Any = None + self._scale_type: int = ImageView.ScaleType.FIT_CENTER + self._max_width: int = 0 + self._max_height: int = 0 + self._adjust_view_bounds: bool = False + self._crop_to_padding: bool = False + self._baseline: int = 0 + self._baseline_align_bottom: bool = False + self._image_matrix: Any = None + self._image_bitmap: Any = None + + def setImageDrawable(self, drawable: Any): + self._drawable = drawable + + def getDrawable(self) -> Any: + return self._drawable + + def setImageResource(self, res_id: int): + self._image_resource = res_id + + def setImageURI(self, uri: Any): + self._uri = uri + + def setImageBitmap(self, bitmap: Any): + self._image_bitmap = bitmap + + def getImageBitmap(self) -> Any: + return self._image_bitmap + + def setScaleType(self, scale_type: int): + self._scale_type = scale_type + + def getScaleType(self) -> int: + return self._scale_type + + def setMaxWidth(self, max_width: int): + self._max_width = max_width + + def setMaxHeight(self, max_height: int): + self._max_height = max_height + + def setAdjustViewBounds(self, adjust: bool): + self._adjust_view_bounds = adjust + + def getAdjustViewBounds(self) -> bool: + return self._adjust_view_bounds + + def setCropToPadding(self, crop: bool): + self._crop_to_padding = crop + + def getCropToPadding(self) -> bool: + return self._crop_to_padding + + def setBaseline(self, baseline: int): + self._baseline = baseline + + def getBaseline(self) -> int: + return self._baseline + + def setBaselineAlignBottom(self, aligned: bool): + self._baseline_align_bottom = aligned + + def getBaselineAlignBottom(self) -> bool: + return self._baseline_align_bottom + + def setImageMatrix(self, matrix: Any): + self._image_matrix = matrix + + def getImageMatrix(self) -> Any: + return self._image_matrix + + def invalidateDrawable(self, dr: Any): + pass + + def onMeasure(self, width_measure_spec: int, height_measure_spec: int): + from .view import MeasureSpec + w = MeasureSpec.getSize(width_measure_spec) + h = MeasureSpec.getSize(height_measure_spec) + if w == 0: + w = 100 + if h == 0: + h = 100 + self.setMeasuredDimension(w, h) + + +class ImageButton(ImageView): + """ImageButton — 图片按钮""" + + def __init__(self, context=None): + super().__init__(context) + self._clickable = True + self._focusable = True + + def onTouchEvent(self, event: MotionEvent) -> bool: + if event.getAction() == MotionEvent.ACTION_UP: + self.performClick() + return super().onTouchEvent(event) + + +# ═══════════════════════════════════════════════════════════════ +# ProgressBar +# ═══════════════════════════════════════════════════════════════ + +class ProgressBar(View): + """ProgressBar — 进度条""" + + def __init__(self, context=None, attrs: Any = None, def_style_attr: int = 0, + def_style_res: int = 0): + super().__init__(context) + self._progress: int = 0 + self._max: int = 100 + self._indeterminate: bool = False + self._indeterminate_drawable: Any = None + self._progress_drawable: Any = None + self._secondary_progress: int = 0 + self._min: int = 0 + self._interpolator: Any = None + self._animation: Any = None + + def setProgress(self, progress: int): + self._progress = max(self._min, min(self._max, progress)) + + def getProgress(self) -> int: + return self._progress + + def setMax(self, max_val: int): + self._max = max_val + if self._progress > self._max: + self._progress = self._max + + def getMax(self) -> int: + return self._max + + def setMin(self, min_val: int): + self._min = min_val + + def getMin(self) -> int: + return self._min + + def incrementProgressBy(self, diff: int): + self.setProgress(self._progress + diff) + + def setIndeterminate(self, indeterminate: bool): + self._indeterminate = indeterminate + + def isIndeterminate(self) -> bool: + return self._indeterminate + + def getIndeterminateDrawable(self) -> Any: + return self._indeterminate_drawable + + def setIndeterminateDrawable(self, drawable: Any): + self._indeterminate_drawable = drawable + + def setProgressDrawable(self, drawable: Any): + self._progress_drawable = drawable + + def getProgressDrawable(self) -> Any: + return self._progress_drawable + + def setSecondaryProgress(self, secondary_progress: int): + self._secondary_progress = max(self._min, min(self._max, secondary_progress)) + + def getSecondaryProgress(self) -> int: + return self._secondary_progress + + def setInterpolator(self, interpolator: Any): + self._interpolator = interpolator + + def getInterpolator(self) -> Any: + return self._interpolator + + def setAnimation(self, animation: Any): + self._animation = animation + + def onMeasure(self, width_measure_spec: int, height_measure_spec: int): + from .view import MeasureSpec + w = MeasureSpec.getSize(width_measure_spec) + h = MeasureSpec.getSize(height_measure_spec) + if h == 0: + h = 20 + self.setMeasuredDimension(w, h) + + +class SeekBar(ProgressBar): + """SeekBar — 可拖动进度条""" + + def __init__(self, context=None, attrs: Any = None, + def_style_attr: int = 0, def_style_res: int = 0): + super().__init__(context, attrs, def_style_attr, def_style_res) + self._on_seek_bar_change_listener: Optional[Callable] = None + + class OnSeekBarChangeListener: + def onProgressChanged(self, seek_bar: "SeekBar", progress: int, from_user: bool): + pass + def onStartTrackingTouch(self, seek_bar: "SeekBar"): + pass + def onStopTrackingTouch(self, seek_bar: "SeekBar"): + pass + + def setOnSeekBarChangeListener(self, listener: Optional[OnSeekBarChangeListener]): + self._on_seek_bar_change_listener = listener + + def getOnSeekBarChangeListener(self) -> Optional[OnSeekBarChangeListener]: + return self._on_seek_bar_change_listener + + def setThumb(self, thumb: Any): + pass + + def getThumb(self) -> Any: + return None + + def setThumbOffset(self, thumb_offset: int): + pass + + def getThumbOffset(self) -> int: + return 0 + + def setKeyProgressIncrement(self, increment: int): + pass + + def getKeyProgressIncrement(self) -> int: + return 1 + + +class RatingBar(ProgressBar): + """RatingBar — 评分条""" + + def __init__(self, context=None, attrs: Any = None, + def_style_attr: int = 0, def_style_res: int = 0): + super().__init__(context, attrs, def_style_attr, def_style_res) + self._num_stars: int = 5 + self._rating: float = 0.0 + self._step_size: float = 0.5 + self._is_indicator: bool = False + self._on_rating_bar_change_listener: Optional[Callable] = None + + class OnRatingBarChangeListener: + def onRatingChanged(self, rating_bar: "RatingBar", rating: float, from_user: bool): + pass + + def setOnRatingBarChangeListener(self, listener: Optional[OnRatingBarChangeListener]): + self._on_rating_bar_change_listener = listener + + def getOnRatingBarChangeListener(self) -> Optional[OnRatingBarChangeListener]: + return self._on_rating_bar_change_listener + + def setNumStars(self, num_stars: int): + self._num_stars = max(1, num_stars) + + def getNumStars(self) -> int: + return self._num_stars + + def setRating(self, rating: float): + self._rating = max(0.0, min(float(self._num_stars), rating)) + + def getRating(self) -> float: + return self._rating + + def setStepSize(self, step_size: float): + self._step_size = max(0.1, step_size) + + def getStepSize(self) -> float: + return self._step_size + + def setIsIndicator(self, is_indicator: bool): + self._is_indicator = is_indicator + + def isIndicator(self) -> bool: + return self._is_indicator + + +# ═══════════════════════════════════════════════════════════════ +# AdapterView +# ═══════════════════════════════════════════════════════════════ + +class AdapterView(ViewGroup): + """AdapterView — 适配器视图基类""" + + CHOICE_MODE_NONE = 0 + CHOICE_MODE_SINGLE = 1 + CHOICE_MODE_MULTIPLE = 2 + CHOICE_MODE_MULTIPLE_MODAL = 3 + + INVALID_POSITION = -1 + INVALID_ROW_ID = -9223372036854775808 # Long.MIN_VALUE + + def __init__(self, context=None): + super().__init__(context) + self._adapter: Any = None + self._on_item_click_listener: Optional[Callable] = None + self._on_item_long_click_listener: Optional[Callable] = None + self._on_item_selected_listener: Optional[Callable] = None + self._choice_mode = AdapterView.CHOICE_MODE_NONE + self._checked_item_positions: Dict[int, bool] = {} + self._checked_id_positions: Dict[int, bool] = {} + self._selected_position = AdapterView.INVALID_POSITION + self._empty_view: Optional[View] = None + self._selection_notifier: Any = None + + def setAdapter(self, adapter: Any): + self._adapter = adapter + + def getAdapter(self) -> Any: + return self._adapter + + def setOnItemClickListener(self, listener: Optional[Callable]): + self._on_item_click_listener = listener + + def getOnItemClickListener(self) -> Optional[Callable]: + return self._on_item_click_listener + + def setOnItemLongClickListener(self, listener: Optional[Callable]): + self._on_item_long_click_listener = listener + + def getOnItemLongClickListener(self) -> Optional[Callable]: + return self._on_item_long_click_listener + + def setOnItemSelectedListener(self, listener: Optional[Callable]): + self._on_item_selected_listener = listener + + def getOnItemSelectedListener(self) -> Optional[Callable]: + return self._on_item_selected_listener + + def getSelectedItemPosition(self) -> int: + return self._selected_position + + def getSelectedItemId(self) -> int: + return 0 + + def getSelectedItem(self) -> Any: + if self._adapter is not None and 0 <= self._selected_position < self._adapter.getCount(): + return self._adapter.getItem(self._selected_position) + return None + + def setSelection(self, position: int): + self._selected_position = position + + def setEmptyView(self, empty_view: View): + self._empty_view = empty_view + + def getEmptyView(self) -> Optional[View]: + return self._empty_view + + def setChoiceMode(self, choice_mode: int): + self._choice_mode = choice_mode + + def getChoiceMode(self) -> int: + return self._choice_mode + + def setItemChecked(self, position: int, checked: bool): + if self._choice_mode == AdapterView.CHOICE_MODE_SINGLE: + self._checked_item_positions.clear() + self._checked_item_positions[position] = checked + elif self._choice_mode == AdapterView.CHOICE_MODE_MULTIPLE: + self._checked_item_positions[position] = checked + + def isItemChecked(self, position: int) -> bool: + return self._checked_item_positions.get(position, False) + + def getCheckedItemPositions(self) -> Dict[int, bool]: + return dict(self._checked_item_positions) + + def getCheckedItemCount(self) -> int: + return len(self._checked_item_positions) + + def getCheckedItemIds(self) -> List[int]: + return list(self._checked_id_positions.keys()) + + def performItemClick(self, view: View, position: int, id_: int) -> bool: + if self._on_item_click_listener: + self._on_item_click_listener(self, view, position, id_) + return True + return False + + +# ═══════════════════════════════════════════════════════════════ +# ListView +# ═══════════════════════════════════════════════════════════════ + +class ListView(AdapterView): + """ListView — 列表视图""" + + CHOICE_MODE_NONE = 0 + CHOICE_MODE_SINGLE = 1 + CHOICE_MODE_MULTIPLE = 2 + CHOICE_MODE_MULTIPLE_MODAL = 3 + + TRANSCRIPT_MODE_DISABLED = 0 + TRANSCRIPT_MODE_NORMAL = 1 + TRANSCRIPT_MODE_ALWAYS_SCROLL = 2 + + def __init__(self, context=None): + super().__init__(context) + self._divider: Any = None + self._divider_height: int = 0 + self._footer_divider: Any = None + self._header_divider: Any = None + self._footer_views: List[View] = [] + self._header_views: List[View] = [] + self._transcript_mode = ListView.TRANSCRIPT_MODE_DISABLED + self._items_can_focus = True + self._smooth_scroll_position = 0 + self._stack_from_bottom = False + self._selection_division = 0 + + def setAdapter(self, adapter: Any): + super().setAdapter(adapter) + + def getAdapter(self) -> Any: + return super().getAdapter() + + def setSelection(self, position: int): + super().setSelection(position) + + def smoothScrollToPosition(self, position: int): + self._smooth_scroll_position = position + + def setDivider(self, divider: Any): + self._divider = divider + + def setDividerHeight(self, height: int): + self._divider_height = height + + def getDivider(self) -> Any: + return self._divider + + def getDividerHeight(self) -> int: + return self._divider_height + + def addFooterView(self, v: View, data: Any = None, is_selectable: bool = True): + self._footer_views.append(v) + + def removeFooterView(self, v: View) -> bool: + if v in self._footer_views: + self._footer_views.remove(v) + return True + return False + + def addHeaderView(self, v: View, data: Any = None, is_selectable: bool = True): + self._header_views.append(v) + + def removeHeaderView(self, v: View) -> bool: + if v in self._header_views: + self._header_views.remove(v) + return True + return False + + def getHeaderViewsCount(self) -> int: + return len(self._header_views) + + def getFooterViewsCount(self) -> int: + return len(self._footer_views) + + def setTranscriptMode(self, mode: int): + self._transcript_mode = mode + + def getTranscriptMode(self) -> int: + return self._transcript_mode + + def setItemsCanFocus(self, items_can_focus: bool): + self._items_can_focus = items_can_focus + + def getItemsCanFocus(self) -> bool: + return self._items_can_focus + + def setStackFromBottom(self, stack_from_bottom: bool): + self._stack_from_bottom = stack_from_bottom + + def isStackFromBottom(self) -> bool: + return self._stack_from_bottom + + def getCheckItemIds(self) -> List[int]: + return list(self._checked_id_positions.keys()) + + +class GridView(AdapterView): + """GridView — 网格视图""" + + def __init__(self, context=None): + super().__init__(context) + self._num_columns: int = 1 + self._column_width: int = 0 + self._horizontal_spacing: int = 0 + self._vertical_spacing: int = 0 + self._stretch_mode: int = 0 # NO_STRETCH + self._gravity = Gravity.LEFT + + STRETCH_NO_STRETCH = 0 + STRETCH_STRETCH_COLUMN_WIDTH = 1 + STRETCH_STRETCH_SPACING = 2 + STRETCH_STRETCH_SPACING_UNIFORM = 3 + STRETCH_STRETCH_COLUMN_WIDTH_UNIFORM = 4 + + def setNumColumns(self, num_columns: int): + self._num_columns = max(1, num_columns) + + def getNumColumns(self) -> int: + return self._num_columns + + def setColumnWidth(self, column_width: int): + self._column_width = column_width + + def getColumnWidth(self) -> int: + return self._column_width + + def setHorizontalSpacing(self, horizontal_spacing: int): + self._horizontal_spacing = horizontal_spacing + + def getHorizontalSpacing(self) -> int: + return self._horizontal_spacing + + def setVerticalSpacing(self, vertical_spacing: int): + self._vertical_spacing = vertical_spacing + + def getVerticalSpacing(self) -> int: + return self._vertical_spacing + + def setStretchMode(self, stretch_mode: int): + self._stretch_mode = stretch_mode + + def getStretchMode(self) -> int: + return self._stretch_mode + + def setGravity(self, gravity: int): + self._gravity = gravity + + def getGravity(self) -> int: + return self._gravity + + def setAdapter(self, adapter: Any): + super().setAdapter(AdapterView) + + +# ═══════════════════════════════════════════════════════════════ +# RecyclerView +# ═══════════════════════════════════════════════════════════════ + +class RecyclerView(ViewGroup): + """RecyclerView — 可回收视图列表""" + + HORIZONTAL = 0 + VERTICAL = 1 + NO_POSITION = -1 + INVALID_TYPE = -1 + TOUCH_SLOP_DEFAULT = 0 + TOUCH_SLOP_PAGING = 1 + + def __init__(self, context=None): + super().__init__(context) + self._adapter: Optional["RecyclerView.Adapter"] = None + self._layout_manager: Optional["RecyclerView.LayoutManager"] = None + self._item_decorations: List[Any] = [] + self._recycler: Any = None + self._child_attached_listener: Optional[Callable] = None + self._child_detached_listener: Optional[Callable] = None + self._item_animator: Any = None + self._on_scroll_listener: Optional[Callable] = None + self._nested_scrolling_enabled = True + self._scroll_state = RecyclerView.SCROLL_STATE_IDLE + self._layout_frozen = False + self._suppress_layout = False + self._clip_to_padding = True + + SCROLL_STATE_IDLE = 0 + SCROLL_STATE_DRAGGING = 1 + SCROLL_STATE_SETTLING = 2 + + # ─── Adapter ───────────────────────────────────────────── + + class Adapter(Stub): + """RecyclerView.Adapter — 适配器""" + + def __init__(self): + super().__init__() + self._observable: Any = None + self._has_stable_ids = False + self._state_restoration_policy = 0 + + def onCreateViewHolder(self, parent: "RecyclerView", view_type: int) -> "RecyclerView.ViewHolder": + raise NotImplementedError + + def onBindViewHolder(self, holder: "RecyclerView.ViewHolder", position: int): + raise NotImplementedError + + def getItemCount(self) -> int: + return 0 + + def getItemViewType(self, position: int) -> int: + return 0 + + def getItemId(self, position: int) -> int: + return position + + def hasStableIds(self) -> bool: + return self._has_stable_ids + + def setHasStableIds(self, has_stable_ids: bool): + self._has_stable_ids = has_stable_ids + + def onAttachedToRecyclerView(self, recycler_view: "RecyclerView"): + pass + + def onDetachedFromRecyclerView(self, recycler_view: "RecyclerView"): + pass + + def onViewAttachedToWindow(self, holder: "RecyclerView.ViewHolder"): + pass + + def onViewDetachedFromWindow(self, holder: "RecyclerView.ViewHolder"): + pass + + def onViewRecycled(self, holder: "RecyclerView.ViewHolder"): + pass + + def onFailedToRecycleView(self, holder: "RecyclerView.ViewHolder") -> bool: + return False + + def notifyDataSetChanged(self): + pass + + def notifyItemChanged(self, position: int): + pass + + def notifyItemInserted(self, position: int): + pass + + def notifyItemRemoved(self, position: int): + pass + + def notifyItemMoved(self, from_position: int, to_position: int): + pass + + def notifyItemRangeChanged(self, position_start: int, item_count: int): + pass + + def notifyItemRangeInserted(self, position_start: int, item_count: int): + pass + + def notifyItemRangeRemoved(self, position_start: int, item_count: int): + pass + + def getStateRestorationPolicy(self) -> int: + return 0 + + # ─── ViewHolder ─────────────────────────────────────────── + + class ViewHolder(Stub): + """RecyclerView.ViewHolder — 视图持有者""" + + FLAG_BOUND = 1 + FLAG_NOT_RECYCLABLE = 2 + FLAG_RETURNED_FROM_SCRAP = 4 + FLAG_CHANGED = 8 + FLAG_IGNORE = 16 + FLAG_TMP_DETACHED = 32 + FLAG_UPDATE = 64 + FLAG_ADAPTER_POSITION_UNKNOWN = 128 + FLAG_ADAPTER_FULLUPDATE = 256 + FLAG_MOVED = 512 + FLAG_APPEARED_IN_PRE_LAYOUT = 1024 + + def __init__(self, item_view: View): + super().__init__() + self.itemView = item_view + self._position = RecyclerView.NO_POSITION + self._old_position = RecyclerView.NO_POSITION + self._item_id = RecyclerView.NO_ID + self._item_view_type = RecyclerView.INVALID_TYPE + self._flags = 0 + self._pre_layout_position = RecyclerView.NO_POSITION + self._is_recyclable = True + self._scrap_container: Optional[RecyclerView] = None + self._shadowed_holder: Optional["RecyclerView.ViewHolder"] = None + self._shadowing_holder: Optional["RecyclerView.ViewHolder"] = None + + NO_ID = -1 + + def getAdapterPosition(self) -> int: + return self._position + + def getLayoutPosition(self) -> int: + return self._position + + def getOldPosition(self) -> int: + return self._old_position + + def getItemId(self) -> int: + return self._item_id + + def getItemViewType(self) -> int: + return self._item_view_type + + def setIsRecyclable(self, recyclable: bool): + self._is_recyclable = recyclable + + def isRecyclable(self) -> bool: + return self._is_recyclable + + def getBindingAdapterPosition(self) -> int: + return self._position + + # ─── LayoutManager ──────────────────────────────────────── + + class LayoutManager(Stub): + """RecyclerView.LayoutManager — 布局管理器""" + + def __init__(self): + super().__init__() + self._recycler_view: Optional["RecyclerView"] = None + self._child_count = 0 + self._width = 0 + self._height = 0 + self._auto_measure = True + self._item_prefetch_enabled = True + + def onLayoutChildren(self, recycler: Any, state: Any): + pass + + def getChildCount(self) -> int: + return self._child_count + + def getPosition(self, view: View) -> int: + if isinstance(view, View): + return 0 + return RecyclerView.NO_POSITION + + def scrollToPosition(self, position: int): + pass + + def smoothScrollToPosition(self, recycler_view: "RecyclerView", state: Any, position: int): + pass + + def onAdapterChanged(self, old_adapter: Any, new_adapter: Any): + pass + + def generateDefaultLayoutParams(self) -> ViewGroup.LayoutParams: + return ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def setAutoMeasureEnabled(self, enabled: bool): + self._auto_measure = enabled + + def isAutoMeasureEnabled(self) -> bool: + return self._auto_measure + + def requestLayout(self): + pass + + def requestSimpleAnimationsInNextLayout(self): + pass + + def assertNotInLayoutOrScroll(self, tag: str = ""): + pass + + def getPaddingLeft(self) -> int: + return 0 + + def getPaddingTop(self) -> int: + return 0 + + def getPaddingRight(self) -> int: + return 0 + + def getPaddingBottom(self) -> int: + return 0 + + # ─── ItemDecoration ────────────────────────────────────── + + class ItemDecoration(Stub): + """RecyclerView.ItemDecoration — 条目装饰""" + + def onDraw(self, c: Any, parent: "RecyclerView", state: Any): + pass + + def onDrawOver(self, c: Any, parent: "RecyclerView", state: Any): + pass + + def getItemOffsets(self, out_rect: Any, view: View, parent: "RecyclerView", state: Any): + if hasattr(out_rect, "set"): + out_rect.set(0, 0, 0, 0) + + # ─── RecycledViewPool ───────────────────────────────────── + + class RecycledViewPool(Stub): + """RecyclerView.RecycledViewPool — 回收视图池""" + + def __init__(self): + super().__init__() + self._max_recycled_views = 5 + + def getRecycledView(self, view_type: int) -> Optional["RecyclerView.ViewHolder"]: + return None + + def putRecycledView(self, scrap: "RecyclerView.ViewHolder"): + pass + + def setMaxRecycledViews(self, view_type: int, max_count: int): + self._max_recycled_views = max_count + + def clear(self): + pass + + # ─── Recycler ───────────────────────────────────────────── + + class Recycler(Stub): + """RecyclerView.Recycler — 回收器""" + + def __init__(self): + super().__init__() + self._view_cache: Dict[int, List["RecyclerView.ViewHolder"]] = {} + + def getViewForPosition(self, position: int) -> View: + raise AndroidOnlyError("RecyclerView.Recycler.getViewForPosition") + + def recycleView(self, view: View): + pass + + def getRecycledViewPool(self) -> "RecyclerView.RecycledViewPool": + return RecyclerView.RecycledViewPool() + + def setViewCacheSize(self, view_count: int): + pass + + def clear(self): + self._view_cache.clear() + + # ─── RecyclerView Instance Methods ─────────────────────── + + def setAdapter(self, adapter: Optional["RecyclerView.Adapter"]): + self._adapter = adapter + + def getAdapter(self) -> Optional["RecyclerView.Adapter"]: + return self._adapter + + def setLayoutManager(self, layout_manager: Optional["RecyclerView.LayoutManager"]): + self._layout_manager = layout_manager + if layout_manager: + layout_manager._recycler_view = self + + def getLayoutManager(self) -> Optional["RecyclerView.LayoutManager"]: + return self._layout_manager + + def addItemDecoration(self, decoration: "RecyclerView.ItemDecoration", index: int = -1): + if index < 0 or index >= len(self._item_decorations): + self._item_decorations.append(decoration) + else: + self._item_decorations.insert(index, decoration) + + def removeItemDecoration(self, decoration: "RecyclerView.ItemDecoration"): + if decoration in self._item_decorations: + self._item_decorations.remove(decoration) + + def getItemDecorationAt(self, index: int) -> "RecyclerView.ItemDecoration": + return self._item_decorations[index] + + def getItemDecorationCount(self) -> int: + return len(self._item_decorations) + + def swapAdapter(self, adapter: Optional["RecyclerView.Adapter"], remove_and_recycle: bool): + self._adapter = adapter + + def getRecycledViewPool(self) -> "RecyclerView.RecycledViewPool": + return RecyclerView.RecycledViewPool() + + def setRecycledViewPool(self, pool: "RecyclerView.RecycledViewPool"): + pass + + def getRecycler(self) -> "RecyclerView.Recycler": + return RecyclerView.Recycler() + + def setItemAnimator(self, animator: Any): + self._item_animator = animator + + def getItemAnimator(self) -> Any: + return self._item_animator + + def setOnScrollListener(self, listener: Optional[Callable]): + self._on_scroll_listener = listener + + def addOnScrollListener(self, listener: Callable): + pass + + def removeOnScrollListener(self, listener: Callable): + pass + + def clearOnScrollListeners(self): + pass + + def addOnChildAttachStateChangeListener(self, listener: Callable): + self._child_attached_listener = listener + + def removeOnChildAttachStateChangeListener(self, listener: Callable): + pass + + def setHasFixedSize(self, has_fixed_size: bool): + pass + + def hasFixedSize(self) -> bool: + return False + + def setNestedScrollingEnabled(self, enabled: bool): + self._nested_scrolling_enabled = enabled + + def isNestedScrollingEnabled(self) -> bool: + return self._nested_scrolling_enabled + + def startNestedScroll(self, axes: int) -> bool: + return False + + def stopNestedScroll(self): + pass + + def hasNestedScrollingParent(self) -> bool: + return False + + def dispatchNestedPreScroll(self, dx: int, dy: int, consumed: List[int], offset: List[int]) -> bool: + return False + + def dispatchNestedScroll(self, dx_consumed: int, dy_consumed: int, + dx_unconsumed: int, dy_unconsumed: int, + offset: List[int]) -> bool: + return False + + def dispatchNestedPreFling(self, velocity_x: float, velocity_y: float) -> bool: + return False + + def dispatchNestedFling(self, velocity_x: float, velocity_y: float, consumed: bool) -> bool: + return False + + def stopScroll(self): + pass + + def getScrollState(self) -> int: + return self._scroll_state + + def setLayoutFrozen(self, frozen: bool): + self._layout_frozen = frozen + + def isLayoutFrozen(self) -> bool: + return self._layout_frozen + + def smoothScrollBy(self, dx: int, dy: int): + pass + + def scrollBy(self, x: int, y: int): + pass + + def computeHorizontalScrollOffset(self) -> int: + return 0 + + def computeHorizontalScrollRange(self) -> int: + return 0 + + def computeVerticalScrollOffset(self) -> int: + return 0 + + def computeVerticalScrollRange(self) -> int: + return 0 + + def findViewHolderForAdapterPosition(self, position: int) -> Optional["RecyclerView.ViewHolder"]: + return None + + def findViewHolderForLayoutPosition(self, position: int) -> Optional["RecyclerView.ViewHolder"]: + return None + + def findViewHolderForItemId(self, id_: int) -> Optional["RecyclerView.ViewHolder"]: + return None + + NO_ID = -1 + + +# 为 __all__ 导出创建别名 +RecyclerViewAdapter = RecyclerView.Adapter +RecyclerViewViewHolder = RecyclerView.ViewHolder +RecyclerViewLayoutManager = RecyclerView.LayoutManager + + +class LinearLayoutManager(RecyclerView.LayoutManager): + """LinearLayoutManager — 线性布局管理器""" + + HORIZONTAL = 0 + VERTICAL = 1 + + def __init__(self, context: Any = None, orientation: int = VERTICAL, reverse_layout: bool = False): + super().__init__() + self._orientation = orientation + self._reverse_layout = reverse_layout + self._stack_from_end = False + self._item_count = 0 + + def setOrientation(self, orientation: int): + self._orientation = orientation + + def getOrientation(self) -> int: + return self._orientation + + def setReverseLayout(self, reverse_layout: bool): + self._reverse_layout = reverse_layout + + def getReverseLayout(self) -> bool: + return self._reverse_layout + + def setStackFromEnd(self, stack_from_end: bool): + self._stack_from_end = stack_from_end + + def getStackFromEnd(self) -> bool: + return self._stack_from_end + + def generateDefaultLayoutParams(self) -> ViewGroup.LayoutParams: + return ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + + def onLayoutChildren(self, recycler: Any, state: Any): + pass + + +class GridLayoutManager(LinearLayoutManager): + """GridLayoutManager — 网格布局管理器""" + + def __init__(self, context: Any = None, span_count: int = 1, + orientation: int = LinearLayoutManager.VERTICAL, + reverse_layout: bool = False): + super().__init__(context, orientation, reverse_layout) + self._span_count = max(1, span_count) + + class SpanSizeLookup(Stub): + """GridLayoutManager.SpanSizeLookup — 跨度大小查找""" + + def __init__(self): + super().__init__() + self._span_index_cache = {} + + def getSpanSize(self, position: int) -> int: + return 1 + + def getSpanIndex(self, position: int, span_count: int) -> int: + return position % span_count + + def getSpanGroupIndex(self, position: int, span_count: int) -> int: + return position // span_count + + def invalidateSpanIndexCache(self): + self._span_index_cache.clear() + + def setSpanCount(self, span_count: int): + self._span_count = max(1, span_count) + + def getSpanCount(self) -> int: + return self._span_count + + def setSpanSizeLookup(self, span_size_lookup: SpanSizeLookup): + pass + + def getSpanSizeLookup(self) -> SpanSizeLookup: + return GridLayoutManager.SpanSizeLookup() + + +class StaggeredGridLayoutManager(RecyclerView.LayoutManager): + """StaggeredGridLayoutManager — 瀑布流布局管理器""" + + HORIZONTAL = 0 + VERTICAL = 1 + + GAP_HANDLING_NONE = 0 + GAP_HANDLING_LAZY = 1 + GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2 + + def __init__(self, span_count: int = 1, orientation: int = VERTICAL): + super().__init__() + self._span_count = max(1, span_count) + self._orientation = orientation + self._gap_strategy = StaggeredGridLayoutManager.GAP_HANDLING_LAZY + + def setSpanCount(self, span_count: int): + self._span_count = max(1, span_count) + + def getSpanCount(self) -> int: + return self._span_count + + def setOrientation(self, orientation: int): + self._orientation = orientation + + def getOrientation(self) -> int: + return self._orientation + + def setGapStrategy(self, gap_strategy: int): + self._gap_strategy = gap_strategy + + def getGapStrategy(self) -> int: + return self._gap_strategy + + +# ═══════════════════════════════════════════════════════════════ +# LinearLayout +# ═══════════════════════════════════════════════════════════════ + +class LinearLayout(ViewGroup): + """LinearLayout — 线性布局""" + + HORIZONTAL = 0 + VERTICAL = 1 + SHOW_DIVIDER_NONE = 0 + SHOW_DIVIDER_BEGINNING = 1 + SHOW_DIVIDER_MIDDLE = 2 + SHOW_DIVIDER_END = 4 + + def __init__(self, context=None): + super().__init__(context) + self._orientation = LinearLayout.VERTICAL + self._gravity = Gravity.NO_GRAVITY + self._weight_sum: float = 0.0 + self._baseline_aligned = True + self._baseline_aligned_child_index = -1 + self._divider: Any = None + self._divider_padding: int = 0 + self._divider_show: int = LinearLayout.SHOW_DIVIDER_NONE + self._total_length: int = 0 + + def setOrientation(self, orientation: int): + self._orientation = orientation + + def getOrientation(self) -> int: + return self._orientation + + def setGravity(self, gravity: int): + self._gravity = gravity + + def getGravity(self) -> int: + return self._gravity + + def setWeightSum(self, weight_sum: float): + self._weight_sum = weight_sum + + def getWeightSum(self) -> float: + return self._weight_sum + + def setBaselineAligned(self, baseline_aligned: bool): + self._baseline_aligned = baseline_aligned + + def isBaselineAligned(self) -> bool: + return self._baseline_aligned + + def setDividerDrawable(self, divider: Any): + self._divider = divider + + def setShowDividers(self, show_dividers: int): + self._divider_show = show_dividers + + def getShowDividers(self) -> int: + return self._divider_show + + def getDividerDrawable(self) -> Any: + return self._divider + + def setDividerPadding(self, padding: int): + self._divider_padding = padding + + def getDividerPadding(self) -> int: + return self._divider_padding + + def getBaseline(self) -> int: + return -1 + + def onMeasure(self, width_measure_spec: int, height_measure_spec: int): + from .view import MeasureSpec + w = MeasureSpec.getSize(width_measure_spec) + h = MeasureSpec.getSize(height_measure_spec) + self.setMeasuredDimension(w, h) + + def onLayout(self, changed: bool, l: int, t: int, r: int, b: int): + pass + + class LayoutParams(ViewGroup.MarginLayoutParams): + """LinearLayout.LayoutParams — 线性布局参数""" + + def __init__(self, width: int = ViewGroup.LayoutParams.WRAP_CONTENT, + height: int = ViewGroup.LayoutParams.WRAP_CONTENT, + weight: float = 0.0): + super().__init__(width, height) + self.weight = weight + self.gravity = -1 + + def setLayoutWeight(self, weight: float): + self.weight = weight + + +# ═══════════════════════════════════════════════════════════════ +# RelativeLayout +# ═══════════════════════════════════════════════════════════════ + +class RelativeLayout(ViewGroup): + """RelativeLayout — 相对布局""" + + TRUE = -1 + FALSE = 0 + + # 规则常量 + ALIGN_PARENT_LEFT = 0 + ALIGN_PARENT_TOP = 1 + ALIGN_PARENT_RIGHT = 2 + ALIGN_PARENT_BOTTOM = 3 + ALIGN_PARENT_START = 4 + ALIGN_PARENT_END = 5 + CENTER_IN_PARENT = 6 + CENTER_HORIZONTAL = 7 + CENTER_VERTICAL = 8 + ABOVE = 9 + BELOW = 10 + LEFT_OF = 11 + RIGHT_OF = 12 + ALIGN_LEFT = 13 + ALIGN_TOP = 14 + ALIGN_RIGHT = 15 + ALIGN_BOTTOM = 16 + ALIGN_BASELINE = 17 + ALIGN_WITH_PARENT_IF_MISSING = 18 + START_OF = 19 + END_OF = 20 + ALIGN_START = 21 + ALIGN_END = 22 + + def __init__(self, context=None): + super().__init__(context) + self._gravity = Gravity.NO_GRAVITY + self._ignore_gravity_of_view_ids: List[int] = [] + self._has_left_weight = False + self._has_right_weight = False + self._has_top_weight = False + self._has_bottom_weight = False + + def setGravity(self, gravity: int): + self._gravity = gravity + + def getGravity(self) -> int: + return self._gravity + + def setHorizontalGravity(self, horizontal_gravity: int): + pass + + def setVerticalGravity(self, vertical_gravity: int): + pass + + def onMeasure(self, width_measure_spec: int, height_measure_spec: int): + from .view import MeasureSpec + w = MeasureSpec.getSize(width_measure_spec) + h = MeasureSpec.getSize(height_measure_spec) + self.setMeasuredDimension(w, h) + + def onLayout(self, changed: bool, l: int, t: int, r: int, b: int): + pass + + class LayoutParams(ViewGroup.MarginLayoutParams): + """RelativeLayout.LayoutParams — 相对布局参数""" + + def __init__(self, width: int = ViewGroup.LayoutParams.WRAP_CONTENT, + height: int = ViewGroup.LayoutParams.WRAP_CONTENT): + super().__init__(width, height) + self._rules: Dict[int, int] = {} + self.align_with_parent = False + + def addRule(self, verb: int, subject: int = -1): + self._rules[verb] = subject + + def removeRule(self, verb: int): + self._rules.pop(verb, None) + + def getRule(self, verb: int) -> int: + return self._rules.get(verb, 0) + + def getRules(self) -> Dict[int, int]: + return dict(self._rules) + + def addLayoutRules(self, layout_rules: int): + pass + + def removeLayoutRule(self, layout_rule: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# FrameLayout +# ═══════════════════════════════════════════════════════════════ + +class FrameLayout(ViewGroup): + """FrameLayout — 帧布局""" + + def __init__(self, context=None): + super().__init__(context) + self._foreground: Any = None + self._foreground_gravity = Gravity.FILL + self._measure_all_children = False + self._foreground_bounds_padding: int = 0 + self._foreground_in_padding = False + + def setForeground(self, drawable: Any): + self._foreground = drawable + + def getForeground(self) -> Any: + return self._foreground + + def setForegroundGravity(self, gravity: int): + self._foreground_gravity = gravity + + def getForegroundGravity(self) -> int: + return self._foreground_gravity + + def setMeasureAllChildren(self, measure_all: bool): + self._measure_all_children = measure_all + + def getMeasureAllChildren(self) -> bool: + return self._measure_all_children + + def setForegroundBoundsPadding(self, padding: int): + self._foreground_bounds_padding = padding + + def onMeasure(self, width_measure_spec: int, height_measure_spec: int): + from .view import MeasureSpec + w = MeasureSpec.getSize(width_measure_spec) + h = MeasureSpec.getSize(height_measure_spec) + if self._measure_all_children: + for child in self._children: + child.measure(width_measure_spec, height_measure_spec) + self.setMeasuredDimension(w, h) + + def onLayout(self, changed: bool, l: int, t: int, r: int, b: int): + pass + + def gatherTransparentRegion(self, region: Any) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# TableLayout / TableRow +# ═══════════════════════════════════════════════════════════════ + +class TableLayout(LinearLayout): + """TableLayout — 表格布局""" + + def __init__(self, context=None): + super().__init__(context) + self._stretchable_columns: List[bool] = [] + self._shrinkable_columns: List[bool] = [] + self._collapsed_columns: List[bool] = [] + self._max_columns: int = 0 + + def setStretchAllColumns(self, stretch_all: bool): + pass + + def setShrinkAllColumns(self, shrink_all: bool): + pass + + def setColumnStretchable(self, column_index: int, stretchable: bool): + pass + + def setColumnShrinkable(self, column_index: int, shrinkable: bool): + pass + + def setColumnCollapsed(self, column_index: int, collapsed: bool): + pass + + def isColumnStretchable(self, column_index: int) -> bool: + return False + + def isColumnShrinkable(self, column_index: int) -> bool: + return False + + def isColumnCollapsed(self, column_index: int) -> bool: + return False + + +class TableRow(LinearLayout): + """TableRow — 表格行""" + + def __init__(self, context=None): + super().__init__(context) + self._layout_params: Dict[int, Any] = {} + + class LayoutParams(LinearLayout.LayoutParams): + """TableRow.LayoutParams — 表格行布局参数""" + + def __init__(self, width: int = ViewGroup.LayoutParams.WRAP_CONTENT, + height: int = ViewGroup.LayoutParams.WRAP_CONTENT): + super().__init__(width, height) + self.column = -1 + self.span = 1 + + +# ═══════════════════════════════════════════════════════════════ +# ScrollView / HorizontalScrollView +# ═══════════════════════════════════════════════════════════════ + +class ScrollView(FrameLayout): + """ScrollView — 滚动视图""" + + def __init__(self, context=None): + super().__init__(context) + self._fill_viewport = False + self._smooth_scrolling_enabled = True + self._max_scroll_amount = 0 + self._scroll_position = 0 + self._is_layout_after_scroll = False + + def setFillViewport(self, fill_viewport: bool): + self._fill_viewport = fill_viewport + + def isFillViewport(self) -> bool: + return self._fill_viewport + + def setSmoothScrollingEnabled(self, enabled: bool): + self._smooth_scrolling_enabled = enabled + + def isSmoothScrollingEnabled(self) -> bool: + return self._smooth_scrolling_enabled + + def fullScroll(self, direction: int) -> bool: + return True + + def arrowScroll(self, direction: int) -> bool: + return True + + def smoothScrollTo(self, x: int, y: int): + self._scroll_position = y + + def smoothScrollBy(self, dx: int, dy: int): + self._scroll_position += dy + + def pageScroll(self, direction: int) -> bool: + return True + + def getMaxScrollAmount(self) -> int: + return self._max_scroll_amount + + def computeScrollDeltaToGetChildRectOnScreen(self, rect: Any) -> int: + return 0 + + def isLayoutAfterScroll(self) -> bool: + return self._is_layout_after_scroll + + +class HorizontalScrollView(FrameLayout): + """HorizontalScrollView — 水平滚动视图""" + + def __init__(self, context=None): + super().__init__(context) + self._fill_viewport = False + + def setFillViewport(self, fill_viewport: bool): + self._fill_viewport = fill_viewport + + def isFillViewport(self) -> bool: + return self._fill_viewport + + def fullScroll(self, direction: int) -> bool: + return True + + def arrowScroll(self, direction: int) -> bool: + return True + + def smoothScrollTo(self, x: int, y: int): + pass + + def smoothScrollBy(self, dx: int, dy: int): + pass + + def pageScroll(self, direction: int) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# WebView +# ═══════════════════════════════════════════════════════════════ + +class WebView(ViewGroup): + """WebView — 网页视图""" + + # 布局模式 + NORMAL = 0 + SMALL_SCREEN = 1 + LARGE_SCREEN = 2 + + def __init__(self, context=None): + super().__init__(context) + self._settings: "WebSettings" = WebSettings() + self._web_view_client: Optional[Callable] = None + self._web_chrome_client: Optional[Callable] = None + self._can_go_back: bool = False + self._can_go_forward: bool = False + self._current_url: str = "" + self._history: List[str] = [] + self._current_index: int = -1 + self._download_listener: Optional[Callable] = None + self._find_listener: Optional[Callable] = None + self._picture_listener: Optional[Callable] = None + + def loadUrl(self, url: str, additional_headers: Optional[Dict[str, str]] = None): + self._current_url = url + self._history = self._history[:self._current_index + 1] + self._history.append(url) + self._current_index = len(self._history) - 1 + self._can_go_back = self._current_index > 0 + self._can_go_forward = False + + def loadData(self, data: str, mime_type: str = "text/html", + encoding: str = "utf-8"): + self._current_url = "about:blank" + + def loadDataWithBaseURL(self, base_url: str, data: str, mime_type: str, + encoding: str, history_url: str): + self._current_url = base_url or "about:blank" + + def getSettings(self) -> "WebSettings": + return self._settings + + def setWebViewClient(self, client: Any): + self._web_view_client = client + + def getWebViewClient(self) -> Any: + return self._web_view_client + + def setWebChromeClient(self, client: Any): + self._web_chrome_client = client + + def getWebChromeClient(self) -> Any: + return self._web_chrome_client + + def goBack(self): + if self._can_go_back: + self._current_index -= 1 + self._current_url = self._history[self._current_index] + self._can_go_back = self._current_index > 0 + self._can_go_forward = True + + def goForward(self): + if self._can_go_forward: + self._current_index += 1 + self._current_url = self._history[self._current_index] + self._can_go_forward = self._current_index < len(self._history) - 1 + self._can_go_back = True + + def reload(self): + pass + + def canGoBack(self) -> bool: + return self._can_go_back + + def canGoForward(self) -> bool: + return self._can_go_forward + + def clearHistory(self): + self._history.clear() + self._current_index = -1 + self._can_go_back = False + self._can_go_forward = False + + def destroy(self): + pass + + def onPause(self): + pass + + def onResume(self): + pass + + def pauseTimers(self): + pass + + def resumeTimers(self): + pass + + def getUrl(self) -> Optional[str]: + return self._current_url if self._current_url else None + + def getTitle(self) -> Optional[str]: + return None + + def evaluateJavascript(self, script: str, callback: Optional[Callable] = None): + if callback: + callback("") + + def addJavascriptInterface(self, obj: Any, name: str): + pass + + def removeJavascriptInterface(self, name: str): + pass + + def canGoBackOrForward(self, steps: int) -> bool: + target = self._current_index + steps + return 0 <= target < len(self._history) + + def goBackOrForward(self, steps: int): + target = self._current_index + steps + if 0 <= target < len(self._history): + self._current_index = target + self._current_url = self._history[target] + self._can_go_back = target > 0 + self._can_go_forward = target < len(self._history) - 1 + + def setDownloadListener(self, listener: Optional[Callable]): + self._download_listener = listener + + def setFindListener(self, listener: Optional[Callable]): + self._find_listener = listener + + class WebViewTransport: + def setWebView(self, web_view: "WebView"): + pass + def getWebView(self) -> Optional["WebView"]: + return None + + class HitTestResult: + UNKNOWN_TYPE = 0 + ANCHOR_TYPE = 1 + EDIT_TEXT_TYPE = 2 + IMAGE_TYPE = 3 + SRC_ANCHOR_TYPE = 4 + SRC_IMAGE_ANCHOR_TYPE = 5 + PHONE_TYPE = 6 + GEO_TYPE = 7 + EMAIL_TYPE = 8 + + def __init__(self): + self._type = WebView.HitTestResult.UNKNOWN_TYPE + self._extra = "" + + def getType(self) -> int: + return self._type + def getExtra(self) -> str: + return self._extra + + def getHitTestResult(self) -> HitTestResult: + return WebView.HitTestResult() + + def findAllAsync(self, find_str: str): + pass + + def findNext(self, forward: bool): + pass + + def clearMatches(self): + pass + + def zoomIn(self) -> bool: + return False + + def zoomOut(self) -> bool: + return False + + def setInitialScale(self, scale_in_percent: int): + pass + + def getScale(self) -> float: + return 1.0 + + def setPictureListener(self, listener: Optional[Callable]): + self._picture_listener = listener + + +class WebSettings(Stub): + """WebSettings — WebView 设置""" + + # 渲染模式 + RENDER_GOES_BROWSER = 0 + RENDER_GOES_SURFACE_VIEW = 1 + RENDER_GOES_DISPLAY_LIST = 2 + RENDER_GOES_GPU = 3 + RENDER_GOES_SURFACE = 4 + + # 布局算法 + LAYOUT_ALGORITHM_NORMAL = 0 + LAYOUT_ALGORITHM_SINGLE_COLUMN = 1 + LAYOUT_ALGORITHM_NARROW_COLUMNS = 2 + LAYOUT_ALGORITHM_TEXT_AUTOSIZING = 3 + + def __init__(self): + super().__init__() + self._javascript_enabled = False + self._dom_storage_enabled = False + self._allow_file_access = True + self._built_in_zoom_controls = True + self._display_zoom_controls = True + self._load_with_overview_mode = False + self._support_zoom = True + self._use_wide_view_port = False + self._database_enabled = False + self._allow_content_access = True + self._cache_mode = -1 # LOAD_DEFAULT + self._user_agent_string: str = "Mozilla/5.0 (Linux; Android 13)" + self._default_font_size = 16 + self._minimum_font_size = 8 + self._text_zoom = 100 + self._standard_font_family = "sans-serif" + self._serif_font_family = "serif" + self._sans_serif_font_family = "sans-serif" + self._fixed_font_family = "monospace" + self._cursive_font_family = "cursive" + self._fantasy_font_family = "fantasy" + self._default_encoding = "utf-8" + self._layout_algorithm = WebSettings.LAYOUT_ALGORITHM_NORMAL + self._block_network_image = False + self._block_network_loads = False + self._mixed_content_mode = 0 # MIXED_CONTENT_ALWAYS_ALLOW + self._offscreen_pre_raster = False + self._safe_browsing_enabled = True + + def setJavaScriptEnabled(self, enabled: bool): + self._javascript_enabled = enabled + + def getJavaScriptEnabled(self) -> bool: + return self._javascript_enabled + + def setDomStorageEnabled(self, enabled: bool): + self._dom_storage_enabled = enabled + + def getDomStorageEnabled(self) -> bool: + return self._dom_storage_enabled + + def setAllowFileAccess(self, allow: bool): + self._allow_file_access = allow + + def getAllowFileAccess(self) -> bool: + return self._allow_file_access + + def setBuiltInZoomControls(self, enabled: bool): + self._built_in_zoom_controls = enabled + + def getBuiltInZoomControls(self) -> bool: + return self._built_in_zoom_controls + + def setDisplayZoomControls(self, enabled: bool): + self._display_zoom_controls = enabled + + def getDisplayZoomControls(self) -> bool: + return self._display_zoom_controls + + def setSupportZoom(self, support: bool): + self._support_zoom = support + + def supportZoom(self) -> bool: + return self._support_zoom + + def setLoadWithOverviewMode(self, overview: bool): + self._load_with_overview_mode = overview + + def getLoadWithOverviewMode(self) -> bool: + return self._load_with_overview_mode + + def setUseWideViewPort(self, use: bool): + self._use_wide_view_port = use + + def getUseWideViewPort(self) -> bool: + return self._use_wide_view_port + + def setDatabaseEnabled(self, enabled: bool): + self._database_enabled = enabled + + def getDatabaseEnabled(self) -> bool: + return self._database_enabled + + def setAllowContentAccess(self, allow: bool): + self._allow_content_access = allow + + def getAllowContentAccess(self) -> bool: + return self._allow_content_access + + def setCacheMode(self, mode: int): + self._cache_mode = mode + + def getCacheMode(self) -> int: + return self._cache_mode + + LOAD_DEFAULT = -1 + LOAD_CACHE_ELSE_NETWORK = 1 + LOAD_NO_CACHE = 2 + LOAD_CACHE_ONLY = 3 + + def setUserAgentString(self, ua: str): + self._user_agent_string = ua + + def getUserAgentString(self) -> str: + return self._user_agent_string + + def setDefaultFontSize(self, size: int): + self._default_font_size = size + + def getDefaultFontSize(self) -> int: + return self._default_font_size + + def setMinimumFontSize(self, size: int): + self._minimum_font_size = size + + def getMinimumFontSize(self) -> int: + return self._minimum_font_size + + def setTextZoom(self, text_zoom: int): + self._text_zoom = text_zoom + + def getTextZoom(self) -> int: + return self._text_zoom + + def setStandardFontFamily(self, font: str): + self._standard_font_family = font + + def setSerifFontFamily(self, font: str): + self._serif_font_family = font + + def setSansSerifFontFamily(self, font: str): + self._sans_serif_font_family = font + + def setFixedFontFamily(self, font: str): + self._fixed_font_family = font + + def setCursiveFontFamily(self, font: str): + self._cursive_font_family = font + + def setFantasyFontFamily(self, font: str): + self._fantasy_font_family = font + + def setDefaultTextEncodingName(self, encoding: str): + self._default_encoding = encoding + + def getDefaultTextEncodingName(self) -> str: + return self._default_encoding + + def setLayoutAlgorithm(self, algorithm: int): + self._layout_algorithm = algorithm + + def getLayoutAlgorithm(self) -> int: + return self._layout_algorithm + + def setBlockNetworkImage(self, block: bool): + self._block_network_image = block + + def getBlockNetworkImage(self) -> bool: + return self._block_network_image + + def setBlockNetworkLoads(self, block: bool): + self._block_network_loads = block + + def getBlockNetworkLoads(self) -> bool: + return self._block_network_loads + + def setMixedContentMode(self, mode: int): + self._mixed_content_mode = mode + + def getMixedContentMode(self) -> int: + return self._mixed_content_mode + + MIXED_CONTENT_ALWAYS_ALLOW = 0 + MIXED_CONTENT_NEVER_ALLOW = 1 + MIXED_CONTENT_COMPATIBILITY_MODE = 2 + + def setOffscreenPreRaster(self, enabled: bool): + self._offscreen_pre_raster = enabled + + def getOffscreenPreRaster(self) -> bool: + return self._offscreen_pre_raster + + def setSafeBrowsingEnabled(self, enabled: bool): + self._safe_browsing_enabled = enabled + + def getSafeBrowsingEnabled(self) -> bool: + return self._safe_browsing_enabled + + def setNeedInitialFocus(self, flag: bool): + pass + + +# ═══════════════════════════════════════════════════════════════ +# ViewAnimator / ViewFlipper / ViewSwitcher etc. +# ═══════════════════════════════════════════════════════════════ + +class ViewAnimator(FrameLayout): + """ViewAnimator — 视图动画容器""" + + def __init__(self, context=None): + super().__init__(context) + self._which_child: int = 0 + self._animate_first_view: bool = False + self._in_animation: Any = None + self._out_animation: Any = None + + def setDisplayedChild(self, which_child: int): + if 0 <= which_child < self.getChildCount(): + self._which_child = which_child + + def getDisplayedChild(self) -> int: + return self._which_child + + def showNext(self): + if self._which_child < self.getChildCount() - 1: + self._which_child += 1 + + def showPrevious(self): + if self._which_child > 0: + self._which_child -= 1 + + def setInAnimation(self, in_animation: Any): + self._in_animation = in_animation + + def getInAnimation(self) -> Any: + return self._in_animation + + def setOutAnimation(self, out_animation: Any): + self._out_animation = out_animation + + def getOutAnimation(self) -> Any: + return self._out_animation + + def setAnimateFirstView(self, animate: bool): + self._animate_first_view = animate + + def getAnimateFirstView(self) -> bool: + return self._animate_first_view + + def getBaseline(self) -> int: + return -1 + + +class ViewFlipper(ViewAnimator): + """ViewFlipper — 视图翻转器""" + + def __init__(self, context=None): + super().__init__(context) + self._flip_interval: int = 3000 + self._auto_start: bool = False + self._running: bool = False + self._flip_timer: Any = None + + def setFlipInterval(self, milliseconds: int): + self._flip_interval = milliseconds + + def getFlipInterval(self) -> int: + return self._flip_interval + + def setAutoStart(self, auto_start: bool): + self._auto_start = auto_start + + def isAutoStart(self) -> bool: + return self._auto_start + + def startFlipping(self): + self._running = True + + def stopFlipping(self): + self._running = False + + def isFlipping(self) -> bool: + return self._running + + def onDetachedFromWindow(self): + super().onDetachedFromWindow() + self._running = False + + def onAttachedToWindow(self): + super().onAttachedToWindow() + if self._auto_start: + self._running = True + + +class ViewSwitcher(ViewAnimator): + """ViewSwitcher — 视图切换器""" + + def __init__(self, context=None): + super().__init__(context) + self._factory: Optional[Callable] = None + + class ViewFactory: + def makeView(self) -> View: + return View() + + def setFactory(self, factory: ViewFactory): + self._factory = factory + + def getCurrentView(self) -> Optional[View]: + return self.getChildAt(self._which_child) + + def getNextView(self) -> Optional[View]: + next_index = self._which_child + 1 + if next_index < self.getChildCount(): + return self.getChildAt(next_index) + return None + + def setInAnimation(self, in_animation: Any): + super().setInAnimation(in_animation) + + def setOutAnimation(self, out_animation: Any): + super().setOutAnimation(out_animation) + + +class ImageSwitcher(ViewSwitcher): + """ImageSwitcher — 图片切换器""" + + def setImageDrawable(self, drawable: Any): + pass + + def setImageResource(self, resid: int): + pass + + def setImageURI(self, uri: Any): + pass + + def setImageBitmap(self, bitmap: Any): + pass + + +class TextSwitcher(ViewSwitcher): + """TextSwitcher — 文本切换器""" + + def setCurrentText(self, text: str): + if self.getChildCount() > 0: + child = self.getChildAt(0) + if hasattr(child, "setText"): + child.setText(text) + + def setText(self, text: str): + self.setCurrentText(text) + + +class AdapterViewAnimator(AdapterView): + """AdapterViewAnimator — 适配器视图动画器""" + + def __init__(self, context=None): + super().__init__(context) + self._in_animation: Any = None + self._out_animation: Any = None + self._displayed_child: int = 0 + self._loop_views: bool = False + + def setDisplayedChild(self, which_child: int): + self._displayed_child = which_child + + def getDisplayedChild(self) -> int: + return self._displayed_child + + def showNext(self): + self._displayed_child = (self._displayed_child + 1) % max(1, self.getCount()) + + def showPrevious(self): + last = max(1, self.getCount()) + self._displayed_child = (self._displayed_child - 1) % last + + def setInAnimation(self, in_animation: Any): + self._in_animation = in_animation + + def getInAnimation(self) -> Any: + return self._in_animation + + def setOutAnimation(self, out_animation: Any): + self._out_animation = out_animation + + def getOutAnimation(self) -> Any: + return self._out_animation + + def getCurrentView(self) -> Optional[View]: + return self.getChildAt(self._displayed_child) + + def getBaseline(self) -> int: + return -1 + + def getCount(self) -> int: + if self._adapter: + return self._adapter.getCount() + return 0 + + +class AdapterViewFlipper(AdapterViewAnimator): + """AdapterViewFlipper — 适配器视图翻转器""" + + def __init__(self, context=None): + super().__init__(context) + self._flip_interval: int = 10000 + self._auto_start: bool = False + self._running: bool = False + + def setFlipInterval(self, milliseconds: int): + self._flip_interval = milliseconds + + def getFlipInterval(self) -> int: + return self._flip_interval + + def startFlipping(self): + self._running = True + + def stopFlipping(self): + self._running = False + + def isFlipping(self) -> bool: + return self._running + + def setAutoStart(self, auto_start: bool): + self._auto_start = auto_start + + def isAutoStart(self) -> bool: + return self._auto_start + + +class StackView(AdapterViewAnimator): + """StackView — 栈视图""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# Adapters +# ═══════════════════════════════════════════════════════════════ + +class BaseAdapter(Stub): + """BaseAdapter — 基础适配器""" + + def __init__(self): + super().__init__() + self._data_set_observable: Any = None + + def getCount(self) -> int: + return 0 + + def getItem(self, position: int) -> Any: + return None + + def getItemId(self, position: int) -> int: + return position + + def getView(self, position: int, convert_view: Optional[View], parent: Optional[ViewGroup]) -> View: + return View() + + def getItemViewType(self, position: int) -> int: + return 0 + + def getViewTypeCount(self) -> int: + return 1 + + def hasStableIds(self) -> bool: + return False + + def isEmpty(self) -> bool: + return self.getCount() == 0 + + def areAllItemsEnabled(self) -> bool: + return True + + def isEnabled(self, position: int) -> bool: + return True + + def getDropDownView(self, position: int, convert_view: Optional[View], + parent: Optional[ViewGroup]) -> View: + return self.getView(position, convert_view, parent) + + def notifyDataSetChanged(self): + pass + + def notifyDataSetInvalidated(self): + pass + + def registerDataSetObserver(self, observer: Any): + pass + + def unregisterDataSetObserver(self, observer: Any): + pass + + +class ArrayAdapter(BaseAdapter): + """ArrayAdapter — 数组适配器""" + + def __init__(self, context: Any, resource: int, objects: Optional[List[Any]] = None): + super().__init__() + self._context = context + self._resource = resource + self._drop_down_resource = resource + self._objects: List[Any] = list(objects) if objects is not None else [] + self._field_id = 0 + self._notify_on_change = True + + def getCount(self) -> int: + return len(self._objects) + + def getItem(self, position: int) -> Any: + if 0 <= position < len(self._objects): + return self._objects[position] + return None + + def getPosition(self, item: Any) -> int: + try: + return self._objects.index(item) + except ValueError: + return -1 + + def getView(self, position: int, convert_view: Optional[View], + parent: Optional[ViewGroup]) -> View: + if convert_view is not None: + tv = convert_view + else: + tv = TextView(self._context) + if hasattr(tv, "setText"): + tv.setText(str(self.getItem(position) if self.getItem(position) is not None else "")) + return tv + + def add(self, obj: Any): + self._objects.append(obj) + if self._notify_on_change: + self.notifyDataSetChanged() + + def addAll(self, *items: Any): + for item in items: + self._objects.append(item) + if self._notify_on_change: + self.notifyDataSetChanged() + + def insert(self, obj: Any, index: int): + self._objects.insert(index, obj) + if self._notify_on_change: + self.notifyDataSetChanged() + + def remove(self, obj: Any): + if obj in self._objects: + self._objects.remove(obj) + if self._notify_on_change: + self.notifyDataSetChanged() + + def clear(self): + self._objects.clear() + if self._notify_on_change: + self.notifyDataSetChanged() + + def sort(self, comparator: Callable): + self._objects.sort(key=comparator) + if self._notify_on_change: + self.notifyDataSetChanged() + + def setNotifyOnChange(self, notify_on_change: bool): + self._notify_on_change = notify_on_change + + def getContext(self): + return self._context + + def getCount(self) -> int: + return len(self._objects) + + def setDropDownViewResource(self, resource: int): + self._drop_down_resource = resource + + def getDropDownViewResource(self) -> int: + return self._drop_down_resource + + +class SimpleAdapter(BaseAdapter): + """SimpleAdapter — 简单适配器""" + + def __init__(self, context: Any, data: List[Dict[str, Any]], resource: int, + from_cols: List[str], to_ids: List[int]): + super().__init__() + self._context = context + self._data = data + self._resource = resource + self._from = from_cols + self._to = to_ids + self._drop_down_resource = resource + + def getCount(self) -> int: + return len(self._data) + + def getItem(self, position: int) -> Any: + if 0 <= position < len(self._data): + return self._data[position] + return None + + def getView(self, position: int, convert_view: Optional[View], + parent: Optional[ViewGroup]) -> View: + if convert_view is not None: + v = convert_view + else: + v = View(self._context) + return v + + def setDropDownViewResource(self, resource: int): + self._drop_down_resource = resource + + +class CursorAdapter(BaseAdapter): + """CursorAdapter — 游标适配器""" + + FLAG_AUTO_REQUERY = 0x01 + FLAG_REGISTER_CONTENT_OBSERVER = 0x02 + + def __init__(self, context: Any, c: Any, flags: int = FLAG_AUTO_REQUERY): + super().__init__() + self._context = context + self._cursor = c + self._flags = flags + + def getCount(self) -> int: + return 0 + + def getItem(self, position: int) -> Any: + return None + + def getView(self, position: int, convert_view: Optional[View], + parent: Optional[ViewGroup]) -> View: + return View(self._context) + + def bindView(self, view: View, context: Any, cursor: Any): + pass + + def newView(self, context: Any, cursor: Any, parent: ViewGroup) -> View: + return View(context) + + def getCursor(self) -> Any: + return self._cursor + + def changeCursor(self, cursor: Any): + self._cursor = cursor + self.notifyDataSetChanged() + + def swapCursor(self, new_cursor: Any) -> Any: + old = self._cursor + self._cursor = new_cursor + self.notifyDataSetChanged() + return old + + +# ═══════════════════════════════════════════════════════════════ +# Spinner +# ═══════════════════════════════════════════════════════════════ + +class Spinner(AdapterView): + """Spinner — 下拉选择器""" + + MODE_DIALOG = 0 + MODE_DROPDOWN = 1 + + def __init__(self, context=None, mode: int = MODE_DROPDOWN): + super().__init__(context) + self._mode = mode + self._prompt: str = "" + self._popup: Any = None + self._on_item_selected_listener: Optional[Callable] = None + + class OnItemSelectedListener: + def onItemSelected(self, parent: AdapterView, view: View, position: int, id_: int): + pass + def onNothingSelected(self, parent: AdapterView): + pass + + def setOnItemSelectedListener(self, listener: Optional[OnItemSelectedListener]): + self._on_item_selected_listener = listener + + def getOnItemSelectedListener(self) -> Optional[OnItemSelectedListener]: + return self._on_item_selected_listener + + def setSelection(self, position: int, animate: bool = True): + super().setSelection(position) + + def getSelectedItem(self) -> Any: + return super().getSelectedItem() + + def getSelectedItemPosition(self) -> int: + return self._selected_position + + def getSelectedView(self) -> Optional[View]: + return None + + def setPrompt(self, prompt: str): + self._prompt = prompt + + def getPrompt(self) -> str: + return self._prompt + + def setPopupBackgroundDrawable(self, drawable: Any): + pass + + def getPopupBackgroundDrawable(self) -> Any: + return None + + def performClick(self) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# AutoCompleteTextView +# ═══════════════════════════════════════════════════════════════ + +class AutoCompleteTextView(EditText): + """AutoCompleteTextView — 自动完成文本""" + + def __init__(self, context=None): + super().__init__(context) + self._threshold: int = 2 + self._completion_hint: str = "" + self._drop_down_anchor: Optional[View] = None + self._drop_down_horizontal_offset: int = 0 + self._drop_down_vertical_offset: int = 0 + self._drop_down_width: int = 0 + self._drop_down_height: int = 0 + self._on_item_click_listener: Optional[Callable] = None + self._on_dismiss_listener: Optional[Callable] = None + self._validator: Optional[Callable] = None + + def setThreshold(self, threshold: int): + self._threshold = max(1, threshold) + + def getThreshold(self) -> int: + return self._threshold + + def setAdapter(self, adapter: Any): + self._adapter = adapter + + def getAdapter(self) -> Any: + return self._adapter + + def setOnItemClickListener(self, listener: Optional[Callable]): + self._on_item_click_listener = listener + + def getOnItemClickListener(self) -> Optional[Callable]: + return self._on_item_click_listener + + def setCompletionHint(self, hint: str): + self._completion_hint = hint + + def getCompletionHint(self) -> str: + return self._completion_hint + + def setDropDownAnchor(self, anchor_id: int): + pass + + def setDropDownHorizontalOffset(self, offset: int): + self._drop_down_horizontal_offset = offset + + def getDropDownHorizontalOffset(self) -> int: + return self._drop_down_horizontal_offset + + def setDropDownVerticalOffset(self, offset: int): + self._drop_down_vertical_offset = offset + + def getDropDownVerticalOffset(self) -> int: + return self._drop_down_vertical_offset + + def setDropDownWidth(self, width: int): + self._drop_down_width = width + + def getDropDownWidth(self) -> int: + return self._drop_down_width + + def showDropDown(self): + pass + + def dismissDropDown(self): + pass + + def isDropDownShowing(self) -> bool: + return False + + def enoughToFilter(self) -> bool: + return len(self._text) >= self._threshold + + def setValidator(self, validator: Optional[Callable]): + self._validator = validator + + def getValidator(self) -> Optional[Callable]: + return self._validator + + def performValidation(self): + pass + + def setOnDismissListener(self, listener: Optional[Callable]): + self._on_dismiss_listener = listener + + def performFiltering(self, text: str, key_code: int): + pass + + def onFilterComplete(self, count: int): + pass + + +class MultiAutoCompleteTextView(AutoCompleteTextView): + """MultiAutoCompleteTextView — 多值自动完成文本""" + + def __init__(self, context=None): + super().__init__(context) + self._tokenizer: Optional[Callable] = None + + class CommaTokenizer: + def tokenize(self, text: str) -> List[str]: + return text.split(",") + def findTokenStart(self, text: str, cursor: int) -> int: + return text.rfind(",", 0, cursor) + 1 + def findTokenEnd(self, text: str, cursor: int) -> int: + next_comma = text.find(",", cursor) + return next_comma if next_comma >= 0 else len(text) + def terminateToken(self, text: str) -> str: + return text + "," + + def setTokenizer(self, tokenizer: Any): + self._tokenizer = tokenizer + + def getTokenizer(self) -> Any: + return self._tokenizer + + +# ═══════════════════════════════════════════════════════════════ +# DatePicker / TimePicker / CalendarView / NumberPicker +# ═══════════════════════════════════════════════════════════════ + +class DatePicker(FrameLayout): + """DatePicker — 日期选择器""" + + def __init__(self, context=None): + super().__init__(context) + self._year: int = 2024 + self._month: int = 0 # 0-based + self._day_of_month: int = 1 + self._min_date: int = 0 + self._max_date: int = 0 + self._spinners_shown: bool = True + self._calendar_view_shown: bool = False + self._on_date_changed_listener: Optional[Callable] = None + self._calendar_view: Optional[CalendarView] = None + + class OnDateChangedListener: + def onDateChanged(self, view: "DatePicker", year: int, month_of_year: int, day_of_month: int): + pass + + def init(self, year: int, month: int, day_of_month: int, + listener: Optional[OnDateChangedListener] = None): + self._year = year + self._month = month + self._day_of_month = day_of_month + self._on_date_changed_listener = listener + if listener: + listener.onDateChanged(self, year, month, day_of_month) + + def getYear(self) -> int: + return self._year + + def getMonth(self) -> int: + return self._month + + def getDayOfMonth(self) -> int: + return self._day_of_month + + def setMinDate(self, min_date: int): + self._min_date = min_date + + def getMinDate(self) -> int: + return self._min_date + + def setMaxDate(self, max_date: int): + self._max_date = max_date + + def getMaxDate(self) -> int: + return self._max_date + + def setSpinnersShown(self, shown: bool): + self._spinners_shown = shown + + def setCalendarViewShown(self, shown: bool): + self._calendar_view_shown = shown + + def getCalendarView(self) -> Optional["CalendarView"]: + return self._calendar_view + + def setOnDateChangedListener(self, listener: Optional[OnDateChangedListener]): + self._on_date_changed_listener = listener + + def updateDate(self, year: int, month: int, day_of_month: int): + self._year = year + self._month = month + self._day_of_month = day_of_month + if self._on_date_changed_listener: + self._on_date_changed_listener.onDateChanged(self, year, month, day_of_month) + + +class TimePicker(FrameLayout): + """TimePicker — 时间选择器""" + + MODE_CLOCK = 0 + MODE_SPINNER = 1 + + def __init__(self, context=None, mode: int = MODE_CLOCK): + super().__init__(context) + self._current_hour: int = 12 + self._current_minute: int = 0 + self._is24_hour_view: Optional[bool] = None # None = system default + self._mode = mode + self._on_time_changed_listener: Optional[Callable] = None + + class OnTimeChangedListener: + def onTimeChanged(self, view: "TimePicker", hour_of_day: int, minute: int): + pass + + def setOnTimeChangedListener(self, listener: Optional[OnTimeChangedListener]): + self._on_time_changed_listener = listener + + def getOnTimeChangedListener(self) -> Optional[OnTimeChangedListener]: + return self._on_time_changed_listener + + def getCurrentHour(self) -> int: + return self._current_hour + + def setCurrentHour(self, current_hour: int): + self._current_hour = current_hour % 24 + if self._on_time_changed_listener: + self._on_time_changed_listener.onTimeChanged(self, self._current_hour, self._current_minute) + + def getCurrentMinute(self) -> int: + return self._current_minute + + def setCurrentMinute(self, current_minute: int): + self._current_minute = current_minute % 60 + if self._on_time_changed_listener: + self._on_time_changed_listener.onTimeChanged(self, self._current_hour, self._current_minute) + + def setIs24HourView(self, is24_hour_view: Optional[bool]): + self._is24_hour_view = is24_hour_view + + def is24HourView(self) -> bool: + if self._is24_hour_view is not None: + return self._is24_hour_view + return False + + def getMode(self) -> int: + return self._mode + + def setHour(self, hour: int): + self.setCurrentHour(hour) + + def getHour(self) -> int: + return self.getCurrentHour() + + def setMinute(self, minute: int): + self.setCurrentMinute(minute) + + def getMinute(self) -> int: + return self.getCurrentMinute() + + +class CalendarView(FrameLayout): + """CalendarView — 日历视图""" + + def __init__(self, context=None): + super().__init__(context) + self._date: int = 0 + self._min_date: int = 0 + self._max_date: int = 0 + self._first_day_of_week: int = 1 # Sunday=1 + self._week_separator: bool = False + self._on_date_change_listener: Optional[Callable] = None + self._shown_week_count: int = 6 + + class OnDateChangeListener: + def onSelectedDayChange(self, view: "CalendarView", year: int, month: int, day_of_month: int): + pass + + def setOnDateChangeListener(self, listener: Optional[OnDateChangeListener]): + self._on_date_change_listener = listener + + def getDate(self) -> int: + return self._date + + def setDate(self, date: int, animate: bool = True, center: bool = True): + self._date = date + + def getMinDate(self) -> int: + return self._min_date + + def setMinDate(self, min_date: int): + self._min_date = min_date + + def getMaxDate(self) -> int: + return self._max_date + + def setMaxDate(self, max_date: int): + self._max_date = max_date + + def getFirstDayOfWeek(self) -> int: + return self._first_day_of_week + + def setFirstDayOfWeek(self, first_day_of_week: int): + self._first_day_of_week = first_day_of_week + + def setWeekSeparatorDrawable(self, drawable: Any): + self._week_separator = True + + def getShownWeekCount(self) -> int: + return self._shown_week_count + + def setShownWeekCount(self, count: int): + self._shown_week_count = count + + +class NumberPicker(ViewGroup): + """NumberPicker — 数字选择器""" + + def __init__(self, context=None): + super().__init__(context) + self._min_value: int = 0 + self._max_value: int = 100 + self._value: int = 0 + self._displayed_values: Optional[List[str]] = None + self._on_value_change_listener: Optional[Callable] = None + self._on_scroll_listener: Optional[Callable] = None + self._wrap_selector_wheel: bool = True + self._selection_divider: bool = True + + class OnValueChangeListener: + def onValueChange(self, picker: "NumberPicker", old_val: int, new_val: int): + pass + + class OnScrollListener: + def onScrollStateChange(self, picker: "NumberPicker", state: int): + pass + + SCROLL_STATE_IDLE = 0 + SCROLL_STATE_TOUCH_SCROLL = 1 + SCROLL_STATE_FLING = 2 + + def setMinValue(self, min_value: int): + self._min_value = min_value + if self._value < self._min_value: + self._value = self._min_value + + def getMinValue(self) -> int: + return self._min_value + + def setMaxValue(self, max_value: int): + self._max_value = max_value + if self._value > self._max_value: + self._value = self._max_value + + def getMaxValue(self) -> int: + return self._max_value + + def setValue(self, value: int): + old_value = self._value + self._value = max(self._min_value, min(self._max_value, value)) + if self._on_value_change_listener and old_value != self._value: + self._on_value_change_listener.onValueChange(self, old_value, self._value) + + def getValue(self) -> int: + return self._value + + def setDisplayedValues(self, displayed_values: Optional[List[str]]): + self._displayed_values = displayed_values + + def getDisplayedValues(self) -> Optional[List[str]]: + return self._displayed_values + + def setOnValueChangedListener(self, listener: Optional[OnValueChangeListener]): + self._on_value_change_listener = listener + + def setOnScrollListener(self, listener: Optional[OnScrollListener]): + self._on_scroll_listener = listener + + def setWrapSelectorWheel(self, wrap: bool): + self._wrap_selector_wheel = wrap + + def getWrapSelectorWheel(self) -> bool: + return self._wrap_selector_wheel + + def setSelectionDivider(self, divider: Any): + self._selection_divider = True + + +# ═══════════════════════════════════════════════════════════════ +# VideoView / MediaController +# ═══════════════════════════════════════════════════════════════ + +class VideoView(SurfaceView): + """VideoView — 视频播放视图""" + + def __init__(self, context=None): + super().__init__(context) + self._video_path: Optional[str] = None + self._uri: Any = None + self._media_controller: Optional["MediaController"] = None + self._on_prepared_listener: Optional[Callable] = None + self._on_completion_listener: Optional[Callable] = None + self._on_error_listener: Optional[Callable] = None + self._is_playing: bool = False + self._current_position: int = 0 + self._duration: int = 0 + self._can_seek_back: bool = True + self._can_seek_forward: bool = True + self._can_pause: bool = True + self._video_width: int = 0 + self._video_height: int = 0 + + def setVideoPath(self, path: str): + self._video_path = path + + def setVideoURI(self, uri: Any): + self._uri = uri + + def setMediaController(self, controller: "MediaController"): + self._media_controller = controller + + def setOnPreparedListener(self, listener: Optional[Callable]): + self._on_prepared_listener = listener + + def setOnCompletionListener(self, listener: Optional[Callable]): + self._on_completion_listener = listener + + def setOnErrorListener(self, listener: Optional[Callable]): + self._on_error_listener = listener + + def start(self): + self._is_playing = True + + def stopPlayback(self): + self._is_playing = False + self._current_position = 0 + + def pause(self): + self._is_playing = False + + def resume(self): + self._is_playing = True + + def seekTo(self, msec: int): + self._current_position = max(0, min(msec, self._duration)) + + def getCurrentPosition(self) -> int: + return self._current_position + + def getDuration(self) -> int: + return self._duration + + def isPlaying(self) -> bool: + return self._is_playing + + def canSeekForward(self) -> bool: + return self._can_seek_forward + + def canSeekBackward(self) -> bool: + return self._can_seek_back + + def canPause(self) -> bool: + return self._can_pause + + def getBufferPercentage(self) -> int: + return 100 + + def resolveAdjustedSize(self, desired_size: int, measure_spec: int) -> int: + from .view import MeasureSpec + return MeasureSpec.getSize(measure_spec) + + def setVideoWidth(self, video_width: int): + self._video_width = video_width + + def setVideoHeight(self, video_height: int): + self._video_height = video_height + + @property + def videoWidth(self) -> int: + return self._video_width + + @property + def videoHeight(self) -> int: + return self._video_height + + +class MediaController(FrameLayout): + """MediaController — 媒体控制器""" + + def __init__(self, context=None, target: Optional[VideoView] = None): + super().__init__(context) + self._target = target + self._anchor_view: Optional[View] = None + self._is_showing: bool = False + self._set_up_listener: Optional[Callable] = None + + def setMediaPlayer(self, player: "MediaController.MediaPlayerControl"): + pass + + def setAnchorView(self, view: View): + self._anchor_view = view + + def show(self, timeout: int = 3000): + self._is_showing = True + + def hide(self): + self._is_showing = False + + def isShowing(self) -> bool: + return self._is_showing + + def setEnabled(self, enabled: bool): + super().setEnabled(enabled) + + def setOnPrepareListener(self, listener: Optional[Callable]): + self._set_up_listener = listener + + class MediaPlayerControl: + def start(self): + pass + def pause(self): + pass + def getDuration(self) -> int: + return 0 + def getCurrentPosition(self) -> int: + return 0 + def seekTo(self, pos: int): + pass + def isPlaying(self) -> bool: + return False + def getBufferPercentage(self) -> int: + return 0 + def canPause(self) -> bool: + return False + def canSeekBackward(self) -> bool: + return False + def canSeekForward(self) -> bool: + return False + def getAudioSessionId(self) -> int: + return 0 + + +# ═══════════════════════════════════════════════════════════════ +# Toast +# ═══════════════════════════════════════════════════════════════ + +class Toast(Stub): + """Toast — 提示消息""" + + LENGTH_SHORT = 0 + LENGTH_LONG = 1 + + def __init__(self, context=None): + super().__init__() + if context is None: + context = android_context + self._context = context + self._duration = Toast.LENGTH_SHORT + self._gravity = Gravity.NO_GRAVITY + self._x_offset: int = 0 + self._y_offset: int = 0 + self._view: Optional[View] = None + self._text: str = "" + self._horizontal_margin: float = 0.0 + self._vertical_margin: float = 0.0 + self._next_view: Optional[View] = None + + @staticmethod + def makeText(context: Any, text: str, duration: int) -> "Toast": + toast = Toast(context) + toast._text = text + toast._duration = duration + return toast + + def show(self): + pass + + def cancel(self): + pass + + def setGravity(self, gravity: int, x_offset: int, y_offset: int): + self._gravity = gravity + self._x_offset = x_offset + self._y_offset = y_offset + + def setDuration(self, duration: int): + self._duration = duration + + def getDuration(self) -> int: + return self._duration + + def setView(self, view: View): + self._view = view + + def getView(self) -> Optional[View]: + return self._view + + def setMargin(self, horizontal_margin: float, vertical_margin: float): + self._horizontal_margin = horizontal_margin + self._vertical_margin = vertical_margin + + def getHorizontalMargin(self) -> float: + return self._horizontal_margin + + def getVerticalMargin(self) -> float: + return self._vertical_margin + + def setText(self, text: str): + self._text = text + + def setNextAnnouncementView(self, view: View): + self._next_view = view + + def getNextAnnouncementView(self) -> Optional[View]: + return self._next_view + + def isTouchable(self) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# PopupMenu / PopupWindow +# ═══════════════════════════════════════════════════════════════ + +class PopupMenu(Stub): + """PopupMenu — 弹出菜单""" + + def __init__(self, context: Any, anchor: View, gravity: int = Gravity.NO_GRAVITY): + super().__init__() + self._context = context + self._anchor = anchor + self._gravity = gravity + self._menu: "PopupMenu.Menu" = PopupMenu.Menu() + self._on_menu_item_click_listener: Optional[Callable] = None + self._on_dismiss_listener: Optional[Callable] = None + + class Menu(Stub): + """PopupMenu.Menu — 弹出菜单项列表""" + + def __init__(self): + super().__init__() + self._items: List[Dict] = [] + + def add(self, title: str) -> Any: + item_id = len(self._items) + self._items.append({"id": item_id, "title": title, "enabled": True}) + return item_id + + def findItem(self, id_: int) -> Optional[Dict]: + for item in self._items: + if item["id"] == id_: + return item + return None + + def removeItem(self, id_: int): + for i, item in enumerate(self._items): + if item["id"] == id_: + self._items.pop(i) + break + + def size(self) -> int: + return len(self._items) + + def getItem(self, index: int) -> Optional[Dict]: + if 0 <= index < len(self._items): + return self._items[index] + return None + + def clear(self): + self._items.clear() + + def setOnMenuItemClickListener(self, listener: Optional[Callable]): + pass + + def getMenu(self) -> "PopupMenu.Menu": + return self._menu + + def inflate(self, menu_res_id: int): + pass + + def show(self): + pass + + def dismiss(self): + pass + + def setOnMenuItemClickListener(self, listener: Optional[Callable]): + self._on_menu_item_click_listener = listener + + def setOnDismissListener(self, listener: Optional[Callable]): + self._on_dismiss_listener = listener + + def getDragToOpenListener(self) -> Optional[Callable]: + return None + + def setForceShowIcon(self, force_show: bool): + pass + + +class PopupWindow(Stub): + """PopupWindow — 弹出窗口""" + + def __init__(self, content_view: Optional[View] = None, + width: int = ViewGroup.LayoutParams.WRAP_CONTENT, + height: int = ViewGroup.LayoutParams.WRAP_CONTENT, + focusable: bool = True): + super().__init__() + self._content_view = content_view + self._width = width + self._height = height + self._focusable = focusable + self._is_showing: bool = False + self._outside_touchable: bool = False + self._touchable: bool = True + self._background_drawable: Any = None + self._animation_style: int = -1 + self._elevation: float = 0.0 + self._input_method_mode: int = -1 + self._soft_input_mode: int = -1 + self._on_dismiss_listener: Optional[Callable] = None + self._window_layout_type: int = 1000 # TYPE_APPLICATION_PANEL + self._overlap_anchor: bool = True + self._clip_to_screen: bool = True + self._split_enabled: bool = False + self._width_mode: int = 0 + self._height_mode: int = 0 + + def showAsDropDown(self, anchor: View, x_off: int = 0, y_off: int = 0, + gravity: int = Gravity.NO_GRAVITY): + self._is_showing = True + + def showAtLocation(self, parent: View, gravity: int, x: int, y: int): + self._is_showing = True + + def dismiss(self): + self._is_showing = False + if self._on_dismiss_listener: + self._on_dismiss_listener() + + def isShowing(self) -> bool: + return self._is_showing + + def setContentView(self, content_view: View): + self._content_view = content_view + + def getContentView(self) -> Optional[View]: + return self._content_view + + def setWidth(self, width: int): + self._width = width + + def getWidth(self) -> int: + return self._width + + def setHeight(self, height: int): + self._height = height + + def getHeight(self) -> int: + return self._height + + def setFocusable(self, focusable: bool): + self._focusable = focusable + + def isFocusable(self) -> bool: + return self._focusable + + def setOutsideTouchable(self, touchable: bool): + self._outside_touchable = touchable + + def isOutsideTouchable(self) -> bool: + return self._outside_touchable + + def setTouchable(self, touchable: bool): + self._touchable = touchable + + def isTouchable(self) -> bool: + return self._touchable + + def setBackgroundDrawable(self, background: Any): + self._background_drawable = background + + def getBackground(self) -> Any: + return self._background_drawable + + def setAnimationStyle(self, anim_style: int): + self._animation_style = anim_style + + def getAnimationStyle(self) -> int: + return self._animation_style + + def setElevation(self, elevation: float): + self._elevation = elevation + + def getElevation(self) -> float: + return self._elevation + + def setOnDismissListener(self, listener: Optional[Callable]): + self._on_dismiss_listener = listener + + def setTouchInterceptor(self, interceptor: Optional[Callable]): + pass + + def setClippingEnabled(self, enabled: bool): + self._clip_to_screen = enabled + + def isClippingEnabled(self) -> bool: + return self._clip_to_screen + + def setSplitTouchEnabled(self, enabled: bool): + self._split_enabled = enabled + + def isSplitTouchEnabled(self) -> bool: + return self._split_enabled + + def setWindowLayoutType(self, layout_type: int): + self._window_layout_type = layout_type + + def getWindowLayoutType(self) -> int: + return self._window_layout_type + + def setSoftInputMode(self, mode: int): + self._soft_input_mode = mode + + def getSoftInputMode(self) -> int: + return self._soft_input_mode + + def setInputMethodMode(self, mode: int): + self._input_method_mode = mode + + def getInputMethodMode(self) -> int: + return self._input_method_mode + + INPUT_METHOD_FROM_FOCUSABLE = 0 + INPUT_METHOD_NEEDED = 1 + INPUT_METHOD_NOT_NEEDED = 2 + + +# ═══════════════════════════════════════════════════════════════ +# Toolbar +# ═══════════════════════════════════════════════════════════════ + +class Toolbar(ViewGroup): + """Toolbar — 工具栏""" + + def __init__(self, context=None): + super().__init__(context) + self._title: str = "" + self._subtitle: str = "" + self._navigation_icon: Any = None + self._navigation_content_description: str = "" + self._logo: Any = None + self._logo_description: str = "" + self._title_text_color: int = 0 + self._subtitle_text_color: int = 0 + self._collapse_icon: Any = None + self._menu: Any = None + self._on_menu_item_click_listener: Optional[Callable] = None + self._navigation_on_click_listener: Optional[Callable] = None + self._popup_theme: int = 0 + self._content_inset_start: int = 16 + self._content_inset_end: int = 16 + self._title_margin_start: int = 0 + + def setTitle(self, title: str): + self._title = title + + def getTitle(self) -> str: + return self._title + + def setSubtitle(self, subtitle: str): + self._subtitle = subtitle + + def getSubtitle(self) -> str: + return self._subtitle + + def setNavigationIcon(self, icon: Any): + self._navigation_icon = icon + + def getNavigationIcon(self) -> Any: + return self._navigation_icon + + def setNavigationContentDescription(self, description: str): + self._navigation_content_description = description + + def getNavigationContentDescription(self) -> str: + return self._navigation_content_description + + def setLogo(self, logo: Any): + self._logo = logo + + def getLogo(self) -> Any: + return self._logo + + def setLogoDescription(self, description: str): + self._logo_description = description + + def getLogoDescription(self) -> str: + return self._logo_description + + def setTitleTextColor(self, color: int): + self._title_text_color = color + + def setSubtitleTextColor(self, color: int): + self._subtitle_text_color = color + + def inflateMenu(self, menu_res_id: int): + pass + + def getMenu(self) -> Any: + return self._menu + + def setOnMenuItemClickListener(self, listener: Optional[Callable]): + self._on_menu_item_click_listener = listener + + def setNavigationOnClickListener(self, listener: Optional[Callable]): + self._navigation_on_click_listener = listener + + def setCollapseIcon(self, icon: Any): + self._collapse_icon = icon + + def getCollapseIcon(self) -> Any: + return self._collapse_icon + + def setPopupTheme(self, res_id: int): + self._popup_theme = res_id + + def getPopupTheme(self) -> int: + return self._popup_theme + + def setContentInsetsRelative(self, content_inset_start: int, content_inset_end: int): + self._content_inset_start = content_inset_start + self._content_inset_end = content_inset_end + + def getContentInsetStart(self) -> int: + return self._content_inset_start + + def getContentInsetEnd(self) -> int: + return self._content_inset_end + + def collapseActionView(self): + pass + + def dismissPopupMenus(self): + pass + + def isOverflowMenuShowing(self) -> bool: + return False + + def showOverflowMenu(self) -> bool: + return False + + def hideOverflowMenu(self) -> bool: + return False + + def getOverflowIcon(self) -> Any: + return None + + def setOverflowIcon(self, icon: Any): + pass + + +class ActionMenuView(LinearLayout): + """ActionMenuView — 操作菜单视图""" + pass + + +# ═══════════════════════════════════════════════════════════════ +# TabHost / TabWidget +# ═══════════════════════════════════════════════════════════════ + +class TabHost(FrameLayout): + """TabHost — 标签主机""" + + def __init__(self, context=None): + super().__init__(context) + self._tab_widget: Optional["TabWidget"] = None + self._tab_content: Optional[FrameLayout] = None + self._tabs: List["TabHost.TabSpec"] = [] + self._current_tab: int = 0 + self._on_tab_changed_listener: Optional[Callable] = None + self._local_activity_manager: Any = None + + class TabSpec(Stub): + """TabHost.TabSpec — 标签规格""" + + def __init__(self, tag: str): + super().__init__() + self._tag = tag + self._indicator: Any = None + self._content: Any = None + self._content_view: Optional[View] = None + + def setIndicator(self, indicator: Any) -> "TabHost.TabSpec": + self._indicator = indicator + return self + + def setContent(self, content: Any) -> "TabHost.TabSpec": + self._content = content + return self + + def setContent(self, view_id: int) -> "TabHost.TabSpec": + return self + + def setContent(self, view: View) -> "TabHost.TabSpec": + self._content_view = view + return self + + def getTag(self) -> str: + return self._tag + + def getIndicator(self) -> Any: + return self._indicator + + class OnTabChangeListener: + def onTabChanged(self, tab_id: str): + pass + + def setup(self): + pass + + def newTabSpec(self, tag: str) -> "TabHost.TabSpec": + spec = TabHost.TabSpec(tag) + self._tabs.append(spec) + return spec + + def addTab(self, tab_spec: "TabHost.TabSpec"): + if tab_spec not in self._tabs: + self._tabs.append(tab_spec) + + def setCurrentTab(self, index: int): + if 0 <= index < len(self._tabs): + self._current_tab = index + + def setCurrentTabByTag(self, tag: str): + for i, tab in enumerate(self._tabs): + if tab.getTag() == tag: + self._current_tab = i + break + + def getCurrentTab(self) -> int: + return self._current_tab + + def getCurrentTabTag(self) -> str: + if 0 <= self._current_tab < len(self._tabs): + return self._tabs[self._current_tab].getTag() + return "" + + def getTabWidget(self) -> "TabWidget": + return self._tab_widget + + def getTabContentView(self) -> FrameLayout: + return self._tab_content + + def setOnTabChangedListener(self, listener: Optional[OnTabChangeListener]): + self._on_tab_changed_listener = listener + + def clearAllTabs(self): + self._tabs.clear() + self._current_tab = 0 + + +class TabWidget(LinearLayout): + """TabWidget — 标签部件""" + + def __init__(self, context=None): + super().__init__(context) + self._strip: Any = None + self._left_strip: Any = None + self._right_strip: Any = None + self._selected_tab: int = -1 + self._draw_full_strip: bool = False + + def setStripEnabled(self, strip_enabled: bool): + self._draw_full_strip = strip_enabled + + def isStripEnabled(self) -> bool: + return self._draw_full_strip + + def setLeftStripDrawable(self, drawable: Any): + self._left_strip = drawable + + def setRightStripDrawable(self, drawable: Any): + self._right_strip = drawable + + def setDividerDrawable(self, drawable: Any): + pass + + def focusCurrentTab(self, index: int): + self._selected_tab = index + + def isEnabled(self) -> bool: + return True + + +# ═══════════════════════════════════════════════════════════════ +# ExpandableListView +# ═══════════════════════════════════════════════════════════════ + +class ExpandableListView(ListView): + """ExpandableListView — 可展开列表视图""" + + PACKED_POSITION_TYPE_GROUP = 0 + PACKED_POSITION_TYPE_CHILD = 1 + PACKED_POSITION_TYPE_NULL = 2 + PACKED_POSITION_VALUE_NULL = 0xFFFFFFFF + + def __init__(self, context=None): + super().__init__(context) + self._expandable_adapter: Optional["ExpandableListAdapter"] = None + self._on_group_collapse_listener: Optional[Callable] = None + self._on_group_expand_listener: Optional[Callable] = None + self._on_child_click_listener: Optional[Callable] = None + self._on_group_click_listener: Optional[Callable] = None + + def setAdapter(self, adapter: "ExpandableListAdapter"): + self._expandable_adapter = adapter + self._adapter = adapter + + def getExpandableListAdapter(self) -> Optional["ExpandableListAdapter"]: + return self._expandable_adapter + + def setOnGroupCollapseListener(self, listener: Optional[Callable]): + self._on_group_collapse_listener = listener + + def setOnGroupExpandListener(self, listener: Optional[Callable]): + self._on_group_expand_listener = listener + + def setOnChildClickListener(self, listener: Optional[Callable]): + self._on_child_click_listener = listener + + def setOnGroupClickListener(self, listener: Optional[Callable]): + self._on_group_click_listener = listener + + def expandGroup(self, group_pos: int, animate: bool = True) -> bool: + return True + + def collapseGroup(self, group_pos: int) -> bool: + return True + + def setOnItemClickListener(self, listener: Optional[Callable]): + self._on_item_click_listener = listener + + def getOnItemClickListener(self) -> Optional[Callable]: + return self._on_item_click_listener + + def setOnItemLongClickListener(self, listener: Optional[Callable]): + self._on_item_long_click_listener = listener + + def getOnItemLongClickListener(self) -> Optional[Callable]: + return self._on_item_long_click_listener + + def getPackedPositionType(self, packed_position: int) -> int: + if packed_position == ExpandableListView.PACKED_POSITION_VALUE_NULL: + return ExpandableListView.PACKED_POSITION_TYPE_NULL + return ExpandableListView.PACKED_POSITION_TYPE_GROUP + + def getPackedPositionGroup(self, packed_position: int) -> int: + return packed_position & 0xFFFF + + def getPackedPositionChild(self, packed_position: int) -> int: + return (packed_position >> 16) & 0xFFFF + + @staticmethod + def getPackedPositionForChild(group_position: int, child_position: int) -> int: + return (child_position << 16) | group_position + + @staticmethod + def getPackedPositionForGroup(group_position: int) -> int: + return group_position + + def isGroupExpanded(self, group_position: int) -> bool: + return False + + def setSelectedGroup(self, group_position: int): + pass + + def setSelectedChild(self, group_position: int, child_position: int, should_expand_group: bool): + pass + + def smoothScrollToPosition(self, position: int): + pass + + +# ═══════════════════════════════════════════════════════════════ +# ExpandableListAdapter +# ═══════════════════════════════════════════════════════════════ + +class ExpandableListAdapter(Stub): + """ExpandableListAdapter — 可扩展列表适配器接口""" + + def __init__(self): + super().__init__() + + def getGroupCount(self) -> int: + raise AndroidOnlyError("ExpandableListAdapter.getGroupCount: not implemented") + + def getChildrenCount(self, group_position: int) -> int: + raise AndroidOnlyError("ExpandableListAdapter.getChildrenCount: not implemented") + + def getGroup(self, group_position: int) -> Any: + raise AndroidOnlyError("ExpandableListAdapter.getGroup: not implemented") + + def getChild(self, group_position: int, child_position: int) -> Any: + raise AndroidOnlyError("ExpandableListAdapter.getChild: not implemented") + + def getGroupId(self, group_position: int) -> int: + raise AndroidOnlyError("ExpandableListAdapter.getGroupId: not implemented") + + def getChildId(self, group_position: int, child_position: int) -> int: + raise AndroidOnlyError("ExpandableListAdapter.getChildId: not implemented") + + def hasStableIds(self) -> bool: + return False + + def getGroupView(self, group_position: int, is_expanded: bool, + convert_view: Optional[Any], parent: Optional[Any]) -> Any: + raise AndroidOnlyError("ExpandableListAdapter.getGroupView: not implemented") + + def getChildView(self, group_position: int, child_position: int, + is_last_child: bool, convert_view: Optional[Any], + parent: Optional[Any]) -> Any: + raise AndroidOnlyError("ExpandableListAdapter.getChildView: not implemented") + + def isChildSelectable(self, group_position: int, child_position: int) -> bool: + return True + + def areAllItemsEnabled(self) -> bool: + return True + + def isEmpty(self) -> bool: + return self.getGroupCount() == 0 + + def onGroupCollapsed(self, group_position: int): + pass + + def onGroupExpanded(self, group_position: int): + pass + + def getCombinedGroupId(self, group_id: int) -> int: + return group_id + + def getCombinedChildId(self, group_id: int, child_id: int) -> int: + return (group_id << 32) | child_id + + +# ═══════════════════════════════════════════════════════════════ +# SimpleExpandableListAdapter +# ═══════════════════════════════════════════════════════════════ + +class SimpleExpandableListAdapter(ExpandableListAdapter, BaseAdapter): + """SimpleExpandableListAdapter — 简单的可展开列表适配器""" + + def __init__(self, context: Any, + group_data: List[Dict[str, Any]], + group_layout: int, + group_from: List[str], + group_to: List[int], + child_data: List[List[Dict[str, Any]]], + child_layout: int, + child_from: List[str], + child_to: List[int]): + super().__init__() + self._context = context + self._group_data = group_data + self._group_layout = group_layout + self._group_from = group_from + self._group_to = group_to + self._child_data = child_data + self._child_layout = child_layout + self._child_from = child_from + self._child_to = child_to + + def getGroupCount(self) -> int: + return len(self._group_data) + + def getChildrenCount(self, group_position: int) -> int: + if 0 <= group_position < len(self._child_data): + return len(self._child_data[group_position]) + return 0 + + def getGroup(self, group_position: int) -> Any: + if 0 <= group_position < len(self._group_data): + return self._group_data[group_position] + return None + + def getChild(self, group_position: int, child_position: int) -> Any: + if 0 <= group_position < len(self._child_data): + children = self._child_data[group_position] + if 0 <= child_position < len(children): + return children[child_position] + return None + + def getGroupId(self, group_position: int) -> int: + return group_position + + def getChildId(self, group_position: int, child_position: int) -> int: + return child_position + + def getGroupView(self, group_position: int, is_expanded: bool, + convert_view: Optional[Any], parent: Optional[Any]) -> Any: + if convert_view is not None: + view = convert_view + else: + view = TextView(self._context) + view.setText(str(self.getGroup(group_position))) + return view + + def getChildView(self, group_position: int, child_position: int, + is_last_child: bool, convert_view: Optional[Any], + parent: Optional[Any]) -> Any: + if convert_view is not None: + view = convert_view + else: + view = TextView(self._context) + child = self.getChild(group_position, child_position) + view.setText(str(child) if child else "") + return view + + def isChildSelectable(self, group_position: int, child_position: int) -> bool: + return True + + def getCount(self) -> int: + return self.getGroupCount() + + def getItem(self, position: int) -> Any: + return self.getGroup(position) + + def getItemId(self, position: int) -> int: + return position + + def getView(self, position: int, convert_view: Optional[Any], + parent: Optional[Any]) -> Any: + return self.getGroupView(position, False, convert_view, parent) + + +# ═══════════════════════════════════════════════════════════════ +# BaseExpandableListAdapter +# ═══════════════════════════════════════════════════════════════ + +class BaseExpandableListAdapter(ExpandableListAdapter, BaseAdapter): + """BaseExpandableListAdapter — 可展开列表适配器基类""" + + def __init__(self): + super().__init__() + + def getGroupCount(self) -> int: + return 0 + + def getChildrenCount(self, group_position: int) -> int: + return 0 + + def getGroup(self, group_position: int) -> Any: + return None + + def getChild(self, group_position: int, child_position: int) -> Any: + return None + + def getGroupId(self, group_position: int) -> int: + return group_position + + def getChildId(self, group_position: int, child_position: int) -> int: + return child_position + + def getGroupView(self, group_position: int, is_expanded: bool, + convert_view: Optional[Any], parent: Optional[Any]) -> Any: + if convert_view is not None: + view = convert_view + else: + view = TextView(android_context) + view.setText(f"Group {group_position}") + return view + + def getChildView(self, group_position: int, child_position: int, + is_last_child: bool, convert_view: Optional[Any], + parent: Optional[Any]) -> Any: + if convert_view is not None: + view = convert_view + else: + view = TextView(android_context) + view.setText(f"Child {group_position}:{child_position}") + return view + + def isChildSelectable(self, group_position: int, child_position: int) -> bool: + return True + + def areAllItemsEnabled(self) -> bool: + return True + + def onGroupCollapsed(self, group_position: int): + pass + + def onGroupExpanded(self, group_position: int): + pass + + def notifyDataSetChanged(self): + pass + + def notifyDataSetInvalidated(self): + pass + + def getCount(self) -> int: + return self.getGroupCount() + + def getItem(self, position: int) -> Any: + return self.getGroup(position) + + def getItemId(self, position: int) -> int: + return position + + def getView(self, position: int, convert_view: Optional[Any], + parent: Optional[Any]) -> Any: + return self.getGroupView(position, False, convert_view, parent) + + +# ═══════════════════════════════════════════════════════════════ +# SearchView +# ═══════════════════════════════════════════════════════════════ + +class SearchView(LinearLayout): + """SearchView — 搜索视图""" + + def __init__(self, context=None): + super().__init__(context) + self._query: str = "" + self._query_hint: Optional[str] = None + self._iconified: bool = True + self._iconified_by_default: bool = True + self._submit_button_enabled: bool = False + self._query_refinement_enabled: bool = False + self._searchable_info: Any = None + self._on_query_text_listener: Optional[Callable] = None + self._on_close_listener: Optional[Callable] = None + self._on_suggestion_listener: Optional[Callable] = None + self._on_query_text_focus_change_listener: Optional[Callable] = None + self._suggestion_adapter: Optional["CursorAdapter"] = None + self._max_width: int = sys.maxsize + self._on_search_click_listener: Optional[Callable] = None + + class OnQueryTextListener: + def onQueryTextSubmit(self, query: str) -> bool: + return False + + def onQueryTextChange(self, new_text: str) -> bool: + return False + + class OnCloseListener: + def onClose(self) -> bool: + return False + + class OnSuggestionListener: + def onSuggestionSelect(self, position: int) -> bool: + return False + + def onSuggestionClick(self, position: int) -> bool: + return False + + def setQuery(self, query: str, submit: bool = False): + self._query = query + if self._on_query_text_listener: + self._on_query_text_listener.onQueryTextChange(query) + if submit and self._on_query_text_listener: + self._on_query_text_listener.onQueryTextSubmit(query) + + def getQuery(self) -> str: + return self._query + + def setQueryHint(self, hint: Optional[str]): + self._query_hint = hint + + def getQueryHint(self) -> Optional[str]: + return self._query_hint + + def setIconified(self, iconified: bool): + self._iconified = iconified + + def isIconified(self) -> bool: + return self._iconified + + def setIconifiedByDefault(self, iconified: bool): + self._iconified_by_default = iconified + + def isIconifiedByDefault(self) -> bool: + return self._iconified_by_default + + def setSubmitButtonEnabled(self, enabled: bool): + self._submit_button_enabled = enabled + + def isSubmitButtonEnabled(self) -> bool: + return self._submit_button_enabled + + def setQueryRefinementEnabled(self, enable: bool): + self._query_refinement_enabled = enable + + def isQueryRefinementEnabled(self) -> bool: + return self._query_refinement_enabled + + def setOnQueryTextListener(self, listener: Optional["SearchView.OnQueryTextListener"]): + self._on_query_text_listener = listener + + def getOnQueryTextListener(self) -> Optional["SearchView.OnQueryTextListener"]: + return self._on_query_text_listener + + def setOnCloseListener(self, listener: Optional["SearchView.OnCloseListener"]): + self._on_close_listener = listener + + def setOnSuggestionListener(self, listener: Optional["SearchView.OnSuggestionListener"]): + self._on_suggestion_listener = listener + + def setOnSearchClickListener(self, listener: Optional[Callable]): + self._on_search_click_listener = listener + + def setSuggestionsAdapter(self, adapter: Optional["CursorAdapter"]): + self._suggestion_adapter = adapter + + def getSuggestionsAdapter(self) -> Optional["CursorAdapter"]: + return self._suggestion_adapter + + def setMaxWidth(self, max_width: int): + self._max_width = max_width + + def getMaxWidth(self) -> int: + return self._max_width + + def onActionViewExpanded(self): + self._iconified = False + + def onActionViewCollapsed(self): + self._iconified = True + + def onClose(self) -> bool: + if self._on_close_listener: + return self._on_close_listener.onClose() + self._iconified = self._iconified_by_default + return True + + def clearFocus(self): + pass + + +# ═══════════════════════════════════════════════════════════════ +# ZoomControls +# ═══════════════════════════════════════════════════════════════ + +class ZoomControls(LinearLayout): + """ZoomControls — 缩放控件""" + + def __init__(self, context=None): + super().__init__(context) + self._zoom_in_enabled: bool = True + self._zoom_out_enabled: bool = True + self._zoom_speed: int = 300 + self._on_zoom_in_listener: Optional[Callable] = None + self._on_zoom_out_listener: Optional[Callable] = None + + def show(self): + self.setVisibility(View.VISIBLE) + + def hide(self): + self.setVisibility(View.GONE) + + def setIsZoomInEnabled(self, is_enabled: bool): + self._zoom_in_enabled = is_enabled + + def setIsZoomOutEnabled(self, is_enabled: bool): + self._zoom_out_enabled = is_enabled + + def setOnZoomInClickListener(self, listener: Optional[Callable]): + self._on_zoom_in_listener = listener + + def setOnZoomOutClickListener(self, listener: Optional[Callable]): + self._on_zoom_out_listener = listener + + def setZoomSpeed(self, speed: int): + self._zoom_speed = speed + + def getZoomSpeed(self) -> int: + return self._zoom_speed + + def hasFocus(self) -> bool: + return False + + +# ═══════════════════════════════════════════════════════════════ +# DigitalClock +# ═══════════════════════════════════════════════════════════════ + +class DigitalClock(TextView): + """DigitalClock — 数字时钟(API 14+ 已弃用)""" + + def __init__(self, context=None): + super().__init__(context) + self._format: str = "%H:%M:%S" + self._update_interval: int = 1000 + self._running: bool = False + self._timer: Optional[Any] = None + + def start(self): + self._running = True + + def stop(self): + self._running = False + + def setFormat(self, fmt: str): + self._format = fmt + + def getFormat(self) -> str: + return self._format + + def onTimeChanged(self): + now = datetime.now().strftime(self._format) if hasattr(datetime, 'now') else "00:00:00" + self.setText(now) + + +# ═══════════════════════════════════════════════════════════════ +# AnalogClock +# ═══════════════════════════════════════════════════════════════ + +class AnalogClock(View): + """AnalogClock — 模拟时钟(API 14+ 已弃用)""" + + def __init__(self, context=None): + super().__init__(context) + self._dial: Optional[Any] = None + self._hand_hour: Optional[Any] = None + self._hand_minute: Optional[Any] = None + self._running: bool = False + self._update_interval: int = 1000 + + def start(self): + self._running = True + self.invalidate() + + def stop(self): + self._running = False + + def onDraw(self, canvas: Any): + super().onDraw(canvas) + + def setDialDrawable(self, drawable: Any): + self._dial = drawable + + def setHourDrawable(self, drawable: Any): + self._hand_hour = drawable + + def setMinuteDrawable(self, drawable: Any): + self._hand_minute = drawable + + +# ═══════════════════════════════════════════════════════════════ +# Chronometer +# ═══════════════════════════════════════════════════════════════ + +class Chronometer(TextView): + """Chronometer — 计时器""" + + def __init__(self, context=None): + super().__init__(context) + self._base: int = 0 + self._format: Optional[str] = None + self._running: bool = False + self._visible: bool = True + self._on_chronometer_tick_listener: Optional[Callable] = None + self._count_down: bool = False + self._elapsed: float = 0.0 + + def start(self): + self._running = True + + def stop(self): + self._running = False + + def setBase(self, base: int): + self._base = base + + def getBase(self) -> int: + return self._base + + def setFormat(self, fmt: str): + self._format = fmt + + def getFormat(self) -> Optional[str]: + return self._format + + def setOnChronometerTickListener(self, listener: Optional[Callable]): + self._on_chronometer_tick_listener = listener + + def getOnChronometerTickListener(self) -> Optional[Callable]: + return self._on_chronometer_tick_listener + + def isCountDown(self) -> bool: + return self._count_down + + def setCountDown(self, count_down: bool): + self._count_down = count_down + + def getElapsedRealtime(self) -> float: + return self._elapsed + + def setVisibility(self, visibility: int): + self._visible = (visibility == View.VISIBLE) + super().setVisibility(visibility) + + def onDraw(self, canvas: Any): + super().onDraw(canvas) + if self._running and self._on_chronometer_tick_listener: + try: + self._on_chronometer_tick_listener(self) + except Exception: + pass + + +# ═══════════════════════════════════════════════════════════════ +# __all__ +# ═══════════════════════════════════════════════════════════════ + +__all__ = [ + # TextView hierarchy + "TextView", "EditText", "Button", "ImageButton", "CheckBox", + "RadioButton", "ToggleButton", "Switch", + # ImageView + "ImageView", + # Progress + "ProgressBar", "SeekBar", "RatingBar", + # AdapterView hierarchy + "AdapterView", "ListView", "GridView", "ExpandableListView", + "Spinner", "AutoCompleteTextView", "MultiAutoCompleteTextView", + # RecyclerView + "RecyclerView", "RecyclerViewAdapter", "RecyclerViewViewHolder", + "RecyclerViewLayoutManager", "LinearLayoutManager", + "GridLayoutManager", "StaggeredGridLayoutManager", + # Layouts + "LinearLayout", "RelativeLayout", "FrameLayout", + "TableLayout", "TableRow", + # Scrolling + "ScrollView", "HorizontalScrollView", + # WebView + "WebView", "WebSettings", + # Adapters + "BaseAdapter", "ArrayAdapter", "SimpleAdapter", "CursorAdapter", + "ExpandableListAdapter", "SimpleExpandableListAdapter", + "BaseExpandableListAdapter", + # ViewAnimator + "ViewAnimator", "ViewFlipper", "ViewSwitcher", + "ImageSwitcher", "TextSwitcher", + "AdapterViewAnimator", "StackView", "AdapterViewFlipper", + # Date/Time + "DatePicker", "TimePicker", "CalendarView", "NumberPicker", + "DigitalClock", "AnalogClock", "Chronometer", + # Media + "VideoView", "MediaController", + # Toast/Popup + "Toast", "PopupMenu", "PopupWindow", + # Toolbar + "Toolbar", "ActionMenuView", + # Tabs + "TabHost", "TabWidget", + # Search + "SearchView", + # Zoom + "ZoomControls", +] \ No newline at end of file diff --git a/android_pyapi/java/__init__.py b/android_pyapi/java/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/android_pyapi/java/__pycache__/__init__.cpython-310.pyc b/android_pyapi/java/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1a1a495 Binary files /dev/null and b/android_pyapi/java/__pycache__/__init__.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/crypto.cpython-310.pyc b/android_pyapi/java/__pycache__/crypto.cpython-310.pyc new file mode 100644 index 0000000..228599e Binary files /dev/null and b/android_pyapi/java/__pycache__/crypto.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/io.cpython-310.pyc b/android_pyapi/java/__pycache__/io.cpython-310.pyc new file mode 100644 index 0000000..902ec88 Binary files /dev/null and b/android_pyapi/java/__pycache__/io.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/lang.cpython-310.pyc b/android_pyapi/java/__pycache__/lang.cpython-310.pyc new file mode 100644 index 0000000..18e3dd7 Binary files /dev/null and b/android_pyapi/java/__pycache__/lang.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/math.cpython-310.pyc b/android_pyapi/java/__pycache__/math.cpython-310.pyc new file mode 100644 index 0000000..5233e35 Binary files /dev/null and b/android_pyapi/java/__pycache__/math.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/net.cpython-310.pyc b/android_pyapi/java/__pycache__/net.cpython-310.pyc new file mode 100644 index 0000000..32d63eb Binary files /dev/null and b/android_pyapi/java/__pycache__/net.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/nio.cpython-310.pyc b/android_pyapi/java/__pycache__/nio.cpython-310.pyc new file mode 100644 index 0000000..d8dbd65 Binary files /dev/null and b/android_pyapi/java/__pycache__/nio.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/nio_file.cpython-310.pyc b/android_pyapi/java/__pycache__/nio_file.cpython-310.pyc new file mode 100644 index 0000000..0b75e61 Binary files /dev/null and b/android_pyapi/java/__pycache__/nio_file.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/security.cpython-310.pyc b/android_pyapi/java/__pycache__/security.cpython-310.pyc new file mode 100644 index 0000000..83345d4 Binary files /dev/null and b/android_pyapi/java/__pycache__/security.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/text.cpython-310.pyc b/android_pyapi/java/__pycache__/text.cpython-310.pyc new file mode 100644 index 0000000..d4d9266 Binary files /dev/null and b/android_pyapi/java/__pycache__/text.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/time.cpython-310.pyc b/android_pyapi/java/__pycache__/time.cpython-310.pyc new file mode 100644 index 0000000..075b376 Binary files /dev/null and b/android_pyapi/java/__pycache__/time.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/util.cpython-310.pyc b/android_pyapi/java/__pycache__/util.cpython-310.pyc new file mode 100644 index 0000000..1297792 Binary files /dev/null and b/android_pyapi/java/__pycache__/util.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/util_concurrent.cpython-310.pyc b/android_pyapi/java/__pycache__/util_concurrent.cpython-310.pyc new file mode 100644 index 0000000..c37022e Binary files /dev/null and b/android_pyapi/java/__pycache__/util_concurrent.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/util_regex.cpython-310.pyc b/android_pyapi/java/__pycache__/util_regex.cpython-310.pyc new file mode 100644 index 0000000..8f0f3c1 Binary files /dev/null and b/android_pyapi/java/__pycache__/util_regex.cpython-310.pyc differ diff --git a/android_pyapi/java/__pycache__/util_zip.cpython-310.pyc b/android_pyapi/java/__pycache__/util_zip.cpython-310.pyc new file mode 100644 index 0000000..cce469b Binary files /dev/null and b/android_pyapi/java/__pycache__/util_zip.cpython-310.pyc differ diff --git a/android_pyapi/java/crypto.py b/android_pyapi/java/crypto.py new file mode 100644 index 0000000..fe80f49 --- /dev/null +++ b/android_pyapi/java/crypto.py @@ -0,0 +1,628 @@ +""" +Java crypto standard library APIs implemented in Python. +javax.crypto package - Cipher, KeyGenerator, Mac, SecretKey, etc. +""" + +from .._core import Stub, AndroidOnlyError +from .lang import RuntimeException +import os as _os + +# Try to import cryptography library +try: + from cryptography.hazmat.primitives.ciphers import Cipher as _Cipher + from cryptography.hazmat.primitives.ciphers import algorithms as _algorithms + from cryptography.hazmat.primitives.ciphers import modes as _modes + from cryptography.hazmat.primitives import hashes as _hashes + from cryptography.hazmat.primitives.asymmetric import rsa as _rsa + from cryptography.hazmat.primitives.asymmetric import padding as _padding + from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC as _PBKDF2 + from cryptography.hazmat.backends import default_backend as _crypto_backend + HAS_CRYPTOGRAPHY = True +except ImportError: + HAS_CRYPTOGRAPHY = False + +from .security import ( + GeneralSecurityException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, + Key, SecretKeySpec, AlgorithmParameters, AlgorithmParametersSpi, + AlgorithmParameterGenerator, AlgorithmParameterGeneratorSpi, + Provider, SecureRandom, +) + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) +AEADBadTagException = type("AEADBadTagException", (GeneralSecurityException,), {}) +BadPaddingException = type("BadPaddingException", (GeneralSecurityException,), {}) +IllegalBlockSizeException = type("IllegalBlockSizeException", (GeneralSecurityException,), {}) +NoSuchPaddingException = type("NoSuchPaddingException", (GeneralSecurityException,), {}) +ShortBufferException = type("ShortBufferException", (GeneralSecurityException,), {}) +ExemptionMechanismException = type("ExemptionMechanismException", (GeneralSecurityException,), {}) + +__all__ = [ + "Cipher", "CipherSpi", + "KeyGenerator", "KeyGeneratorSpi", + "SecretKey", "SecretKeyFactory", "SecretKeyFactorySpi", + "Mac", "MacSpi", + "IvParameterSpec", "PBEParameterSpec", "GCMParameterSpec", + "OAEPParameterSpec", "PSource", "MGF1ParameterSpec", + "DHParameterSpec", "DSAParameterSpec", + "ECParameterSpec", "ECGenParameterSpec", "ECPoint", + "ECField", "ECFieldF2m", "ECFieldFp", "EllipticCurve", + "AlgorithmParameters", "AlgorithmParameterGenerator", + "AlgorithmParametersSpi", + "AEADBadTagException", "BadPaddingException", + "IllegalBlockSizeException", "NoSuchPaddingException", + "ShortBufferException", + "ExemptionMechanism", "ExemptionMechanismException", + "ExemptionMechanismSpi", +] + + +# ============================================================ +# SecretKey +# ============================================================ + +class SecretKey(Key): + def __init__(self, key_bytes=None, algorithm=""): + super().__init__() + self._key = key_bytes or b"" + self._algorithm = algorithm + + def getAlgorithm(self): + return self._algorithm + + def getFormat(self): + return "RAW" + + def getEncoded(self): + return self._key + + +# ============================================================ +# Cipher +# ============================================================ + +class Cipher(Stub): + ENCRYPT_MODE = 1 + DECRYPT_MODE = 2 + WRAP_MODE = 3 + UNWRAP_MODE = 4 + PUBLIC_KEY = 1 + PRIVATE_KEY = 2 + SECRET_KEY = 3 + + def __init__(self, transformation): + super().__init__() + self._transformation = transformation + self._opmode = 0 + self._key = None + self._iv = None + self._provider = Provider("AndroidOpenSSL", 1.0, "Android OpenSSL provider") + + @staticmethod + def getInstance(transformation, provider=None): + return Cipher(transformation) + + def init(self, opmode, key_or_cert, params=None, random=None): + self._opmode = opmode + self._key = key_or_cert + if isinstance(params, IvParameterSpec): + self._iv = params.getIV() + elif isinstance(params, GCMParameterSpec): + self._iv = params.getIV() + + def doFinal(self, input_data=None): + if input_data is None: + return b"" + if HAS_CRYPTOGRAPHY and self._key is not None: + try: + algo = self._transformation.split("/")[0].upper() + key_bytes = self._key.getEncoded() if hasattr(self._key, "getEncoded") else bytes(self._key) + iv = self._iv or b"\x00" * 16 + if algo == "AES": + if len(iv) == 12: + mode = _modes.GCM(iv) + else: + mode = _modes.CBC(iv) + cipher = _Cipher(_algorithms.AES(key_bytes), mode, backend=_crypto_backend()) + elif algo == "DES": + cipher = _Cipher(_algorithms.TripleDES(key_bytes), _modes.CBC(iv), backend=_crypto_backend()) + else: + return self._basic_cipher(input_data) + if self._opmode == Cipher.ENCRYPT_MODE: + encryptor = cipher.encryptor() + result = encryptor.update(input_data) + encryptor.finalize() + return result + else: + decryptor = cipher.decryptor() + result = decryptor.update(input_data) + decryptor.finalize() + return result + except Exception: + return self._basic_cipher(input_data) + else: + return self._basic_cipher(input_data) + + def _basic_cipher(self, data): + if self._opmode == Cipher.ENCRYPT_MODE: + if self._key is not None: + key_bytes = self._key.getEncoded() if hasattr(self._key, "getEncoded") else bytes(self._key) + return bytes([(b ^ key_bytes[i % len(key_bytes)]) & 0xFF for i, b in enumerate(data)]) + return data + else: + if self._key is not None: + key_bytes = self._key.getEncoded() if hasattr(self._key, "getEncoded") else bytes(self._key) + return bytes([(b ^ key_bytes[i % len(key_bytes)]) & 0xFF for i, b in enumerate(data)]) + return data + + def update(self, input_data): + return self._basic_cipher(input_data) + + def getBlockSize(self): + algo = self._transformation.split("/")[0].upper() + if algo == "AES": + return 16 + if algo == "DES" or algo == "DESede": + return 8 + return 1 + + def getOutputSize(self, input_len): + bs = self.getBlockSize() + if bs > 1: + return input_len + bs - (input_len % bs) + return input_len + + def getIV(self): + return self._iv + + def getParameters(self): + if self._iv: + return AlgorithmParameters(self._transformation.split("/")[0]) + return None + + def getAlgorithm(self): + return self._transformation + + def getProvider(self): + return self._provider + + def getExemptionMechanism(self): + return None + + def unwrap(self, wrapped_key, wrapped_key_algorithm, wrapped_key_type): + return SecretKey(wrapped_key, wrapped_key_algorithm) + + def wrap(self, key): + key_bytes = key.getEncoded() if hasattr(key, "getEncoded") else b"" + return self.doFinal(key_bytes) + + @staticmethod + def maxAllowedKeyLength(transformation): + return 2147483647 + + +class CipherSpi(Stub): + pass + + +# ============================================================ +# KeyGenerator +# ============================================================ + +class KeyGenerator(Stub): + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + self._key_size = 128 + self._secure_random = None + + @staticmethod + def getInstance(algorithm, provider=None): + return KeyGenerator(algorithm) + + def init(self, size_or_random, random=None): + if isinstance(size_or_random, int): + self._key_size = size_or_random + self._secure_random = random + else: + self._secure_random = size_or_random + + def generateKey(self): + key_bytes = _os.urandom(self._key_size // 8) + return SecretKey(key_bytes, self._algorithm) + + def getAlgorithm(self): + return self._algorithm + + def getProvider(self): + return Provider("AndroidOpenSSL", 1.0, "Android OpenSSL provider") + + +class KeyGeneratorSpi(Stub): + pass + + +# ============================================================ +# SecretKeyFactory +# ============================================================ + +class SecretKeyFactory(Stub): + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + + @staticmethod + def getInstance(algorithm, provider=None): + return SecretKeyFactory(algorithm) + + def generateSecret(self, key_spec): + if isinstance(key_spec, SecretKeySpec): + return SecretKey(key_spec.getEncoded(), self._algorithm) + return SecretKey(b"", self._algorithm) + + def getKeySpec(self, secret_key, key_spec_cls): + return SecretKeySpec(secret_key.getEncoded(), self._algorithm) + + def translateKey(self, key): + return key + + def getAlgorithm(self): + return self._algorithm + + +class SecretKeyFactorySpi(Stub): + pass + + +# ============================================================ +# Mac +# ============================================================ + +class Mac(Stub): + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + self._key = None + self._state = "UNINITIALIZED" + self._data = b"" + + @staticmethod + def getInstance(algorithm, provider=None): + return Mac(algorithm) + + def init(self, key, params=None): + self._key = key + self._state = "INITIALIZED" + self._data = b"" + + def update(self, input_data, offset=0, length=-1): + if length < 0: + self._data += bytes(input_data) + else: + self._data += bytes(input_data[offset:offset+length]) + + def doFinal(self, input_data=None): + if input_data is not None: + self._data += bytes(input_data) + if HAS_CRYPTOGRAPHY and self._key is not None: + try: + key_bytes = self._key.getEncoded() if hasattr(self._key, "getEncoded") else bytes(self._key) + algo = self._algorithm.lower().replace("-", "") + if "sha256" in algo or "sha-256" in self._algorithm: + h = _hashlib.sha256(key_bytes + self._data) + elif "sha1" in algo or "sha-1" in self._algorithm: + h = _hashlib.sha1(key_bytes + self._data) + elif "md5" in algo: + h = _hashlib.md5(key_bytes + self._data) + else: + h = _hashlib.sha256(key_bytes + self._data) + result = h.digest() + self._data = b"" + return result + except Exception: + pass + import hashlib as _hashlib + key_bytes = self._key.getEncoded() if self._key and hasattr(self._key, "getEncoded") else b"key" + h = _hashlib.sha256(key_bytes + self._data) + result = h.digest() + self._data = b"" + return result + + def reset(self): + self._data = b"" + + def getAlgorithm(self): + return self._algorithm + + def getMacLength(self): + return 32 + + def getProvider(self): + return Provider("AndroidOpenSSL", 1.0, "Android OpenSSL provider") + + +class MacSpi(Stub): + pass + + +# ============================================================ +# Parameter specs +# ============================================================ + +class IvParameterSpec(Stub): + def __init__(self, iv): + super().__init__() + self._iv = bytes(iv) + + def getIV(self): + return self._iv + + +class PBEParameterSpec(Stub): + def __init__(self, salt, iteration_count): + super().__init__() + self._salt = bytes(salt) + self._iteration_count = iteration_count + + def getSalt(self): + return self._salt + + def getIterationCount(self): + return self._iteration_count + + +class GCMParameterSpec(Stub): + def __init__(self, t_len, src): + super().__init__() + self._t_len = t_len + self._iv = bytes(src) + + def getTLen(self): + return self._t_len + + def getIV(self): + return self._iv + + +class OAEPParameterSpec(Stub): + def __init__(self, md_name, mgf_name, mgf_spec, p_src): + super().__init__() + self._md_name = md_name + self._mgf_name = mgf_name + self._mgf_spec = mgf_spec + self._p_src = p_src + + def getDigestAlgorithm(self): + return self._md_name + + def getMGFAlgorithm(self): + return self._mgf_name + + def getMGFParameters(self): + return self._mgf_spec + + def getPSource(self): + return self._p_src + + +class PSource(Stub): + class PSpecified(Stub): + DEFAULT = None + + def __init__(self, p): + super().__init__() + self._p = p + + def getValue(self): + return self._p + + def __init__(self, p_src_id): + super().__init__() + self._p_src_id = p_src_id + + def getAlgorithm(self): + return self._p_src_id + + +PSource.PSpecified.DEFAULT = PSource.PSpecified(b"") + + +class MGF1ParameterSpec(Stub): + SHA1 = None + SHA256 = None + SHA384 = None + SHA512 = None + + def __init__(self, md_name): + super().__init__() + self._md_name = md_name + + def getDigestAlgorithm(self): + return self._md_name + + def toString(self): + return f"MGF1 {self._md_name}" + + +MGF1ParameterSpec.SHA1 = MGF1ParameterSpec("SHA-1") +MGF1ParameterSpec.SHA256 = MGF1ParameterSpec("SHA-256") +MGF1ParameterSpec.SHA384 = MGF1ParameterSpec("SHA-384") +MGF1ParameterSpec.SHA512 = MGF1ParameterSpec("SHA-512") + + +# ============================================================ +# DH / DSA / EC parameter specs (stubs) +# ============================================================ + +class DHParameterSpec(Stub): + def __init__(self, p, g, l=0): + super().__init__() + self._p = p + self._g = g + self._l = l + + def getP(self): + return self._p + + def getG(self): + return self._g + + def getL(self): + return self._l + + +class DSAParameterSpec(Stub): + def __init__(self, p, q, g): + super().__init__() + self._p = p + self._q = q + self._g = g + + def getP(self): + return self._p + + def getQ(self): + return self._q + + def getG(self): + return self._g + + +class ECPoint(Stub): + def __init__(self, affine_x, affine_y): + super().__init__() + self._affine_x = affine_x + self._affine_y = affine_y + + def getAffineX(self): + return self._affine_x + + def getAffineY(self): + return self._affine_y + + def equals(self, other): + if isinstance(other, ECPoint): + return self._affine_x == other._affine_x and self._affine_y == other._affine_y + return False + + def hashCode(self): + return hash((self._affine_x, self._affine_y)) + + +class ECField(Stub): + def getFieldSize(self): + raise NotImplementedError + + +class ECFieldFp(ECField): + def __init__(self, p): + super().__init__() + self._p = p + + def getP(self): + return self._p + + def getFieldSize(self): + return self._p.bit_length() + + +class ECFieldF2m(ECField): + def __init__(self, m, ks=None): + super().__init__() + self._m = m + self._ks = ks + + def getM(self): + return self._m + + def getMidTermsOfReductionPolynomial(self): + return self._ks + + def getFieldSize(self): + return self._m + + +class EllipticCurve(Stub): + def __init__(self, field, a, b, seed=None): + super().__init__() + self._field = field + self._a = a + self._b = b + self._seed = seed + + def getField(self): + return self._field + + def getA(self): + return self._a + + def getB(self): + return self._b + + def getSeed(self): + return self._seed + + +class ECParameterSpec(Stub): + def __init__(self, curve, generator, order, cofactor, seed=None): + super().__init__() + self._curve = curve + self._generator = generator + self._order = order + self._cofactor = cofactor + + def getCurve(self): + return self._curve + + def getGenerator(self): + return self._generator + + def getOrder(self): + return self._order + + def getCofactor(self): + return self._cofactor + + +class ECGenParameterSpec(Stub): + def __init__(self, name): + super().__init__() + self._name = name + + def getName(self): + return self._name + + +# ============================================================ +# ExemptionMechanism +# ============================================================ + +class ExemptionMechanism(Stub): + def __init__(self, name): + super().__init__() + self._name = name + self._state = False + + @staticmethod + def getInstance(name, provider=None): + return ExemptionMechanism(name) + + def init(self, key): + self._state = False + + def isCryptoAllowed(self, key): + return True + + def genExemptionBlob(self, data=None): + return b"" + + def getName(self): + return self._name + + def getOutputSize(self, input_len): + return input_len + + def storeMechanism(self, mechanism): + pass + + +class ExemptionMechanismSpi(Stub): + pass \ No newline at end of file diff --git a/android_pyapi/java/io.py b/android_pyapi/java/io.py new file mode 100644 index 0000000..c757108 --- /dev/null +++ b/android_pyapi/java/io.py @@ -0,0 +1,1933 @@ +""" +Java I/O standard library APIs implemented in Python. +java.io package - File, streams, readers, writers, and exceptions. +""" + +from .._core import Stub, AndroidOnlyError +import io as _pyio +import os as _os +import sys as _sys +import errno as _errno +import stat as _stat +import threading as _threading +from datetime import datetime as _datetime +from collections import OrderedDict as _OrderedDict +from pathlib import Path as _PathLibPath + +__all__ = [ + "Serializable", "Externalizable", "ObjectStreamField", "ObjectStreamConstants", + "FileFilter", "FilenameFilter", "File", "FileDescriptor", + "FileInputStream", "FileOutputStream", + "BufferedInputStream", "BufferedOutputStream", + "BufferedReader", "BufferedWriter", + "InputStreamReader", "OutputStreamWriter", + "FileReader", "FileWriter", "PrintWriter", + "ByteArrayInputStream", "ByteArrayOutputStream", + "StringReader", "StringWriter", + "DataInputStream", "DataOutputStream", + "ObjectInputStream", "ObjectOutputStream", + "FilterInputStream", "FilterOutputStream", + "PipedInputStream", "PipedOutputStream", + "PushbackInputStream", "SequenceInputStream", + "RandomAccessFile", + "Closeable", "Flushable", + "IOError", "IOException", "FileNotFoundException", + "EOFException", "CharConversionException", + "ObjectStreamException", "SyncFailedException", + "UTFDataFormatException", "WriteAbortedException", + "UnsupportedEncodingException", +] + + +# ============================================================ +# Marker / mixin interfaces +# ============================================================ + +class Closeable(Stub): + def close(self): + raise NotImplementedError + +class Flushable(Stub): + def flush(self): + raise NotImplementedError + +class Serializable(Stub): + pass + +class Externalizable(Stub): + pass + +class ObjectStreamConstants: + STREAM_MAGIC = 0xACED + STREAM_VERSION = 5 + TC_BASE = 0x70 + TC_NULL = 0x70 + TC_REFERENCE = 0x71 + TC_CLASS = 0x72 + TC_OBJECT = 0x73 + TC_STRING = 0x74 + TC_ARRAY = 0x75 + TC_ENUM = 0x7E + TC_CLASSDESC = 0x72 + TC_PROXYCLASSDESC = 0x7D + TC_BLOCKDATA = 0x77 + TC_ENDBLOCKDATA = 0x78 + TC_RESET = 0x79 + TC_EXCEPTION = 0x7A + TC_LONGSTRING = 0x7C + baseWireHandle = 0x7E0000 + +class ObjectStreamField(Stub): + def __init__(self, name, typ): + self._name = name + self._type = typ + + def getName(self): + return self._name + + def getType(self): + return self._type + + def toString(self): + return f"{self._type} {self._name}" + + +# ============================================================ +# FileFilter / FilenameFilter +# ============================================================ + +class FileFilter(Stub): + def accept(self, pathname): + raise NotImplementedError + +class FilenameFilter(Stub): + def accept(self, dir, name): + raise NotImplementedError + + +# ============================================================ +# FileDescriptor +# ============================================================ + +class FileDescriptor(Stub): + _valid_fd = -1 + + def __init__(self, fd=None): + super().__init__() + if fd is not None: + self._valid_fd = fd + + def valid(self): + return self._valid_fd >= 0 + + def sync(self): + if self._valid_fd >= 0: + try: + _os.fsync(self._valid_fd) + except OSError: + pass + + def hashCode(self): + return self._valid_fd + + @staticmethod + def _from_fileno(fd): + return FileDescriptor(fd) + + +# ============================================================ +# Exception classes +# ============================================================ + +class IOException(Exception): + def __init__(self, message=None, cause=None): + super().__init__(message) + self._cause = cause + + def getCause(self): + return self._cause + +class FileNotFoundException(IOException): + pass + +class EOFException(IOException): + pass + +class CharConversionException(IOException): + pass + +class ObjectStreamException(IOException): + pass + +class SyncFailedException(IOException): + pass + +class UTFDataFormatException(IOException): + pass + +class WriteAbortedException(ObjectStreamException): + def __init__(self, message=None, cause=None): + super().__init__(message) + self.detail = cause + +class UnsupportedEncodingException(IOException): + pass + +class IOError(Exception): + def __init__(self, message=None, cause=None): + super().__init__(message) + self._cause = cause + + def getCause(self): + return self._cause + + +# ============================================================ +# File +# ============================================================ + +class File(Stub): + separator = _os.sep + separatorChar = _os.sep + pathSeparator = _os.pathsep + pathSeparatorChar = _os.pathsep + + def __init__(self, *args): + super().__init__() + if len(args) == 1: + path = args[0] + if isinstance(path, File): + self._path = path._path + else: + self._path = str(path) + elif len(args) == 2: + parent, child = args + if parent is None: + self._path = str(child) + else: + parent_str = parent._path if isinstance(parent, File) else str(parent) + child_str = str(child) + if child_str.startswith(self.separator): + self._path = child_str + else: + self._path = _os.path.join(parent_str, child_str) + else: + self._path = "" + self._path = self._path or "" + + def _resolve(self): + return _os.path.realpath(self._path) if self._path else "" + + def exists(self): + return _os.path.exists(self._path) + + def isFile(self): + return _os.path.isfile(self._path) + + def isDirectory(self): + return _os.path.isdir(self._path) + + def getName(self): + return _os.path.basename(self._path) + + def getPath(self): + return self._path + + def getAbsolutePath(self): + return _os.path.abspath(self._path) + + def getCanonicalPath(self): + return _os.path.realpath(self._path) + + def getParent(self): + p = _os.path.dirname(self._path) + return p if p else None + + def getParentFile(self): + p = self.getParent() + return File(p) if p else None + + def length(self): + try: + return _os.path.getsize(self._path) + except OSError: + return 0 + + def lastModified(self): + try: + return int(_os.path.getmtime(self._path) * 1000) + except OSError: + return 0 + + def createNewFile(self): + try: + f = _os.open(self._path, _os.O_CREAT | _os.O_EXCL, 0o644) + _os.close(f) + return True + except OSError: + return False + + def delete(self): + try: + if self.isDirectory(): + _os.rmdir(self._path) + else: + _os.remove(self._path) + return True + except OSError: + return False + + def mkdir(self): + try: + _os.mkdir(self._path) + return True + except OSError: + return False + + def mkdirs(self): + try: + _os.makedirs(self._path, exist_ok=True) + return True + except OSError: + return False + + def renameTo(self, dest): + try: + dest_path = dest._path if isinstance(dest, File) else str(dest) + _os.rename(self._path, dest_path) + return True + except OSError: + return False + + def list(self): + try: + return _os.listdir(self._path) + except OSError: + return None + + def listFiles(self, filter=None): + try: + names = _os.listdir(self._path) + files = [File(self._path, n) for n in names] + if filter: + return [f for f in files if filter.accept(f)] + return files + except OSError: + return None + + @staticmethod + def listRoots(): + if _sys.platform == "win32": + import string + return [File(d + ":\\") for d in string.ascii_uppercase if _os.path.exists(d + ":\\")] + return [File("/")] + + def canRead(self): + return _os.access(self._path, _os.R_OK) + + def canWrite(self): + return _os.access(self._path, _os.W_OK) + + def canExecute(self): + return _os.access(self._path, _os.X_OK) + + def setReadable(self, readable, ownerOnly=True): + try: + if readable: + if ownerOnly: + _os.chmod(self._path, _stat.S_IRUSR | (_stat.S_IRGRP if _os.stat(self._path).st_gid else 0)) + else: + _os.chmod(self._path, _stat.S_IRUSR | _stat.S_IRGRP | _stat.S_IROTH) + else: + mode = _os.stat(self._path).st_mode + if ownerOnly: + mode &= ~_stat.S_IRUSR + else: + mode &= ~(_stat.S_IRUSR | _stat.S_IRGRP | _stat.S_IROTH) + _os.chmod(self._path, mode) + return True + except OSError: + return False + + def setWritable(self, writable, ownerOnly=True): + try: + if writable: + if ownerOnly: + _os.chmod(self._path, _stat.S_IWUSR) + else: + _os.chmod(self._path, _stat.S_IWUSR | _stat.S_IWGRP | _stat.S_IWOTH) + else: + mode = _os.stat(self._path).st_mode + if ownerOnly: + mode &= ~_stat.S_IWUSR + else: + mode &= ~(_stat.S_IWUSR | _stat.S_IWGRP | _stat.S_IWOTH) + _os.chmod(self._path, mode) + return True + except OSError: + return False + + def setExecutable(self, executable, ownerOnly=True): + try: + if executable: + if ownerOnly: + _os.chmod(self._path, _stat.S_IXUSR) + else: + _os.chmod(self._path, _stat.S_IXUSR | _stat.S_IXGRP | _stat.S_IXOTH) + else: + mode = _os.stat(self._path).st_mode + if ownerOnly: + mode &= ~_stat.S_IXUSR + else: + mode &= ~(_stat.S_IXUSR | _stat.S_IXGRP | _stat.S_IXOTH) + _os.chmod(self._path, mode) + return True + except OSError: + return False + + def isHidden(self): + return _os.path.basename(self._path).startswith(".") + + def toURL(self): + from urllib.parse import quote + return f"file:{quote(self.getAbsolutePath())}" + + def toURI(self): + return self.toURL() + + def getTotalSpace(self): + try: + st = _os.statvfs(self._path) + return st.f_frsize * st.f_blocks + except OSError: + return 0 + + def getFreeSpace(self): + try: + st = _os.statvfs(self._path) + return st.f_frsize * st.f_bfree + except OSError: + return 0 + + def getUsableSpace(self): + try: + st = _os.statvfs(self._path) + return st.f_frsize * st.f_bavail + except OSError: + return 0 + + def compareTo(self, other): + if isinstance(other, File): + return self._path.__cmp__(other._path) + return -1 + + def equals(self, other): + if isinstance(other, File): + return self._path == other._path + return False + + def hashCode(self): + return hash(self._path) & 0x7FFFFFFF + + def toString(self): + return self._path + + def toPath(self): + return _PathLibPath(self._path) + + def __str__(self): + return self._path + + def __repr__(self): + return f"File({self._path!r})" + + +# ============================================================ +# FileInputStream +# ============================================================ + +class FileInputStream(Closeable): + def __init__(self, file): + super().__init__() + if isinstance(file, File): + path = file.getPath() + else: + path = str(file) + self._path = path + self._fd = None + self._channel = None + self._closed = False + try: + self._fd = _os.open(path, _os.O_RDONLY) + except OSError as e: + raise FileNotFoundException(str(e)) + + def read(self, b=None, off=None, length=None): + if self._closed: + raise IOException("Stream closed") + if b is None: + data = _os.read(self._fd, 1) + return data[0] if data else -1 + if isinstance(b, (bytes, bytearray)): + if off is None: + off = 0 + if length is None: + length = len(b) - off + data = _os.read(self._fd, length) + if not data: + return -1 + n = len(data) + if isinstance(b, bytearray): + b[off:off+n] = data + else: + ba = bytearray(b) + ba[off:off+n] = data + b[:] = bytes(ba) + return n + return -1 + + def skip(self, n): + try: + pos = _os.lseek(self._fd, 0, _os.SEEK_CUR) + end = _os.lseek(self._fd, 0, _os.SEEK_END) + _os.lseek(self._fd, pos, _os.SEEK_SET) + actual = min(n, end - pos) + _os.lseek(self._fd, actual, _os.SEEK_CUR) + return actual + except OSError: + return 0 + + def available(self): + if self._closed: + return 0 + try: + pos = _os.lseek(self._fd, 0, _os.SEEK_CUR) + end = _os.lseek(self._fd, 0, _os.SEEK_END) + _os.lseek(self._fd, pos, _os.SEEK_SET) + return end - pos + except OSError: + return 0 + + def close(self): + if not self._closed: + self._closed = True + if self._fd is not None: + _os.close(self._fd) + self._fd = None + + def getFD(self): + if self._closed: + raise IOException("Stream closed") + return FileDescriptor(self._fd) + + def getChannel(self): + if self._closed: + raise IOException("Stream closed") + if self._channel is None: + import io as _pyio_mod + self._channel = _pyio_mod.FileIO(self._fd, closefd=False) + return self._channel + + def __del__(self): + self.close() + + +# ============================================================ +# FileOutputStream +# ============================================================ + +class FileOutputStream(Closeable, Flushable): + def __init__(self, file, append=False): + super().__init__() + if isinstance(file, File): + path = file.getPath() + elif isinstance(file, FileDescriptor): + self._fd = file._valid_fd + self._path = None + self._closed = False + self._channel = None + return + else: + path = str(file) + self._path = path + self._closed = False + self._channel = None + try: + flags = _os.O_WRONLY | _os.O_CREAT + if append: + flags |= _os.O_APPEND + else: + flags |= _os.O_TRUNC + self._fd = _os.open(path, flags, 0o644) + except OSError as e: + raise FileNotFoundException(str(e)) + + def write(self, b=None, off=None, length=None): + if self._closed: + raise IOException("Stream closed") + if b is None: + return + if isinstance(b, int): + _os.write(self._fd, bytes([b])) + elif isinstance(b, (bytes, bytearray)): + if off is None: + data = bytes(b) + else: + data = bytes(b[off:off+length if length else (len(b)-off)]) + _os.write(self._fd, data) + else: + if off is None: + data = bytes(b) + else: + data = bytes(b[off:off+length if length else (len(b)-off)]) + _os.write(self._fd, data) + + def close(self): + if not self._closed: + self._closed = True + if self._fd is not None: + _os.close(self._fd) + self._fd = None + + def getFD(self): + if self._closed: + raise IOException("Stream closed") + return FileDescriptor(self._fd) + + def getChannel(self): + if self._closed: + raise IOException("Stream closed") + if self._channel is None: + import io as _pyio_mod + self._channel = _pyio_mod.FileIO(self._fd, closefd=False, mode='ab') + return self._channel + + def flush(self): + if self._fd is not None: + try: + _os.fsync(self._fd) + except OSError: + pass + + def __del__(self): + self.close() + + +# ============================================================ +# FilterInputStream / FilterOutputStream (abstract wrappers) +# ============================================================ + +class FilterInputStream(Closeable): + def __init__(self, inp): + super().__init__() + self._in = inp + + def read(self, *args): + return self._in.read(*args) + + def skip(self, n): + return self._in.skip(n) + + def available(self): + return self._in.available() + + def close(self): + self._in.close() + + def mark(self, readlimit): + pass + + def reset(self): + raise IOException("mark/reset not supported") + + def markSupported(self): + return False + + +class FilterOutputStream(Closeable, Flushable): + def __init__(self, out): + super().__init__() + self._out = out + + def write(self, *args): + self._out.write(*args) + + def close(self): + self._out.close() + + def flush(self): + self._out.flush() + + +# ============================================================ +# BufferedInputStream +# ============================================================ + +class BufferedInputStream(FilterInputStream): + def __init__(self, inp, size=8192): + super().__init__(inp) + self._buffer = bytearray() + self._buf_size = size + self._pos = 0 + self._count = 0 + + def read(self, b=None, off=None, length=None): + if b is None: + if self._pos >= self._count: + self._fill() + if self._pos >= self._count: + return -1 + val = self._buffer[self._pos] + self._pos += 1 + return val + if off is None: + off = 0 + if length is None: + length = len(b) + total = 0 + while total < length: + if self._pos >= self._count: + if not self._fill(): + break + chunk = min(length - total, self._count - self._pos) + if isinstance(b, bytearray): + b[off+total:off+total+chunk] = self._buffer[self._pos:self._pos+chunk] + else: + ba = bytearray(b) + ba[off+total:off+total+chunk] = self._buffer[self._pos:self._pos+chunk] + b[:] = bytes(ba) + self._pos += chunk + total += chunk + return total if total > 0 else -1 + + def _fill(self): + data = self._in.read(bytearray(self._buf_size), 0, self._buf_size) + if data is None or data == -1: + return False + n = data if isinstance(data, int) else len(data) + self._buffer = bytearray(self._in._buffer if hasattr(self._in, '_buffer') else data) if n > 0 else bytearray() + self._pos = 0 + self._count = n + return n > 0 + + +# ============================================================ +# BufferedOutputStream +# ============================================================ + +class BufferedOutputStream(FilterOutputStream): + def __init__(self, out, size=8192): + super().__init__(out) + self._buf = bytearray() + self._buf_size = size + + def write(self, b=None, off=None, length=None): + if b is None: + return + if isinstance(b, int): + self._buf.append(b & 0xFF) + else: + if off is not None and length is not None: + data = b[off:off+length] + else: + data = b + self._buf.extend(data) + if len(self._buf) >= self._buf_size: + self.flush() + + def flush(self): + if self._buf: + self._out.write(bytes(self._buf)) + self._buf.clear() + self._out.flush() + + +# ============================================================ +# InputStreamReader +# ============================================================ + +class InputStreamReader(Closeable): + def __init__(self, inp, charset=None): + super().__init__() + self._in = inp + self._charset = charset or "utf-8" + self._buffer = None + + def read(self, cbuf=None, off=None, length=None): + if cbuf is None: + data = self._in.read() + if data is None or data == -1: + return -1 + return data & 0xFF + if off is None: + off = 0 + if length is None: + length = len(cbuf) + raw = self._in.read(bytearray(length), 0, length) + if raw is None or raw == -1: + return -1 + n = len(raw) if isinstance(raw, (bytes, bytearray)) else raw + if isinstance(raw, int): + return raw + decoded = raw.decode(self._charset, errors='replace') + for i, ch in enumerate(decoded): + if off + i < len(cbuf): + cbuf[off + i] = ch + return len(decoded) + + def getEncoding(self): + return self._charset + + def close(self): + self._in.close() + + +# ============================================================ +# OutputStreamWriter +# ============================================================ + +class OutputStreamWriter(Closeable, Flushable): + def __init__(self, out, charset=None): + super().__init__() + self._out = out + self._charset = charset or "utf-8" + + def write(self, s, off=None, length=None): + if isinstance(s, str): + if off is not None and length is not None: + s = s[off:off+length] + self._out.write(s.encode(self._charset)) + elif isinstance(s, (list, tuple)): + chars = "".join(s) + if off is not None and length is not None: + chars = chars[off:off+length] + self._out.write(chars.encode(self._charset)) + + def flush(self): + self._out.flush() + + def close(self): + self._out.close() + + def getEncoding(self): + return self._charset + + +# ============================================================ +# FileReader +# ============================================================ + +class FileReader(InputStreamReader): + def __init__(self, file): + if isinstance(file, File): + path = file.getPath() + else: + path = str(file) + inp = FileInputStream(path) + super().__init__(inp, "utf-8") + self._file_path = path + + +# ============================================================ +# FileWriter +# ============================================================ + +class FileWriter(OutputStreamWriter): + def __init__(self, file, append=False): + if isinstance(file, File): + path = file.getPath() + append = append + else: + path = str(file) + out = FileOutputStream(path, append) + super().__init__(out, "utf-8") + self._file_path = path + + +# ============================================================ +# BufferedReader +# ============================================================ + +class BufferedReader(Closeable): + def __init__(self, reader, size=8192): + super().__init__() + self._reader = reader + self._buf_size = size + self._buffer = "" + self._pos = 0 + self._closed = False + + def read(self, cbuf=None, off=None, length=None): + if self._closed: + raise IOException("Stream closed") + if cbuf is None: + if self._pos >= len(self._buffer): + self._fill() + if self._pos >= len(self._buffer): + return -1 + ch = ord(self._buffer[self._pos]) + self._pos += 1 + return ch + if off is None: + off = 0 + if length is None: + length = len(cbuf) + total = 0 + while total < length: + if self._pos >= len(self._buffer): + if not self._fill(): + break + chunk = min(length - total, len(self._buffer) - self._pos) + for i in range(chunk): + cbuf[off + total + i] = self._buffer[self._pos + i] + self._pos += chunk + total += chunk + return total if total > 0 else -1 + + def _fill(self): + if hasattr(self._reader, '_in'): + raw = self._reader._in.read(bytearray(self._buf_size), 0, self._buf_size) + if raw is None or raw == -1: + return False + data = bytes(raw) if isinstance(raw, int) else bytes(raw) + self._buffer = data.decode(self._reader.getEncoding() if hasattr(self._reader, 'getEncoding') else "utf-8", errors='replace') + else: + raw = [] + for _ in range(self._buf_size): + ch = self._reader.read() + if ch == -1: + break + raw.append(chr(ch)) + self._buffer = "".join(raw) + self._pos = 0 + return len(self._buffer) > 0 + + def readLine(self): + if self._closed: + raise IOException("Stream closed") + result = [] + while True: + if self._pos >= len(self._buffer): + if not self._fill(): + break + while self._pos < len(self._buffer): + ch = self._buffer[self._pos] + self._pos += 1 + if ch == "\n": + return "".join(result) + if ch == "\r": + if self._pos < len(self._buffer) and self._buffer[self._pos] == "\n": + self._pos += 1 + return "".join(result) + result.append(ch) + return "".join(result) if result else None + + def ready(self): + return self._pos < len(self._buffer) + + def mark(self, readAheadLimit): + self._mark_pos = self._pos + self._mark_buffer = self._buffer + + def reset(self): + if hasattr(self, '_mark_pos'): + self._pos = self._mark_pos + self._buffer = self._mark_buffer + else: + raise IOException("Mark not set") + + def markSupported(self): + return True + + def skip(self, n): + skipped = 0 + while skipped < n: + if self._pos >= len(self._buffer): + if not self._fill(): + break + chunk = min(n - skipped, len(self._buffer) - self._pos) + self._pos += chunk + skipped += chunk + return skipped + + def close(self): + self._closed = True + self._reader.close() + + def lines(self): + line = self.readLine() + while line is not None: + yield line + line = self.readLine() + + +# ============================================================ +# BufferedWriter +# ============================================================ + +class BufferedWriter(Closeable, Flushable): + def __init__(self, writer, size=8192): + super().__init__() + self._writer = writer + self._buf_size = size + self._buffer = [] + + def write(self, s, off=None, length=None): + if isinstance(s, str): + if off is not None and length is not None: + s = s[off:off+length] + self._buffer.append(s) + elif isinstance(s, (list, tuple)): + chars = "".join(s) + if off is not None and length is not None: + chars = chars[off:off+length] + self._buffer.append(chars) + elif isinstance(s, int): + self._buffer.append(chr(s)) + if sum(len(x) for x in self._buffer) >= self._buf_size: + self.flush() + + def newLine(self): + self._buffer.append("\n") + + def flush(self): + if self._buffer: + data = "".join(self._buffer) + self._writer.write(data) + self._buffer.clear() + self._writer.flush() + + def close(self): + self.flush() + self._writer.close() + + +# ============================================================ +# PrintWriter +# ============================================================ + +class PrintWriter(Closeable, Flushable): + def __init__(self, out, autoFlush=False): + super().__init__() + if isinstance(out, _pyio.TextIOBase): + self._out = out + else: + self._out = out + self._auto_flush = autoFlush + self._error = False + + def _write_str(self, s): + s = str(s) + if hasattr(self._out, 'write'): + self._out.write(s) + else: + self._out.write(s.encode('utf-8')) + + def print_bool(self, b): + self._write_str(str(b)) + if self._auto_flush: + self.flush() + + def print_char(self, c): + self._write_str(str(c)) + if self._auto_flush: + self.flush() + + def print_char_array(self, c): + self._write_str("".join(c)) + if self._auto_flush: + self.flush() + + def print_double(self, d): + self._write_str(str(d)) + if self._auto_flush: + self.flush() + + def print_float(self, f): + self._write_str(str(f)) + if self._auto_flush: + self.flush() + + def print_int(self, i): + self._write_str(str(i)) + if self._auto_flush: + self.flush() + + def print_long(self, l): + self._write_str(str(l)) + if self._auto_flush: + self.flush() + + def print_obj(self, obj): + self._write_str(str(obj)) + if self._auto_flush: + self.flush() + + def print_str(self, s): + self._write_str(str(s) if s is not None else "null") + if self._auto_flush: + self.flush() + + print = print_str + print_boolean = print_bool + + def println(self, s=None): + if s is None: + self._write_str("\n") + else: + self._write_str(str(s) + "\n") + if self._auto_flush: + self.flush() + + def println_bool(self, b): + self._write_str(str(b) + "\n") + if self._auto_flush: + self.flush() + + def println_char(self, c): + self._write_str(str(c) + "\n") + if self._auto_flush: + self.flush() + + def println_char_array(self, c): + self._write_str("".join(c) + "\n") + if self._auto_flush: + self.flush() + + def println_double(self, d): + self._write_str(str(d) + "\n") + if self._auto_flush: + self.flush() + + def println_float(self, f): + self._write_str(str(f) + "\n") + if self._auto_flush: + self.flush() + + def println_int(self, i): + self._write_str(str(i) + "\n") + if self._auto_flush: + self.flush() + + def println_long(self, l): + self._write_str(str(l) + "\n") + if self._auto_flush: + self.flush() + + def println_obj(self, obj): + self._write_str(str(obj) + "\n") + if self._auto_flush: + self.flush() + + def write(self, s, off=None, length=None): + if isinstance(s, str): + if off is not None and length is not None: + s = s[off:off+length] + self._write_str(s) + else: + self._write_str(str(s)) + + def format(self, fmt, *args): + s = str(fmt) % args if args else str(fmt) + self._write_str(s) + return self + + def printf(self, fmt, *args): + return self.format(fmt, *args) + + def flush(self): + if hasattr(self._out, 'flush'): + self._out.flush() + + def close(self): + self.flush() + if hasattr(self._out, 'close'): + self._out.close() + + def checkError(self): + return self._error + + def setError(self): + self._error = True + + +# ============================================================ +# ByteArrayInputStream +# ============================================================ + +class ByteArrayInputStream(Closeable): + def __init__(self, buf): + super().__init__() + self._buf = bytes(buf) + self._pos = 0 + self._count = len(self._buf) + self._mark = 0 + self._closed = False + + def read(self, b=None, off=None, length=None): + if self._closed: + raise IOException("Stream closed") + if b is None: + if self._pos >= self._count: + return -1 + val = self._buf[self._pos] & 0xFF + self._pos += 1 + return val + if off is None: + off = 0 + if length is None: + length = len(b) + avail = self._count - self._pos + if avail <= 0: + return -1 + n = min(length, avail) + if isinstance(b, bytearray): + b[off:off+n] = self._buf[self._pos:self._pos+n] + else: + ba = bytearray(b) + ba[off:off+n] = self._buf[self._pos:self._pos+n] + b[:] = bytes(ba) + self._pos += n + return n + + def skip(self, n): + if self._closed: + return 0 + avail = self._count - self._pos + actual = min(n, avail) + self._pos += actual + return actual + + def available(self): + if self._closed: + return 0 + return self._count - self._pos + + def close(self): + self._closed = True + + def mark(self, readAheadLimit): + self._mark = self._pos + + def reset(self): + self._pos = self._mark + + +# ============================================================ +# ByteArrayOutputStream +# ============================================================ + +class ByteArrayOutputStream(Closeable, Flushable): + def __init__(self, size=32): + super().__init__() + self._buf = bytearray() + self._closed = False + + def write(self, b=None, off=None, length=None): + if self._closed: + raise IOException("Stream closed") + if b is None: + return + if isinstance(b, int): + self._buf.append(b & 0xFF) + elif isinstance(b, (bytes, bytearray)): + if off is not None and length is not None: + self._buf.extend(b[off:off+length]) + else: + self._buf.extend(b) + + def toByteArray(self): + return bytes(self._buf) + + def size(self): + return len(self._buf) + + def toString(self, charset=None): + if charset: + return bytes(self._buf).decode(charset, errors='replace') + return bytes(self._buf).decode('utf-8', errors='replace') + + def reset(self): + self._buf.clear() + + def close(self): + self._closed = True + + def flush(self): + pass + + +# ============================================================ +# StringReader +# ============================================================ + +class StringReader(Closeable): + def __init__(self, s): + super().__init__() + self._str = s + self._pos = 0 + self._length = len(s) + self._mark = 0 + self._closed = False + + def read(self, cbuf=None, off=None, length=None): + if self._closed: + raise IOException("Stream closed") + if cbuf is None: + if self._pos >= self._length: + return -1 + val = ord(self._str[self._pos]) + self._pos += 1 + return val + if off is None: + off = 0 + if length is None: + length = len(cbuf) + avail = self._length - self._pos + if avail <= 0: + return -1 + n = min(length, avail) + for i in range(n): + cbuf[off + i] = self._str[self._pos + i] + self._pos += n + return n + + def skip(self, n): + avail = self._length - self._pos + actual = min(n, avail) + self._pos += actual + return max(actual, 0) + + def ready(self): + return self._pos < self._length + + def mark(self, readAheadLimit): + self._mark = self._pos + + def reset(self): + self._pos = self._mark + + def markSupported(self): + return True + + def close(self): + self._closed = True + + +# ============================================================ +# StringWriter +# ============================================================ + +class StringWriter(Closeable, Flushable): + def __init__(self): + super().__init__() + self._buffer = [] + self._closed = False + + def write(self, s, off=None, length=None): + if self._closed: + raise IOException("Stream closed") + if isinstance(s, str): + if off is not None and length is not None: + s = s[off:off+length] + self._buffer.append(s) + elif isinstance(s, int): + self._buffer.append(chr(s)) + elif isinstance(s, (list, tuple)): + chars = "".join(s) + if off is not None and length is not None: + chars = chars[off:off+length] + self._buffer.append(chars) + + def flush(self): + pass + + def close(self): + self._closed = True + + def toString(self): + return "".join(self._buffer) + + def getBuffer(self): + return "".join(self._buffer) + + +# ============================================================ +# DataInputStream +# ============================================================ + +class DataInputStream(Closeable): + def __init__(self, inp): + super().__init__() + self._in = inp + + def read(self, b=None): + if b is None: + return self._in.read() + return self._in.read(b) + + def readBoolean(self): + v = self._in.read() + if v == -1: + raise EOFException() + return bool(v) + + def readByte(self): + v = self._in.read() + if v == -1: + raise EOFException() + return (v & 0xFF) if v <= 127 else (v - 256) + + def readChar(self): + hi = self._in.read() + lo = self._in.read() + if hi == -1 or lo == -1: + raise EOFException() + return chr((hi << 8) | lo) + + def readDouble(self): + return _struct.unpack(">d", self._read_bytes(8))[0] + + def readFloat(self): + return _struct.unpack(">f", self._read_bytes(4))[0] + + def readFully(self, b, off=0, length=None): + if length is None: + length = len(b) + total = 0 + while total < length: + n = self._in.read(b, off + total, length - total) + if n == -1: + raise EOFException() + total += n + + def readInt(self): + return _struct.unpack(">i", self._read_bytes(4))[0] + + def readLong(self): + return _struct.unpack(">q", self._read_bytes(8))[0] + + def readShort(self): + v = _struct.unpack(">h", self._read_bytes(2))[0] + return v + + def readUTF(self): + length = self.readUnsignedShort() + data = self._read_bytes(length) + return data.decode("utf-8") + + def readUnsignedByte(self): + v = self._in.read() + if v == -1: + raise EOFException() + return v & 0xFF + + def readUnsignedShort(self): + hi = self._in.read() + lo = self._in.read() + if hi == -1 or lo == -1: + raise EOFException() + return ((hi & 0xFF) << 8) | (lo & 0xFF) + + def skipBytes(self, n): + return self._in.skip(n) + + def _read_bytes(self, n): + buf = bytearray(n) + total = 0 + while total < n: + r = self._in.read(buf, total, n - total) + if r == -1: + raise EOFException() + total += r + return bytes(buf) + + def close(self): + self._in.close() + + +# ============================================================ +# DataOutputStream +# ============================================================ + +import struct as _struct + +class DataOutputStream(Closeable, Flushable): + def __init__(self, out): + super().__init__() + self._out = out + self._written = 0 + + def write(self, b=None, off=None, length=None): + if b is not None: + self._out.write(b, off, length) + self._written += length if length else (len(b) if isinstance(b, (bytes, bytearray)) else 1) + + def writeBoolean(self, v): + self._out.write(int(v)) + + def writeByte(self, v): + self._out.write(v & 0xFF) + + def writeBytes(self, s): + for ch in str(s): + self._out.write(ord(ch) & 0xFF) + + def writeChar(self, v): + self._out.write((v >> 8) & 0xFF) + self._out.write(v & 0xFF) + + def writeChars(self, s): + for ch in str(s): + self.writeChar(ord(ch)) + + def writeDouble(self, v): + self._write_struct(">d", v) + + def writeFloat(self, v): + self._write_struct(">f", v) + + def writeInt(self, v): + self._write_struct(">i", v) + + def writeLong(self, v): + self._write_struct(">q", v) + + def writeShort(self, v): + self._write_struct(">h", v) + + def writeUTF(self, s): + encoded = str(s).encode("utf-8") + self.writeShort(len(encoded)) + for b in encoded: + self._out.write(b) + + def size(self): + return self._written + + def _write_struct(self, fmt, v): + data = _struct.pack(fmt, v) + for b in data: + self._out.write(b) + + def flush(self): + self._out.flush() + + def close(self): + self._out.close() + + +# ============================================================ +# ObjectInputStream / ObjectOutputStream (stub) +# ============================================================ + +class ObjectInputStream(Closeable): + def __init__(self, inp): + super().__init__() + self._in = inp + + def readObject(self): + raise AndroidOnlyError("ObjectInputStream.readObject()") + + def readInt(self): + return DataInputStream(self._in).readInt() + + def close(self): + self._in.close() + + +class ObjectOutputStream(Closeable, Flushable): + def __init__(self, out): + super().__init__() + self._out = out + + def writeObject(self, obj): + raise AndroidOnlyError("ObjectOutputStream.writeObject()") + + def writeInt(self, v): + DataOutputStream(self._out).writeInt(v) + + def flush(self): + self._out.flush() + + def close(self): + self._out.close() + + +# ============================================================ +# PipedInputStream / PipedOutputStream +# ============================================================ + +class PipedInputStream(Closeable): + def __init__(self, src=None, pipe_size=1024): + super().__init__() + self._buffer = bytearray() + self._pipe_size = pipe_size + self._closed = False + self._lock = _threading.RLock() + self._data_available = _threading.Condition(self._lock) + if src: + src.connect(self) + + def connect(self, src): + src._sink = self + + def receive(self, b): + with self._lock: + self._buffer.append(b & 0xFF) + self._data_available.notify() + + def read(self, b=None, off=None, length=None): + with self._lock: + while len(self._buffer) == 0 and not self._closed: + self._data_available.wait(timeout=1) + if self._closed and len(self._buffer) == 0: + return -1 + if b is None: + if len(self._buffer) == 0: + return -1 + return self._buffer.pop(0) + if off is None: + off = 0 + if length is None: + length = len(b) + n = min(length, len(self._buffer)) + for i in range(n): + if isinstance(b, bytearray): + b[off + i] = self._buffer.pop(0) + else: + b[off + i] = self._buffer.pop(0) + return n + + def close(self): + with self._lock: + self._closed = True + self._data_available.notify_all() + + +class PipedOutputStream(Closeable): + def __init__(self, sink=None): + super().__init__() + self._sink = sink + + def connect(self, sink): + self._sink = sink + + def write(self, b, off=None, length=None): + if self._sink is None: + raise IOException("Pipe not connected") + if isinstance(b, int): + self._sink.receive(b) + else: + if off is not None and length is not None: + for i in range(off, off + length): + self._sink.receive(b[i]) + else: + for byte in b: + self._sink.receive(byte) + + def flush(self): + pass + + def close(self): + if self._sink: + self._sink.close() + + +# ============================================================ +# PushbackInputStream +# ============================================================ + +class PushbackInputStream(FilterInputStream): + def __init__(self, inp, size=1): + super().__init__(inp) + self._pushback_buf = bytearray() + self._max_size = size + + def read(self, b=None, off=None, length=None): + if b is None: + if self._pushback_buf: + return self._pushback_buf.pop(0) + return self._in.read() + if off is None: + off = 0 + if length is None: + length = len(b) + total = 0 + while self._pushback_buf and total < length: + if isinstance(b, bytearray): + b[off + total] = self._pushback_buf.pop(0) + else: + ba = bytearray(b) + ba[off + total] = self._pushback_buf.pop(0) + b[:] = bytes(ba) + total += 1 + if total < length: + n = self._in.read(b, off + total, length - total) + if n > 0: + total += n + return total if total > 0 else -1 + + def unread(self, b): + if isinstance(b, int): + if len(self._pushback_buf) >= self._max_size: + raise IOException("Pushback buffer overflow") + self._pushback_buf.append(b & 0xFF) + else: + for byte_val in b: + self.unread(byte_val) + + def close(self): + self._pushback_buf.clear() + self._in.close() + + +# ============================================================ +# SequenceInputStream +# ============================================================ + +class SequenceInputStream(Closeable): + def __init__(self, streams): + super().__init__() + if hasattr(streams, 'hasMoreElements'): + self._streams = [] + while streams.hasMoreElements(): + self._streams.append(streams.nextElement()) + else: + self._streams = list(streams) + self._current_idx = 0 + + def _current(self): + if self._current_idx < len(self._streams): + return self._streams[self._current_idx] + return None + + def read(self, b=None, off=None, length=None): + while self._current(): + inp = self._current() + if b is None: + result = inp.read() + else: + result = inp.read(b, off, length) + if result != -1: + return result + self._current_idx += 1 + return -1 + + def close(self): + for s in self._streams: + s.close() + + +# ============================================================ +# RandomAccessFile +# ============================================================ + +class RandomAccessFile(Closeable, Flushable): + def __init__(self, file, mode="r"): + super().__init__() + if isinstance(file, File): + path = file.getPath() + else: + path = str(file) + self._path = path + self._mode = mode + self._closed = False + py_modes = {"r": "rb", "rw": "r+b", "rws": "r+b", "rwd": "r+b"} + py_mode = py_modes.get(mode, "r+b") + try: + if "w" in mode and mode != "r": + self._file = open(path, py_mode) + else: + self._file = open(path, py_mode) + except FileNotFoundError as e: + if "w" in mode: + self._file = open(path, "w+b") + else: + raise FileNotFoundException(str(e)) + + def seek(self, pos): + self._file.seek(pos) + + def getFilePointer(self): + return self._file.tell() + + def length(self): + pos = self._file.tell() + self._file.seek(0, 2) + end = self._file.tell() + self._file.seek(pos) + return end + + def read(self, b=None, off=None, length=None): + if b is None: + data = self._file.read(1) + return data[0] if data else -1 + if off is None: + off = 0 + if length is None: + length = len(b) + data = self._file.read(length) + if not data: + return -1 + n = len(data) + if isinstance(b, bytearray): + b[off:off+n] = data + else: + ba = bytearray(b) + ba[off:off+n] = data + b[:] = bytes(ba) + return n + + def readBoolean(self): + v = self.read() + if v == -1: + raise EOFException() + return bool(v) + + def readByte(self): + v = self.read() + if v == -1: + raise EOFException() + return (v & 0xFF) if v <= 127 else (v - 256) + + def readChar(self): + hi = self.read() + lo = self.read() + if hi == -1 or lo == -1: + raise EOFException() + return chr((hi << 8) | lo) + + def readDouble(self): + data = self._read_bytes(8) + return _struct.unpack(">d", data)[0] + + def readFloat(self): + data = self._read_bytes(4) + return _struct.unpack(">f", data)[0] + + def readInt(self): + data = self._read_bytes(4) + return _struct.unpack(">i", data)[0] + + def readLine(self): + result = [] + while True: + ch = self.read() + if ch == -1: + break + if ch == 0x0A: + break + if ch == 0x0D: + next_ch = self.read() + if next_ch != 0x0A: + self.seek(self.getFilePointer() - 1) + break + result.append(chr(ch)) + return "".join(result) if result else None + + def readLong(self): + data = self._read_bytes(8) + return _struct.unpack(">q", data)[0] + + def readShort(self): + data = self._read_bytes(2) + return _struct.unpack(">h", data)[0] + + def readUnsignedByte(self): + v = self.read() + if v == -1: + raise EOFException() + return v & 0xFF + + def readUnsignedShort(self): + hi = self.read() + lo = self.read() + if hi == -1 or lo == -1: + raise EOFException() + return ((hi & 0xFF) << 8) | (lo & 0xFF) + + def readUTF(self): + length = self.readUnsignedShort() + data = self._read_bytes(length) + return data.decode("utf-8") + + def write(self, b=None, off=None, length=None): + if b is None: + return + if isinstance(b, int): + self._file.write(bytes([b & 0xFF])) + else: + if off is not None and length is not None: + self._file.write(bytes(b[off:off+length])) + else: + self._file.write(bytes(b)) + + def writeBoolean(self, v): + self.write(int(v)) + + def writeByte(self, v): + self.write(v & 0xFF) + + def writeBytes(self, s): + for ch in str(s): + self.write(ord(ch) & 0xFF) + + def writeChar(self, v): + self.write((v >> 8) & 0xFF) + self.write(v & 0xFF) + + def writeChars(self, s): + for ch in str(s): + self.writeChar(ord(ch)) + + def writeDouble(self, v): + data = _struct.pack(">d", v) + self._file.write(data) + + def writeFloat(self, v): + data = _struct.pack(">f", v) + self._file.write(data) + + def writeInt(self, v): + data = _struct.pack(">i", v) + self._file.write(data) + + def writeLong(self, v): + data = _struct.pack(">q", v) + self._file.write(data) + + def writeShort(self, v): + data = _struct.pack(">h", v) + self._file.write(data) + + def writeUTF(self, s): + encoded = str(s).encode("utf-8") + self.writeShort(len(encoded)) + self._file.write(encoded) + + def _read_bytes(self, n): + data = self._file.read(n) + if len(data) < n: + raise EOFException() + return data + + def setLength(self, new_length): + pos = self.getFilePointer() + self._file.truncate(new_length) + if pos > new_length: + self.seek(new_length) + + def close(self): + if not self._closed: + self._closed = True + self._file.close() + + def getChannel(self): + return self._file + + def flush(self): + self._file.flush() + + def __del__(self): + self.close() \ No newline at end of file diff --git a/android_pyapi/java/lang.py b/android_pyapi/java/lang.py new file mode 100644 index 0000000..59d746e --- /dev/null +++ b/android_pyapi/java/lang.py @@ -0,0 +1,1452 @@ +""" +Java lang standard library APIs implemented in Python. +java.lang package - String, Integer, Math, Thread, System, Object, etc. +""" + +from .._core import Stub, AndroidOnlyError +import re as _re +import math as _py_math +import sys as _sys +import os as _os +import time as _time +import threading as _threading +import inspect as _inspect +import math +import random +import time +from datetime import datetime as _datetime + +__all__ = [ + "Object", "Throwable", "Exception", "RuntimeException", "Error", + "String", "Integer", "Long", "Short", "Byte", "Float", "Double", + "Boolean", "Character", "Math", "StrictMath", "System", + "Thread", "ThreadLocal", "StringBuilder", "StringBuffer", + "Class", "Void", "VoidType", + "Stub", "Comparable", "CharSequence", "Iterable", "Appendable", + "Readable", "AutoCloseable", "Cloneable", "Runnable", + "StackTraceElement", "Number", "SuppressWarnings", "Deprecated", + "Override", "FunctionalInterface", +] + + +# ============================================================ +# Interfaces +# ============================================================ + +class Comparable(Stub): + def compareTo(self, other): + raise NotImplementedError + +class Iterable(Stub): + def iterator(self): + raise NotImplementedError + +class AutoCloseable(Stub): + def close(self): + raise NotImplementedError + +class Cloneable(Stub): + def clone(self): + return self.__class__() + +class CharSequence(Stub): + def length(self): + raise NotImplementedError + def charAt(self, index): + raise NotImplementedError + def subSequence(self, start, end): + raise NotImplementedError + def toString(self): + raise NotImplementedError + +class Appendable(Stub): + def append(self, c): + raise NotImplementedError + +class Readable(Stub): + def read(self, cb): + raise NotImplementedError + +class Runnable(Stub): + def run(self): + pass + + +# ============================================================ +# Throwable hierarchy +# ============================================================ + +class Throwable(Exception, Stub): + def __init__(self, message=None, cause=None): + super().__init__(message or "") + self._detail_message = str(message) if message else None + self._cause = cause + self._stack_trace = [] + self._suppressed = [] + self._fill_in_stack() + + def _fill_in_stack(self): + import traceback + stack = traceback.extract_stack() + self._stack_trace = [ + StackTraceElement( + frame.filename, frame.name, frame.filename, frame.lineno, False + ) + for frame in stack[:-1] + ] + + def getMessage(self): + return self._detail_message + + def getLocalizedMessage(self): + return self._detail_message + + def getCause(self): + return self._cause + + def initCause(self, cause): + self._cause = cause + return self + + def toString(self): + msg = self._detail_message or "" + return f"{self.__class__.__name__}: {msg}" if msg else self.__class__.__name__ + + def printStackTrace(self, file=None): + import traceback + if file is None: + file = _sys.stderr + traceback.print_exc(file=file) + + def fillInStackTrace(self): + self._fill_in_stack() + return self + + def getStackTrace(self): + return self._stack_trace + + def setStackTrace(self, stack_trace): + self._stack_trace = stack_trace + + def addSuppressed(self, exception): + self._suppressed.append(exception) + + def getSuppressed(self): + return self._suppressed + + def __str__(self): + return self.toString() + + +class Exception(Throwable): + pass + +class RuntimeException(Exception): + pass + +class Error(Throwable): + pass + + +# ============================================================ +# StackTraceElement +# ============================================================ + +class StackTraceElement(Stub): + def __init__(self, declaring_class, method_name, file_name, line_number, native_method): + super().__init__() + self._class = declaring_class + self._method = method_name + self._file = file_name + self._line = line_number + self._native = native_method + + def getClassName(self): + return self._class + + def getMethodName(self): + return self._method + + def getFileName(self): + return self._file + + def getLineNumber(self): + return self._line + + def isNativeMethod(self): + return self._native + + def toString(self): + return f"{self._class}.{self._method}({self._file}:{self._line})" + + +# ============================================================ +# Object +# ============================================================ + +class Object(Stub): + def __init__(self): + super().__init__() + + def getClass(self): + return self.__class__ + + def hashCode(self): + return hash(self) & 0x7FFFFFFF + + def equals(self, other): + return self is other + + def toString(self): + return f"{self.__class__.__name__}@{hex(id(self))}" + + def notify(self): + pass + + def notifyAll(self): + pass + + def wait(self, timeout=None): + pass + + def clone(self): + return self.__class__() + + def finalize(self): + pass + + +# ============================================================ +# String +# ============================================================ + +class String(CharSequence): + def __init__(self, value=None): + super().__init__() + if value is None: + self._str = "" + elif isinstance(value, bytes): + self._str = value.decode("utf-8", errors="replace") + else: + self._str = str(value) + + def charAt(self, index): + return self._str[index] + + def codePointAt(self, index): + return ord(self._str[index]) + + def codePointBefore(self, index): + return ord(self._str[index - 1]) if index > 0 else -1 + + def codePointCount(self, beginIndex, endIndex): + return endIndex - beginIndex + + def compareTo(self, other): + if isinstance(other, String): + return (self._str > other._str) - (self._str < other._str) + return -1 + + def compareToIgnoreCase(self, other): + if isinstance(other, String): + return (self._str.lower() > other._str.lower()) - (self._str.lower() < other._str.lower()) + return -1 + + def concat(self, s): + return String(self._str + str(s)) + + def contains(self, s): + return str(s) in self._str + + def contentEquals(self, cs): + return self._str == str(cs) + + def endsWith(self, suffix): + return self._str.endswith(suffix) + + def equals(self, other): + if isinstance(other, String): + return self._str == other._str + return False + + def equalsIgnoreCase(self, other): + if isinstance(other, String): + return self._str.lower() == other._str.lower() + return False + + def getBytes(self, charset="UTF-8"): + return self._str.encode(charset, errors="replace") + + def hashCode(self): + h = 0 + for c in self._str: + h = (31 * h + ord(c)) & 0xFFFFFFFF + return h if h < 0x80000000 else h - 0x100000000 + return h & 0x7FFFFFFF + + def indexOf(self, ch, fromIndex=0): + if isinstance(ch, int): + ch = chr(ch) + return self._str.index(str(ch), fromIndex) if str(ch) in self._str[fromIndex:] else -1 + + def intern(self): + return self + + def isEmpty(self): + return len(self._str) == 0 + + def lastIndexOf(self, ch, fromIndex=None): + if isinstance(ch, int): + ch = chr(ch) + if fromIndex is None: + fromIndex = len(self._str) - 1 + search_in = self._str[:fromIndex + 1] + return search_in.rfind(str(ch)) if str(ch) in search_in else -1 + + def length(self): + return len(self._str) + + def matches(self, regex): + return bool(_re.fullmatch(regex, self._str)) + + def offsetByCodePoints(self, index, codePointOffset): + return index + codePointOffset + + def regionMatches(self, ignoreCase, toffset, other, ooffset, length): + if ignoreCase: + return self._str[toffset:toffset+length].lower() == other._str[ooffset:ooffset+length].lower() + return self._str[toffset:toffset+length] == other._str[ooffset:ooffset+length] + + def replace(self, oldChar, newChar): + if isinstance(oldChar, int): + oldChar = chr(oldChar) + if isinstance(newChar, int): + newChar = chr(newChar) + return String(self._str.replace(oldChar, newChar)) + + def replaceAll(self, regex, replacement): + return String(_re.sub(regex, replacement, self._str)) + + def replaceFirst(self, regex, replacement): + return String(_re.sub(regex, replacement, self._str, count=1)) + + def split(self, regex, limit=0): + if limit <= 0: + parts = _re.split(regex, self._str) + if limit == 0: + while parts and parts[-1] == "": + parts.pop() + else: + parts = _re.split(regex, self._str, maxsplit=limit - 1) + while len(parts) < limit: + parts.append("") + return [String(p) for p in parts] + + def startsWith(self, prefix): + return self._str.startswith(prefix) + + def substring(self, beginIndex, endIndex=None): + if endIndex is None: + return String(self._str[beginIndex:]) + return String(self._str[beginIndex:endIndex]) + + def toCharArray(self): + return list(self._str) + + def toLowerCase(self): + return String(self._str.lower()) + + def toUpperCase(self): + return String(self._str.upper()) + + def trim(self): + return String(self._str.strip()) + + @staticmethod + def valueOf(obj): + return String(str(obj)) + + @staticmethod + def copyValueOf(data, offset=0, count=None): + if count is None: + return String("".join(data)) + return String("".join(data[offset:offset+count])) + + @staticmethod + def format(fmt, *args): + return String(str(fmt) % args if args else str(fmt)) + + @staticmethod + def join(delimiter, *elements): + if hasattr(elements[0], '__iter__') and not isinstance(elements[0], (str, bytes)): + elements = elements[0] + return String(str(delimiter).join(str(s) for s in elements)) + + def toString(self): + return self._str + + def __str__(self): + return self._str + + def __repr__(self): + return f"String({self._str!r})" + + def __len__(self): + return len(self._str) + + def __getitem__(self, idx): + return self._str[idx] + + def __add__(self, other): + return String(self._str + str(other)) + + def __eq__(self, other): + if isinstance(other, String): + return self._str == other._str + return NotImplemented + + +# ============================================================ +# Number +# ============================================================ + +class Number(Stub): + def intValue(self): + raise NotImplementedError + def longValue(self): + raise NotImplementedError + def floatValue(self): + raise NotImplementedError + def doubleValue(self): + raise NotImplementedError + def byteValue(self): + return self.intValue() & 0xFF + def shortValue(self): + return self.intValue() & 0xFFFF + + +# ============================================================ +# Integer +# ============================================================ + +class Integer(Number, Comparable): + MAX_VALUE = 2147483647 + MIN_VALUE = -2147483648 + SIZE = 32 + BYTES = 4 + TYPE = int + + def __init__(self, value=0): + super().__init__() + if isinstance(value, str): + self._value = Integer.parseInt(value) + else: + self._value = int(value) + + @staticmethod + def parseInt(s, radix=10): + if isinstance(s, String): + s = str(s) + if radix == 10: + return int(str(s)) + return int(str(s), radix) + + @staticmethod + def toString(i, radix=10): + if radix == 10: + return String(str(i)) + if radix == 2: + return String(bin(i)[2:]) + if radix == 8: + return String(oct(i)[2:]) + if radix == 16: + return String(hex(i)[2:]) + return String(str(i)) + + @staticmethod + def toBinaryString(i): + if i >= 0: + return String(bin(i)[2:]) + return String(bin(i & 0xFFFFFFFF)[2:]) + + @staticmethod + def toHexString(i): + if i >= 0: + return String(hex(i)[2:]) + return String(hex(i & 0xFFFFFFFF)[2:]) + + @staticmethod + def toOctalString(i): + if i >= 0: + return String(oct(i)[2:]) + return String(oct(i & 0xFFFFFFFF)[2:]) + + @staticmethod + def valueOf(i, radix=10): + if isinstance(i, str): + return Integer(Integer.parseInt(i, radix)) + return Integer(i) + + def intValue(self): + return self._value + + def compareTo(self, other): + if isinstance(other, Integer): + return (self._value > other._value) - (self._value < other._value) + return -1 + + def equals(self, other): + if isinstance(other, Integer): + return self._value == other._value + return False + + def byteValue(self): + return self._value & 0xFF + + def shortValue(self): + return self._value & 0xFFFF + + def longValue(self): + return self._value + + def floatValue(self): + return float(self._value) + + def doubleValue(self): + return float(self._value) + + @staticmethod + def bitCount(i): + return bin(i & 0xFFFFFFFF).count("1") + + @staticmethod + def highestOneBit(i): + if i == 0: + return 0 + return 1 << (i.bit_length() - 1) + + @staticmethod + def lowestOneBit(i): + return i & -i + + @staticmethod + def numberOfLeadingZeros(i): + if i == 0: + return 32 + return 32 - i.bit_length() + + @staticmethod + def numberOfTrailingZeros(i): + if i == 0: + return 32 + return (i & -i).bit_length() - 1 + + @staticmethod + def reverse(i): + v = i & 0xFFFFFFFF + result = 0 + for _ in range(32): + result = (result << 1) | (v & 1) + v >>= 1 + return result + + @staticmethod + def reverseBytes(i): + v = i & 0xFFFFFFFF + return ((v >> 24) & 0xFF) | ((v >> 8) & 0xFF00) | ((v << 8) & 0xFF0000) | ((v << 24) & 0xFF000000) + + @staticmethod + def rotateLeft(i, distance): + v = i & 0xFFFFFFFF + d = distance & 31 + return ((v << d) | (v >> (32 - d))) & 0xFFFFFFFF + + @staticmethod + def rotateRight(i, distance): + v = i & 0xFFFFFFFF + d = distance & 31 + return ((v >> d) | (v << (32 - d))) & 0xFFFFFFFF + + @staticmethod + def signum(i): + if i > 0: + return 1 + if i < 0: + return -1 + return 0 + + @staticmethod + def decode(nm): + if nm.startswith("0x") or nm.startswith("0X"): + return Integer(int(nm, 16)) + if nm.startswith("0") and len(nm) > 1: + return Integer(int(nm, 8)) + if nm.startswith("#"): + return Integer(int(nm[1:], 16)) + return Integer(int(nm)) + + @staticmethod + def getInteger(nm, val=None): + env_val = _os.environ.get(nm) + if env_val is not None: + return Integer(int(env_val)) + return Integer(val) if val is not None else None + + def toString(self): + return String(str(self._value)) + + def __str__(self): + return str(self._value) + + def __int__(self): + return self._value + + +# ============================================================ +# Long +# ============================================================ + +class Long(Number, Comparable): + MAX_VALUE = 9223372036854775807 + MIN_VALUE = -9223372036854775808 + SIZE = 64 + BYTES = 8 + TYPE = int + + def __init__(self, value=0): + super().__init__() + if isinstance(value, str): + self._value = Long.parseLong(value) + else: + self._value = int(value) + + @staticmethod + def parseLong(s, radix=10): + if isinstance(s, String): + s = str(s) + if radix == 10: + return int(str(s)) + return int(str(s), radix) + + @staticmethod + def toString(i, radix=10): + if radix == 10: + return String(str(i)) + return String(str(i)) + + @staticmethod + def toBinaryString(i): + if i >= 0: + return String(bin(i)[2:]) + return String(bin(i & 0xFFFFFFFFFFFFFFFF)[2:]) + + @staticmethod + def toHexString(i): + if i >= 0: + return String(hex(i)[2:]) + return String(hex(i & 0xFFFFFFFFFFFFFFFF)[2:]) + + @staticmethod + def toOctalString(i): + if i >= 0: + return String(oct(i)[2:]) + return String(oct(i & 0xFFFFFFFFFFFFFFFF)[2:]) + + @staticmethod + def valueOf(i, radix=10): + if isinstance(i, str): + return Long(Long.parseLong(i, radix)) + return Long(i) + + def longValue(self): + return self._value + + def intValue(self): + return int(self._value) + + def floatValue(self): + return float(self._value) + + def doubleValue(self): + return float(self._value) + + def compareTo(self, other): + if isinstance(other, Long): + return (self._value > other._value) - (self._value < other._value) + return -1 + + def equals(self, other): + if isinstance(other, Long): + return self._value == other._value + return False + + def toString(self): + return String(str(self._value)) + + def __str__(self): + return str(self._value) + + +# ============================================================ +# Short, Byte +# ============================================================ + +class Short(Number, Comparable): + MAX_VALUE = 32767 + MIN_VALUE = -32768 + SIZE = 16 + BYTES = 2 + TYPE = int + + def __init__(self, value=0): + super().__init__() + if isinstance(value, str): + self._value = Short.parseShort(value) + else: + self._value = int(value) & 0xFFFF + + @staticmethod + def parseShort(s, radix=10): + return int(str(s), radix) + + @staticmethod + def valueOf(i, radix=10): + if isinstance(i, str): + return Short(Short.parseShort(i, radix)) + return Short(i) + + def shortValue(self): + return self._value + + def intValue(self): + return self._value + + def longValue(self): + return self._value + + def floatValue(self): + return float(self._value) + + def doubleValue(self): + return float(self._value) + + def compareTo(self, other): + if isinstance(other, Short): + return (self._value > other._value) - (self._value < other._value) + return -1 + + def equals(self, other): + if isinstance(other, Short): + return self._value == other._value + return False + + def toString(self): + return String(str(self._value)) + + +class Byte(Number, Comparable): + MAX_VALUE = 127 + MIN_VALUE = -128 + SIZE = 8 + BYTES = 1 + TYPE = int + + def __init__(self, value=0): + super().__init__() + if isinstance(value, str): + self._value = Byte.parseByte(value) + else: + self._value = int(value) & 0xFF + if self._value > 127: + self._value -= 256 + + @staticmethod + def parseByte(s, radix=10): + return int(str(s), radix) + + @staticmethod + def valueOf(i, radix=10): + if isinstance(i, str): + return Byte(Byte.parseByte(i, radix)) + return Byte(i) + + def byteValue(self): + return self._value + + def intValue(self): + return self._value + + def longValue(self): + return self._value + + def compareTo(self, other): + if isinstance(other, Byte): + return (self._value > other._value) - (self._value < other._value) + return -1 + + def equals(self, other): + if isinstance(other, Byte): + return self._value == other._value + return False + + def toString(self): + return String(str(self._value)) + + +# ============================================================ +# Float +# ============================================================ + +class Float(Number, Comparable): + MAX_VALUE = 3.4028235e38 + MIN_VALUE = 1.4e-45 + MIN_NORMAL = 1.17549435e-38 + MAX_EXPONENT = 127 + MIN_EXPONENT = -126 + SIZE = 32 + BYTES = 4 + TYPE = float + NaN = float("nan") + POSITIVE_INFINITY = float("inf") + NEGATIVE_INFINITY = float("-inf") + + def __init__(self, value=0.0): + super().__init__() + if isinstance(value, str): + self._value = Float.parseFloat(value) + else: + self._value = float(value) + + @staticmethod + def parseFloat(s): + if isinstance(s, String): + s = str(s) + s = str(s).strip() + if s == "NaN": + return float("nan") + if s == "Infinity" or s == "Inf": + return float("inf") + if s == "-Infinity" or s == "-Inf": + return float("-inf") + return float(s) + + @staticmethod + def toString(f): + return String(str(f)) + + @staticmethod + def valueOf(f): + if isinstance(f, str): + return Float(Float.parseFloat(f)) + return Float(f) + + def floatValue(self): + return self._value + + def intValue(self): + return int(self._value) + + def longValue(self): + return int(self._value) + + def doubleValue(self): + return float(self._value) + + def compareTo(self, other): + if isinstance(other, Float): + a, b = self._value, other._value + if _py_math.isnan(a) and _py_math.isnan(b): + return 0 + if _py_math.isnan(a): + return 1 + if _py_math.isnan(b): + return -1 + return (a > b) - (a < b) + return -1 + + def isNaN(self): + return _py_math.isnan(self._value) + + def isInfinite(self): + return _py_math.isinf(self._value) + + def equals(self, other): + if isinstance(other, Float): + if _py_math.isnan(self._value) and _py_math.isnan(other._value): + return True + return self._value == other._value + return False + + def hashCode(self): + return _struct.pack(">f", self._value) + + def toString(self): + return String(str(self._value)) + + def __str__(self): + return str(self._value) + +import struct as _struct + + +# ============================================================ +# Double +# ============================================================ + +class Double(Number, Comparable): + MAX_VALUE = 1.7976931348623157e308 + MIN_VALUE = 4.9e-324 + MIN_NORMAL = 2.2250738585072014e-308 + MAX_EXPONENT = 1023 + MIN_EXPONENT = -1022 + SIZE = 64 + BYTES = 8 + TYPE = float + NaN = float("nan") + POSITIVE_INFINITY = float("inf") + NEGATIVE_INFINITY = float("-inf") + + def __init__(self, value=0.0): + super().__init__() + if isinstance(value, str): + self._value = Double.parseDouble(value) + else: + self._value = float(value) + + @staticmethod + def parseDouble(s): + if isinstance(s, String): + s = str(s) + s = str(s).strip() + if s == "NaN": + return float("nan") + if s == "Infinity" or s == "Inf": + return float("inf") + if s == "-Infinity" or s == "-Inf": + return float("-inf") + return float(s) + + @staticmethod + def toString(d): + return String(str(d)) + + @staticmethod + def valueOf(d): + if isinstance(d, str): + return Double(Double.parseDouble(d)) + return Double(d) + + def doubleValue(self): + return self._value + + def intValue(self): + return int(self._value) + + def longValue(self): + return int(self._value) + + def floatValue(self): + return float(self._value) + + def compareTo(self, other): + if isinstance(other, Double): + a, b = self._value, other._value + if _py_math.isnan(a) and _py_math.isnan(b): + return 0 + if _py_math.isnan(a): + return 1 + if _py_math.isnan(b): + return -1 + return (a > b) - (a < b) + return -1 + + def isNaN(self): + return _py_math.isnan(self._value) + + def isInfinite(self): + return _py_math.isinf(self._value) + + def equals(self, other): + if isinstance(other, Double): + if _py_math.isnan(self._value) and _py_math.isnan(other._value): + return True + return self._value == other._value + return False + + def toString(self): + return String(str(self._value)) + + def __str__(self): + return str(self._value) + + +# ============================================================ +# Boolean +# ============================================================ + +class Boolean(Stub): + TRUE = "true" + FALSE = "false" + TYPE = bool + + def __init__(self, value=False): + super().__init__() + if isinstance(value, str): + self._value = Boolean.parseBoolean(value) + elif isinstance(value, bool): + self._value = value + else: + self._value = bool(value) + + @staticmethod + def parseBoolean(s): + return str(s).lower() == "true" + + @staticmethod + def toString(b): + return String(str(b).lower()) + + @staticmethod + def valueOf(b): + if isinstance(b, str): + return Boolean(Boolean.parseBoolean(b)) + return Boolean(b) + + def booleanValue(self): + return self._value + + @staticmethod + def logicalAnd(a, b): + return a and b + + @staticmethod + def logicalOr(a, b): + return a or b + + @staticmethod + def logicalXor(a, b): + return a ^ b + + def compareTo(self, other): + if isinstance(other, Boolean): + return (self._value > other._value) - (self._value < other._value) + return -1 + + def equals(self, other): + if isinstance(other, Boolean): + return self._value == other._value + return False + + def toString(self): + return String("true" if self._value else "false") + + def __str__(self): + return "true" if self._value else "false" + + def __bool__(self): + return self._value + + +# ============================================================ +# Character +# ============================================================ + +class Character(Stub): + MAX_VALUE = 65535 + MIN_VALUE = 0 + SIZE = 16 + BYTES = 2 + TYPE = str + + def __init__(self, value=""): + super().__init__() + if isinstance(value, int): + self._value = chr(value) + else: + self._value = str(value)[0] if value else "" + + def charValue(self): + return self._value + + @staticmethod + def isDigit(ch): + return str(ch).isdigit() + + @staticmethod + def isLetter(ch): + return str(ch).isalpha() + + @staticmethod + def isLetterOrDigit(ch): + return str(ch).isalnum() + + @staticmethod + def isLowerCase(ch): + return str(ch).islower() + + @staticmethod + def isUpperCase(ch): + return str(ch).isupper() + + @staticmethod + def isWhitespace(ch): + return str(ch).isspace() + + @staticmethod + def isSpaceChar(ch): + return str(ch) in " \t\n\r\f\u000B" + + @staticmethod + def isISOControl(ch): + c = ord(ch) if isinstance(ch, str) else ch + return c < 0x20 or c == 0x7F + + @staticmethod + def isJavaIdentifierPart(ch): + c = str(ch) + return c.isalnum() or c == "_" or c == "$" + + @staticmethod + def isJavaIdentifierStart(ch): + c = str(ch) + return c.isalpha() or c == "_" or c == "$" + + @staticmethod + def isJavaLetter(ch): + return Character.isJavaIdentifierStart(ch) + + @staticmethod + def isJavaLetterOrDigit(ch): + return Character.isJavaIdentifierPart(ch) + + @staticmethod + def toLowerCase(ch): + return str(ch).lower() + + @staticmethod + def toUpperCase(ch): + return str(ch).upper() + + @staticmethod + def toTitleCase(ch): + return str(ch).upper() + + @staticmethod + def getNumericValue(ch): + s = str(ch) + if s.isdigit(): + return int(s) + if "a" <= s <= "z": + return ord(s) - ord("a") + 10 + if "A" <= s <= "Z": + return ord(s) - ord("A") + 10 + return -1 + + @staticmethod + def digit(ch, radix): + val = Character.getNumericValue(ch) + return val if val < radix else -1 + + @staticmethod + def forDigit(digit, radix): + if digit >= radix: + return "\0" + return str(digit) if digit < 10 else chr(ord("a") + digit - 10) + + @staticmethod + def getType(ch): + return 0 + + @staticmethod + def getDirectionality(ch): + return 0 + + @staticmethod + def getName(ch): + return f"CHARACTER {ord(ch)}" + + @staticmethod + def charCount(codePoint): + return 1 if codePoint < 0x10000 else 2 + + @staticmethod + def isBmpCodePoint(cp): + return cp <= 0xFFFF + + @staticmethod + def isHighSurrogate(ch): + c = ord(ch) if isinstance(ch, str) else ch + return 0xD800 <= c <= 0xDBFF + + @staticmethod + def isLowSurrogate(ch): + c = ord(ch) if isinstance(ch, str) else ch + return 0xDC00 <= c <= 0xDFFF + + @staticmethod + def isSupplementaryCodePoint(cp): + return 0x10000 + + +class Math: + """java.lang.Math — 数学运算""" + E = 2.718281828459045 + PI = 3.141592653589793 + + @staticmethod + def abs(a): return abs(a) + @staticmethod + def ceil(a): return int(math.ceil(a)) + @staticmethod + def floor(a): return int(math.floor(a)) + @staticmethod + def round(a): return round(a) + @staticmethod + def min(a, b): return a if a < b else b + @staticmethod + def max(a, b): return a if a > b else b + @staticmethod + def sqrt(a): return math.sqrt(a) + @staticmethod + def cbrt(a): return math.cbrt(a) + @staticmethod + def pow(a, b): return math.pow(a, b) + @staticmethod + def exp(a): return math.exp(a) + @staticmethod + def log(a): return math.log(a) + @staticmethod + def log10(a): return math.log10(a) + @staticmethod + def sin(a): return math.sin(a) + @staticmethod + def cos(a): return math.cos(a) + @staticmethod + def tan(a): return math.tan(a) + @staticmethod + def asin(a): return math.asin(a) + @staticmethod + def acos(a): return math.acos(a) + @staticmethod + def atan(a): return math.atan(a) + @staticmethod + def atan2(y, x): return math.atan2(y, x) + @staticmethod + def toDegrees(r): return math.degrees(r) + @staticmethod + def toRadians(d): return math.radians(d) + @staticmethod + def hypot(x, y): return math.hypot(x, y) + @staticmethod + def sinh(x): return math.sinh(x) + @staticmethod + def cosh(x): return math.cosh(x) + @staticmethod + def tanh(x): return math.tanh(x) + @staticmethod + def signum(x): return 0.0 if x == 0 else (1.0 if x > 0 else -1.0) + @staticmethod + def random(): return random.random() + @staticmethod + def addExact(x, y): + result = x + y + if (x > 0 and y > 0 and result < 0) or (x < 0 and y < 0 and result > 0): + raise ArithmeticError("integer overflow") + return result + @staticmethod + def copySign(m, s): return abs(m) * (1 if s >= 0 else -1) + @staticmethod + def floorDiv(x, y): return x // y if (x ^ y) >= 0 else -(abs(x) // abs(y)) + @staticmethod + def floorMod(x, y): return x - Math.floorDiv(x, y) * y + + +class StrictMath(Math): + """java.lang.StrictMath""" + pass + + +class System: + """java.lang.System""" + @staticmethod + def currentTimeMillis(): return int(time.time() * 1000) + @staticmethod + def nanoTime(): return time.perf_counter_ns() + @staticmethod + def arraycopy(src, srcPos, dest, destPos, length): + if isinstance(src, (list, bytearray)): + dest[destPos:destPos+length] = src[srcPos:srcPos+length] + else: + raise TypeError("Unsupported type for arraycopy") + @staticmethod + def identityHashCode(obj): return id(obj) & 0x7FFFFFFF + @staticmethod + def gc(): pass + @staticmethod + def exit(status): raise SystemExit(status) + @staticmethod + def getProperty(key, default=None): + import os + return os.environ.get(key, default) + @staticmethod + def setProperty(key, value): + import os + os.environ[key] = value + return value + @staticmethod + def lineSeparator(): return _os.linesep + in_ = _sys.stdin + out = _sys.stdout + err = _sys.stderr + + +class Thread(Runnable): + """java.lang.Thread""" + MIN_PRIORITY = 1 + NORM_PRIORITY = 5 + MAX_PRIORITY = 10 + + def __init__(self, target=None, name=None): + super().__init__() + self._target = target + self._name = name or f"Thread-{id(self)}" + self._alive = False + self._daemon = False + self._interrupted = False + + def start(self): + self._alive = True + if self._target: + _threading.Thread(target=self.run, daemon=self._daemon).start() + + def run(self): + if self._target: + self._target() + + def interrupt(self): + self._interrupted = True + + def isInterrupted(self): + return self._interrupted + + def isAlive(self): + return self._alive + + def getName(self): return self._name + def setName(self, n): self._name = n + def setDaemon(self, d): self._daemon = d + def isDaemon(self): return self._daemon + + @staticmethod + def currentThread(): + return Thread(name="main") + @staticmethod + def sleep(millis): + import time + time.sleep(millis / 1000.0) + @staticmethod + def yield_(): + import time + time.sleep(0) + + +class ThreadLocal(Stub): + """java.lang.ThreadLocal""" + def __init__(self): + super().__init__() + self._data = {} + + def get(self): + tid = _threading.get_ident() + return self._data.get(tid, None) + def set(self, value): + self._data[_threading.get_ident()] = value + def remove(self): + self._data.pop(_threading.get_ident(), None) + + +class StringBuilder(Stub): + """java.lang.StringBuilder — 可变字符序列""" + def __init__(self, s=""): + super().__init__() + self._buf = list(str(s)) + + def append(self, obj): + if isinstance(obj, bool): + self._buf.append("true" if obj else "false") + else: + self._buf.append(str(obj)) + return self + + def insert(self, offset, obj): + self._buf[offset:offset] = list(str(obj)) + return self + + def delete(self, start, end): + del self._buf[start:end] + return self + + def deleteCharAt(self, index): + del self._buf[index] + return self + + def replace(self, start, end, s): + self._buf[start:end] = list(s) + return self + + def reverse(self): + self._buf.reverse() + return self + + def toString(self): return "".join(self._buf) + def length(self): return len(self._buf) + def capacity(self): return len(self._buf) + 16 + def charAt(self, i): return self._buf[i] + def setCharAt(self, i, c): self._buf[i] = c + def substring(self, s, e=None): + return "".join(self._buf[s:e]) + def indexOf(self, s, f=0): + return self.toString().find(s, f) + def lastIndexOf(self, s, f=None): + return self.toString().rfind(s) + + +class StringBuffer(StringBuilder): + """java.lang.StringBuffer — 线程安全的可变字符序列""" + pass + + +class Class(Stub): + """java.lang.Class""" + def __init__(self, cls): + super().__init__() + self._cls = cls + def getName(self): return self._cls.__name__ + def getSimpleName(self): return self._cls.__name__ + def getCanonicalName(self): return self._cls.__name__ + def isInterface(self): return isinstance(self._cls, type) + def isArray(self): return self._cls is list + def isPrimitive(self): return self._cls in (int, float, bool, str, bytes) + def getSuperclass(self): return Class(self._cls.__base__) if self._cls.__base__ else None + def cast(self, obj): return obj + + +class Void(Stub): + """java.lang.Void""" + TYPE = None + + +class VoidType(Stub): + """java.lang.Void (inner type)""" + pass + + +# 注解类 +class SuppressWarnings(Stub): + pass +class Deprecated(Stub): + pass +class Override(Stub): + pass +class FunctionalInterface(Stub): + pass \ No newline at end of file diff --git a/android_pyapi/java/math.py b/android_pyapi/java/math.py new file mode 100644 index 0000000..bdbf2c7 --- /dev/null +++ b/android_pyapi/java/math.py @@ -0,0 +1,683 @@ +""" +Java math standard library APIs implemented in Python. +java.math package - BigInteger, BigDecimal, MathContext, RoundingMode. +""" + +from .._core import Stub +from .lang import Comparable +from decimal import ( + Decimal as _Decimal, + ROUND_DOWN as _DEC_ROUND_DOWN, + ROUND_HALF_UP as _DEC_ROUND_HALF_UP, + ROUND_HALF_EVEN as _DEC_ROUND_HALF_EVEN, + ROUND_UP as _DEC_ROUND_UP, + ROUND_CEILING as _DEC_ROUND_CEILING, + ROUND_FLOOR as _DEC_ROUND_FLOOR, + ROUND_HALF_DOWN as _DEC_ROUND_HALF_DOWN, + localcontext as _local_context, + getcontext as _get_context, +) +import random as _random +import math as _py_math +import sys as _sys + +__all__ = [ + "BigInteger", "BigDecimal", "MathContext", "RoundingMode", +] + + +# ============================================================ +# RoundingMode +# ============================================================ + +class RoundingMode(Stub): + UP = 0 + DOWN = 1 + CEILING = 2 + FLOOR = 3 + HALF_UP = 4 + HALF_DOWN = 5 + HALF_EVEN = 6 + UNNECESSARY = 7 + + _names = { + 0: "UP", 1: "DOWN", 2: "CEILING", 3: "FLOOR", + 4: "HALF_UP", 5: "HALF_DOWN", 6: "HALF_EVEN", 7: "UNNECESSARY", + } + + _py_map = { + 0: _DEC_ROUND_UP, + 1: _DEC_ROUND_DOWN, + 2: _DEC_ROUND_CEILING, + 3: _DEC_ROUND_FLOOR, + 4: _DEC_ROUND_HALF_UP, + 5: _DEC_ROUND_HALF_DOWN, + 6: _DEC_ROUND_HALF_EVEN, + 7: _DEC_ROUND_HALF_EVEN, # closest approximation + } + + def __init__(self, value): + super().__init__() + self._value = value + + def toString(self): + return self._names.get(self._value, "UNKNOWN") + + +# ============================================================ +# MathContext +# ============================================================ + +class MathContext(Stub): + UNLIMITED = None + DECIMAL32 = None + DECIMAL64 = None + DECIMAL128 = None + + def __init__(self, *args): + super().__init__() + if len(args) == 1: + if isinstance(args[0], str): + s = args[0].strip().lower() + if s == "precision=7 roundingmode=half_even": + self._precision = 7 + self._rounding_mode = RoundingMode.HALF_EVEN + elif s == "precision=16 roundingmode=half_even": + self._precision = 16 + self._rounding_mode = RoundingMode.HALF_EVEN + elif s == "precision=34 roundingmode=half_even": + self._precision = 34 + self._rounding_mode = RoundingMode.HALF_EVEN + else: + self._precision = 0 + self._rounding_mode = RoundingMode.HALF_UP + else: + self._precision = int(args[0]) + self._rounding_mode = RoundingMode.HALF_UP + elif len(args) == 2: + self._precision = int(args[0]) + self._rounding_mode = args[1] + + def getPrecision(self): + return self._precision + + def getRoundingMode(self): + return self._rounding_mode + + def equals(self, other): + if isinstance(other, MathContext): + return (self._precision == other._precision and + self._rounding_mode == other._rounding_mode) + return False + + def hashCode(self): + return hash((self._precision, self._rounding_mode)) & 0x7FFFFFFF + + def toString(self): + return f"precision={self._precision} roundingMode={self._rounding_mode.toString()}" + + +MathContext.UNLIMITED = MathContext(0) +MathContext.DECIMAL32 = MathContext(7, RoundingMode.HALF_EVEN) +MathContext.DECIMAL64 = MathContext(16, RoundingMode.HALF_EVEN) +MathContext.DECIMAL128 = MathContext(34, RoundingMode.HALF_EVEN) + + +# ============================================================ +# BigInteger +# ============================================================ + +class BigInteger(Comparable): + ZERO = None + ONE = None + TEN = None + + def __init__(self, *args): + super().__init__() + if len(args) == 0: + self._value = 0 + elif len(args) == 1: + if isinstance(args[0], str): + self._value = int(str(args[0])) + elif isinstance(args[0], bytes): + self._value = int.from_bytes(args[0], byteorder="big", signed=True) + elif isinstance(args[0], bytearray): + self._value = int.from_bytes(bytes(args[0]), byteorder="big", signed=True) + elif isinstance(args[0], int): + self._value = args[0] + elif isinstance(args[0], BigInteger): + self._value = args[0]._value + else: + self._value = int(args[0]) + elif len(args) == 2: + if isinstance(args[1], _random.Random): + bit_length, rng = args + self._value = rng.getrandbits(bit_length) + elif isinstance(args[1], int): + val_str, radix = args + self._value = int(str(val_str), radix) + else: + self._value = int.from_bytes(bytes(args[0]), byteorder="big", signed=True) + else: + self._value = 0 + + @staticmethod + def valueOf(val): + if isinstance(val, int): + if val == 0: + return BigInteger.ZERO + if val == 1: + return BigInteger.ONE + if val == 10: + return BigInteger.TEN + return BigInteger(val) + + def add(self, other): + if isinstance(other, BigInteger): + return BigInteger(self._value + other._value) + return BigInteger(self._value + int(other)) + + def subtract(self, other): + if isinstance(other, BigInteger): + return BigInteger(self._value - other._value) + return BigInteger(self._value - int(other)) + + def multiply(self, other): + if isinstance(other, BigInteger): + return BigInteger(self._value * other._value) + return BigInteger(self._value * int(other)) + + def divide(self, other): + if isinstance(other, BigInteger): + return BigInteger(self._value // other._value) + return BigInteger(self._value // int(other)) + + def pow(self, exponent): + return BigInteger(self._value ** exponent) + + def gcd(self, other): + return BigInteger(_py_math.gcd(self._value, other._value if isinstance(other, BigInteger) else int(other))) + + def abs(self): + return BigInteger(abs(self._value)) + + def negate(self): + return BigInteger(-self._value) + + def signum(self): + if self._value > 0: + return 1 + if self._value < 0: + return -1 + return 0 + + def mod(self, m): + if isinstance(m, BigInteger): + return BigInteger(self._value % m._value) + return BigInteger(self._value % int(m)) + + def remainder(self, other): + if isinstance(other, BigInteger): + return BigInteger(self._value % other._value) + return BigInteger(self._value % int(other)) + + def and_(self, other): + return BigInteger(self._value & (other._value if isinstance(other, BigInteger) else int(other))) + + def or_(self, other): + return BigInteger(self._value | (other._value if isinstance(other, BigInteger) else int(other))) + + def xor(self, other): + return BigInteger(self._value ^ (other._value if isinstance(other, BigInteger) else int(other))) + + def not_(self): + return BigInteger(~self._value) + + def andNot(self, other): + return BigInteger(self._value & ~(other._value if isinstance(other, BigInteger) else int(other))) + + def shiftLeft(self, n): + return BigInteger(self._value << n) + + def shiftRight(self, n): + return BigInteger(self._value >> n) + + def getLowestSetBit(self): + if self._value == 0: + return -1 + return (self._value & -self._value).bit_length() - 1 + + def bitLength(self): + if self._value == 0: + return 0 + return self._value.bit_length() + + def bitCount(self): + return bin(self._value & ((1 << self.bitLength()) - 1) if self._value >= 0 else self._value).count("1") + + def isProbablePrime(self, certainty): + if abs(self._value) < 2: + return False + if abs(self._value) == 2 or abs(self._value) == 3: + return True + if self._value % 2 == 0: + return False + d = self._value - 1 + s = 0 + while d % 2 == 0: + d //= 2 + s += 1 + for _ in range(min(certainty, 20)): + a = _random.randrange(2, min(self._value - 1, 1000000)) + x = pow(a, d, self._value) + if x == 1 or x == self._value - 1: + continue + for _ in range(s - 1): + x = pow(x, 2, self._value) + if x == self._value - 1: + break + else: + return False + return True + + def nextProbablePrime(self): + v = self._value + 1 + if v < 3: + return BigInteger(2) if v <= 2 else BigInteger(3) + while True: + if v % 2 == 0: + v += 1 + for i in range(3, int(_py_math.sqrt(v)) + 1, 2): + if v % i == 0: + break + else: + return BigInteger(v) + v += 2 + + def compareTo(self, other): + if isinstance(other, BigInteger): + return (self._value > other._value) - (self._value < other._value) + return -1 + + def equals(self, other): + if isinstance(other, BigInteger): + return self._value == other._value + return False + + def toByteArray(self): + if self._value == 0: + return b"\x00" + byte_length = (self._value.bit_length() + 8) // 8 + return self._value.to_bytes(byte_length, byteorder="big", signed=True) + + def toString(self, radix=10): + if radix == 10: + return str(self._value) + if radix == 16: + return hex(self._value)[2:] + if radix == 8: + return oct(self._value)[2:] + if radix == 2: + return bin(self._value)[2:] + return str(self._value) + + def intValue(self): + return self._value & 0xFFFFFFFF + + def longValue(self): + return self._value + + def floatValue(self): + return float(self._value) + + def doubleValue(self): + return float(self._value) + + def min(self, other): + return BigInteger(min(self._value, other._value if isinstance(other, BigInteger) else int(other))) + + def max(self, other): + return BigInteger(max(self._value, other._value if isinstance(other, BigInteger) else int(other))) + + def modPow(self, exponent, m): + if isinstance(exponent, BigInteger): + exp_val = exponent._value + else: + exp_val = int(exponent) + mod_val = m._value if isinstance(m, BigInteger) else int(m) + return BigInteger(pow(self._value, exp_val, mod_val)) + + def modInverse(self, m): + mod_val = m._value if isinstance(m, BigInteger) else int(m) + return BigInteger(pow(self._value, -1, mod_val)) + + def divideAndRemainder(self, other): + if isinstance(other, BigInteger): + divisor = other._value + else: + divisor = int(other) + q = self._value // divisor + r = self._value % divisor + return [BigInteger(q), BigInteger(r)] + + def __int__(self): + return self._value + + def __str__(self): + return str(self._value) + + def __neg__(self): + return self.negate() + + def __abs__(self): + return self.abs() + + def __add__(self, other): + return self.add(other) + + def __sub__(self, other): + return self.subtract(other) + + def __mul__(self, other): + return self.multiply(other) + + def __truediv__(self, other): + return self.divide(other) + + def __floordiv__(self, other): + return self.divide(other) + + def __mod__(self, other): + return self.mod(other) + + def __pow__(self, other): + return self.pow(other) + + def __and__(self, other): + return self.and_(other) + + def __or__(self, other): + return self.or_(other) + + def __xor__(self, other): + return self.xor(other) + + def __invert__(self): + return self.not_() + + def __lshift__(self, n): + return self.shiftLeft(n) + + def __rshift__(self, n): + return self.shiftRight(n) + + +BigInteger.ZERO = BigInteger(0) +BigInteger.ONE = BigInteger(1) +BigInteger.TEN = BigInteger(10) + + +# ============================================================ +# BigDecimal +# ============================================================ + +class BigDecimal(Comparable): + ZERO = None + ONE = None + TEN = None + + ROUND_UP = 0 + ROUND_DOWN = 1 + ROUND_CEILING = 2 + ROUND_FLOOR = 3 + ROUND_HALF_UP = 4 + ROUND_HALF_DOWN = 5 + ROUND_HALF_EVEN = 6 + ROUND_UNNECESSARY = 7 + + _round_map = { + 0: _DEC_ROUND_UP, + 1: _DEC_ROUND_DOWN, + 2: _DEC_ROUND_CEILING, + 3: _DEC_ROUND_FLOOR, + 4: _DEC_ROUND_HALF_UP, + 5: _DEC_ROUND_HALF_DOWN, + 6: _DEC_ROUND_HALF_EVEN, + 7: _DEC_ROUND_HALF_EVEN, + } + + def __init__(self, *args): + super().__init__() + if len(args) == 1: + val = args[0] + if isinstance(val, str): + self._dec = _Decimal(str(val)) + elif isinstance(val, float): + self._dec = _Decimal(repr(val)) + elif isinstance(val, int): + self._dec = _Decimal(val) + elif isinstance(val, BigInteger): + self._dec = _Decimal(val._value) + elif isinstance(val, BigDecimal): + self._dec = _Decimal(val._dec) + elif isinstance(val, _Decimal): + self._dec = val + else: + self._dec = _Decimal(str(val)) + elif len(args) == 2: + val, mc = args + if isinstance(val, str): + self._dec = _Decimal(str(val)) + elif isinstance(val, float): + self._dec = _Decimal(repr(val)) + else: + self._dec = _Decimal(val) + if mc and hasattr(mc, 'getPrecision') and mc.getPrecision() > 0: + self._dec = +self._dec + + @staticmethod + def valueOf(val): + if isinstance(val, int): + if val == 0: + return BigDecimal.ZERO + if val == 1: + return BigDecimal.ONE + if val == 10: + return BigDecimal.TEN + if isinstance(val, float): + return BigDecimal(repr(val)) + return BigDecimal(val) + + def add(self, other): + if isinstance(other, BigDecimal): + return BigDecimal(self._dec + other._dec) + return BigDecimal(self._dec + _Decimal(str(other))) + + def subtract(self, other): + if isinstance(other, BigDecimal): + return BigDecimal(self._dec - other._dec) + return BigDecimal(self._dec - _Decimal(str(other))) + + def multiply(self, other): + if isinstance(other, BigDecimal): + return BigDecimal(self._dec * other._dec) + return BigDecimal(self._dec * _Decimal(str(other))) + + def divide(self, *args): + if len(args) == 1: + other = args[0] + if isinstance(other, BigDecimal): + return BigDecimal(self._dec / other._dec) + return BigDecimal(self._dec / _Decimal(str(other))) + elif len(args) == 2: + other, rounding_mode = args + if isinstance(other, BigDecimal): + result = self._dec / other._dec + else: + result = self._dec / _Decimal(str(other)) + return BigDecimal(result) + elif len(args) == 3: + other, scale, rounding_mode = args + if isinstance(other, BigDecimal): + result = self._dec / other._dec + else: + result = self._dec / _Decimal(str(other)) + rounded = BigDecimal(result) + return rounded.setScale(scale, rounding_mode) + + def divideToIntegralValue(self, other): + if isinstance(other, BigDecimal): + return BigDecimal(int(self._dec / other._dec)) + return BigDecimal(int(self._dec / _Decimal(str(other)))) + + def remainder(self, other): + if isinstance(other, BigDecimal): + return BigDecimal(self._dec % other._dec) + return BigDecimal(self._dec % _Decimal(str(other))) + + def pow(self, n): + return BigDecimal(self._dec ** n) + + def abs(self): + return BigDecimal(abs(self._dec)) + + def negate(self): + return BigDecimal(-self._dec) + + def signum(self): + if self._dec > 0: + return 1 + if self._dec < 0: + return -1 + return 0 + + def scale(self): + return abs(self._dec.as_tuple().exponent) + + def precision(self): + return len(self._dec.as_tuple().digits) + + def round(self, mc): + if mc.getPrecision() > 0: + with _local_context() as ctx: + ctx.prec = mc.getPrecision() + return BigDecimal(+self._dec) + return BigDecimal(self._dec) + + def setScale(self, new_scale, rounding_mode=None): + if rounding_mode is None: + qe = _Decimal(10) ** (-new_scale) + return BigDecimal(self._dec.quantize(qe)) + py_mode = self._round_map.get(rounding_mode, _DEC_ROUND_HALF_UP) + qe = _Decimal(10) ** (-new_scale) + return BigDecimal(self._dec.quantize(qe, rounding=py_mode)) + + def ulp(self): + exponent = self._dec.as_tuple().exponent + return BigDecimal(_Decimal(10) ** exponent) + + def unscaledValue(self): + t = self._dec.as_tuple() + digits = t.digits + val = int("".join(str(d) for d in digits)) + return BigInteger(-val if t.sign else val) + + def byteValueExact(self): + val = self.intValueExact() + if val < -128 or val > 127: + raise ArithmeticError("Overflow") + return val + + def shortValueExact(self): + val = self.intValueExact() + if val < -32768 or val > 32767: + raise ArithmeticError("Overflow") + return val + + def intValueExact(self): + if self._dec != int(self._dec): + raise ArithmeticError("Rounding necessary") + return int(self._dec) + + def longValueExact(self): + if self._dec != int(self._dec): + raise ArithmeticError("Rounding necessary") + return int(self._dec) + + def floatValue(self): + return float(self._dec) + + def doubleValue(self): + return float(self._dec) + + def intValue(self): + return int(self._dec) + + def longValue(self): + return int(self._dec) + + def toString(self): + return str(self._dec) + + def toEngineeringString(self): + return format(self._dec, "e") + + def toPlainString(self): + return format(self._dec, "f") + + def compareTo(self, other): + if isinstance(other, BigDecimal): + return (self._dec > other._dec) - (self._dec < other._dec) + return -1 + + def equals(self, other): + if isinstance(other, BigDecimal): + return self._dec == other._dec + return False + + def hashCode(self): + return hash(self._dec) & 0x7FFFFFFF + + def min(self, other): + return BigDecimal(min(self._dec, other._dec if isinstance(other, BigDecimal) else _Decimal(str(other)))) + + def max(self, other): + return BigDecimal(max(self._dec, other._dec if isinstance(other, BigDecimal) else _Decimal(str(other)))) + + def __str__(self): + return str(self._dec) + + def __int__(self): + return int(self._dec) + + def __float__(self): + return float(self._dec) + + def __neg__(self): + return self.negate() + + def __abs__(self): + return self.abs() + + def __add__(self, other): + return self.add(other) + + def __sub__(self, other): + return self.subtract(other) + + def __mul__(self, other): + return self.multiply(other) + + def __truediv__(self, other): + return self.divide(other) + + def __mod__(self, other): + return self.remainder(other) + + def __pow__(self, other): + return self.pow(other) + + +BigDecimal.ZERO = BigDecimal(0) +BigDecimal.ONE = BigDecimal(1) +BigDecimal.TEN = BigDecimal(10) \ No newline at end of file diff --git a/android_pyapi/java/net.py b/android_pyapi/java/net.py new file mode 100644 index 0000000..9995689 --- /dev/null +++ b/android_pyapi/java/net.py @@ -0,0 +1,1582 @@ +""" +Java net standard library APIs implemented in Python. +java.net package - URL, Socket, HTTP connection, and networking classes. +""" + +from .._core import Stub, AndroidOnlyError, HAS_REQUESTS, HAS_HTTPX +from .io import IOException + +import os as _os +import socket as _socket +import struct as _struct +import threading as _threading +from urllib.parse import ( + urlparse as _urlparse, + urlunparse as _urlunparse, + quote as _quote, + unquote as _unquote, +) +from datetime import datetime as _datetime +from collections import OrderedDict as _OrderedDict + +__all__ = [ + "URL", "URLConnection", "HttpURLConnection", + "InetAddress", "InetSocketAddress", + "Socket", "ServerSocket", "DatagramSocket", "DatagramPacket", + "Proxy", "ProxySelector", "Authenticator", "PasswordAuthentication", + "NetworkInterface", "URI", + "CookieHandler", "CookieManager", "HttpCookie", + "URLDecoder", "URLEncoder", + "SocketException", "ConnectException", "BindException", + "NoRouteToHostException", "PortUnreachableException", + "ProtocolException", "SocketTimeoutException", + "MalformedURLException", "UnknownHostException", "UnknownServiceException", +] + + +# ============================================================ +# Exception classes +# ============================================================ + +class SocketException(IOException): + pass + +class ConnectException(SocketException): + pass + +class BindException(SocketException): + pass + +class NoRouteToHostException(SocketException): + pass + +class PortUnreachableException(SocketException): + pass + +class ProtocolException(IOException): + pass + +class SocketTimeoutException(IOException): + pass + +class MalformedURLException(IOException): + pass + +class UnknownHostException(IOException): + pass + +class UnknownServiceException(IOException): + pass + + +# ============================================================ +# InetAddress +# ============================================================ + +class InetAddress(Stub): + def __init__(self, host, addr): + super().__init__() + self._host = host + self._addr = addr # bytes, 4 or 16 + + @staticmethod + def getByName(host): + try: + addr = _socket.getaddrinfo(host, None, family=_socket.AF_UNSPEC) + if addr: + raw = addr[0][4][0] + return InetAddress(host, _socket.inet_pton(_socket.AF_INET, raw) if "." in raw else _socket.inet_pton(_socket.AF_INET6, raw)) + except Exception: + raise UnknownHostException(host) + + @staticmethod + def getAllByName(host): + try: + addrs = _socket.getaddrinfo(host, None, family=_socket.AF_UNSPEC) + results = [] + seen = set() + for a in addrs: + raw = a[4][0] + if raw not in seen: + seen.add(raw) + if "." in raw: + results.append(InetAddress(host, _socket.inet_pton(_socket.AF_INET, raw))) + else: + results.append(InetAddress(host, _socket.inet_pton(_socket.AF_INET6, raw))) + return results + except Exception: + raise UnknownHostException(host) + + @staticmethod + def getLocalHost(): + hostname = _socket.gethostname() + return InetAddress.getByName(hostname) + + @staticmethod + def getLoopbackAddress(): + return InetAddress("localhost", b"\x7f\x00\x00\x01") + + @staticmethod + def getByAddress(host, addr): + return InetAddress(host, addr) + + def getHostName(self): + return self._host + + def getHostAddress(self): + if len(self._addr) == 4: + return _socket.inet_ntoa(self._addr) + return _socket.inet_ntop(_socket.AF_INET6, self._addr) + + def getCanonicalHostName(self): + try: + name, _, _ = _socket.gethostbyaddr(self.getHostAddress()) + return name + except Exception: + return self._host + + def isReachable(self, timeout=5000): + try: + s = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) + s.settimeout(timeout / 1000.0) + result = s.connect_ex((self.getHostAddress(), 7)) + s.close() + return result == 0 + except Exception: + return False + + def isLoopbackAddress(self): + if len(self._addr) == 4: + return self._addr[0] == 127 + return self._addr == b"\x00" * 15 + b"\x01" + + def isLinkLocalAddress(self): + if len(self._addr) == 4: + return self._addr[0] == 169 and self._addr[1] == 254 + return (self._addr[0] & 0xFF) == 0xFE and (self._addr[1] & 0xC0) == 0x80 + + def isSiteLocalAddress(self): + if len(self._addr) == 4: + b0 = self._addr[0] + return b0 == 10 or (b0 == 172 and 16 <= self._addr[1] <= 31) or (b0 == 192 and self._addr[1] == 168) + return False + + def isMCGlobal(self): + if len(self._addr) == 4: + return 224 <= self._addr[0] <= 239 and self._addr[0] != 224 and self._addr[0] != 239 + return (self._addr[0] & 0xFF) == 0xFF and (self._addr[1] & 0x0F) == 0x0E and (self._addr[2] & 0x20) == 0 + + def isMCLinkLocal(self): + if len(self._addr) == 4: + return self._addr[0] == 224 and self._addr[1] == 0 and self._addr[2] == 0 + return (self._addr[0] & 0xFF) == 0xFF and (self._addr[1] & 0x0F) == 0x02 + + def isMCNodeLocal(self): + if len(self._addr) == 4: + return self._addr[0] == 224 and self._addr[1] == 0 and self._addr[2] == 0 and self._addr[3] == 1 + return (self._addr[0] & 0xFF) == 0xFF and (self._addr[1] & 0x0F) == 0x01 + + def isMCOrgLocal(self): + if len(self._addr) == 4: + return self._addr[0] == 239 and 192 <= self._addr[1] <= 195 + return (self._addr[0] & 0xFF) == 0xFF and (self._addr[1] & 0x0F) == 0x08 + + def isMCSiteLocal(self): + if len(self._addr) == 4: + return self._addr[0] == 239 and 255 <= self._addr[1] <= 255 + return (self._addr[0] & 0xFF) == 0xFF and (self._addr[1] & 0x0F) == 0x05 + + def getAddress(self): + return self._addr + + def toString(self): + return f"{self._host}/{self.getHostAddress()}" + + def equals(self, other): + if isinstance(other, InetAddress): + return self._addr == other._addr + return False + + def hashCode(self): + return hash(bytes(self._addr)) & 0x7FFFFFFF + + +# ============================================================ +# InetSocketAddress +# ============================================================ + +class InetSocketAddress(Stub): + def __init__(self, *args): + super().__init__() + if len(args) == 2 and isinstance(args[0], int): + self._addr = None + self._port = args[0] if isinstance(args[0], int) else args[1] + elif len(args) == 2: + hostname, port = args + self._addr = InetAddress.getByName(hostname) if isinstance(hostname, str) else hostname + self._port = port + elif len(args) == 1 and isinstance(args[0], int): + self._addr = None + self._port = args[0] + + def getAddress(self): + return self._addr + + def getPort(self): + return self._port + + def getHostName(self): + return self._addr.getHostName() if self._addr else None + + def getHostString(self): + return self.getHostName() + + def isUnresolved(self): + return self._addr is None + + def toString(self): + return f"{self.getHostName()}:{self._port}" + + def equals(self, other): + if isinstance(other, InetSocketAddress): + return self._port == other._port and self._addr == other._addr + return False + + def hashCode(self): + return hash((self._port, self._addr)) & 0x7FFFFFFF + + +# ============================================================ +# URI +# ============================================================ + +class URI(Stub): + def __init__(self, *args): + super().__init__() + if len(args) == 1: + self._uri = args[0] + parsed = _urlparse(self._uri) + self._scheme = parsed.scheme + self._host = parsed.hostname + self._port = parsed.port + self._path = parsed.path + self._query = parsed.query + self._fragment = parsed.fragment + self._user_info = parsed.username + self._authority = parsed.netloc + elif len(args) == 5: + self._scheme = args[0] + self._authority = args[1] or "" + self._path = args[2] or "" + self._query = args[3] or "" + self._fragment = args[4] or "" + parsed = _urlparse(f"{self._scheme}://{self._authority}{self._path}") + self._host = parsed.hostname + self._port = parsed.port + self._user_info = parsed.username + self._uri = f"{self._scheme}://{self._authority}{self._path}" + if self._query: + self._uri += "?" + self._query + if self._fragment: + self._uri += "#" + self._fragment + else: + raise ValueError("URI constructor requires 1 or 5 arguments") + + def getScheme(self): + return self._scheme + + def getHost(self): + return self._host + + def getPort(self): + return self._port if self._port else -1 + + def getPath(self): + return self._path + + def getQuery(self): + return self._query + + def getFragment(self): + return self._fragment + + def getAuthority(self): + return self._authority + + def getRawAuthority(self): + return self._authority + + def getRawFragment(self): + return self._fragment + + def getRawPath(self): + return self._path + + def getRawQuery(self): + return self._query + + def getRawSchemeSpecificPart(self): + return f"//{self._authority}{self._path}" + + def getRawUserInfo(self): + return self._user_info or "" + + def getSchemeSpecificPart(self): + return self.getRawSchemeSpecificPart() + + def getUserInfo(self): + return self._user_info or "" + + def isAbsolute(self): + return bool(self._scheme) + + def isOpaque(self): + return not self._path and not self._authority and self._scheme + + def normalize(self): + import posixpath + normalized = posixpath.normpath(self._path) + return URI(f"{self._scheme}://{self._authority}{normalized}") + + def resolve(self, uri_or_str): + if isinstance(uri_or_str, URI): + other = uri_or_str + else: + other = URI(uri_or_str) + if other.isAbsolute(): + return other + base_path = self._path or "/" + if base_path.endswith("/"): + new_path = base_path + other._path + else: + new_path = "/".join(base_path.rsplit("/", 1)[:-1] + [other._path]) if other._path else base_path + return URI(f"{self._scheme}://{self._authority}{new_path}") + + def relativize(self, other): + if self._path and other._path and other._path.startswith(self._path): + rel = other._path[len(self._path):] + if rel.startswith("/"): + rel = rel[1:] + return URI(rel) + return other + + def toASCIIString(self): + return _quote(self._uri, safe=":/?#[]@!$&'()*+,;=-._~") + + def toString(self): + return self._uri + + def toURL(self): + return URL(self._uri) + + def compareTo(self, other): + return -1 if self._uri < other._uri else (1 if self._uri > other._uri else 0) + + def equals(self, other): + if isinstance(other, URI): + return self._uri == other._uri + return False + + def hashCode(self): + return hash(self._uri) & 0x7FFFFFFF + + def parseServerAuthority(self): + return self + + +# ============================================================ +# URL +# ============================================================ + +class URL(Stub): + def __init__(self, *args): + super().__init__() + if len(args) == 1: + spec = args[0] + parsed = _urlparse(str(spec)) + self._protocol = parsed.scheme + self._host = parsed.hostname or "" + self._port = parsed.port + self._path = parsed.path or "" + self._file = parsed.path or "" + if parsed.query: + self._file += "?" + parsed.query + self._query = parsed.query or "" + self._ref = parsed.fragment or "" + self._authority = parsed.netloc or "" + self._user_info = parsed.username or "" + self._url_str = str(spec) + elif len(args) == 4: + protocol, host, port, file = args + self._protocol = protocol or "" + self._host = host or "" + self._port = port + self._path = file or "" + self._file = file or "" + self._query = "" + self._ref = "" + self._authority = f"{host}:{port}" if port and port > 0 else host + self._user_info = "" + parts = [file or ""] + self._url_str = f"{protocol}://{self._authority}{file}" + + def getProtocol(self): + return self._protocol + + def getHost(self): + return self._host + + def getPort(self): + return self._port if self._port else -1 + + def getDefaultPort(self): + defaults = { + "http": 80, "https": 443, "ftp": 21, "smtp": 25, + "ssh": 22, "telnet": 23, "ldap": 389, + } + return defaults.get(self._protocol, -1) + + def getPath(self): + return self._path + + def getFile(self): + return self._file + + def getQuery(self): + return self._query + + def getRef(self): + return self._ref + + def getAuthority(self): + return self._authority + + def getUserInfo(self): + return self._user_info + + def getContent(self, classes=None): + return self.openConnection().getContent(classes) + + def openConnection(self, proxy=None): + return HttpURLConnection(self._url_str, proxy) + + def openStream(self): + return self.openConnection().getInputStream() + + def sameFile(self, other): + return (self._protocol == other._protocol and self._host == other._host + and self._port == other._port and self._path == other._path) + + def toExternalForm(self): + return self._url_str + + def toString(self): + return self._url_str + + def toURI(self): + return URI(self._url_str) + + def equals(self, other): + if isinstance(other, URL): + return self._url_str == other._url_str + return False + + def hashCode(self): + return hash(self._url_str) & 0x7FFFFFFF + + def openConnection(self, proxy=None): + return HttpURLConnection(self._url_str, proxy) + + def __str__(self): + return self._url_str + + +# ============================================================ +# URLConnection +# ============================================================ + +class URLConnection(Stub): + def __init__(self, url_str): + super().__init__() + self._url_str = url_str + self._connected = False + self._do_input = True + self._do_output = False + self._allow_user_interaction = False + self._use_caches = True + self._default_use_caches = True + self._if_modified_since = 0 + self._read_timeout = 0 + self._connect_timeout = 0 + self._request_properties = _OrderedDict() + self._response_code = None + self._response_message = None + self._response_headers = _OrderedDict() + self._content = None + + def connect(self): + if not self._connected: + self._connected = True + + def getContent(self, classes=None): + raise UnknownServiceException("getContent not implemented") + + def getContentEncoding(self): + return self._response_headers.get("Content-Encoding") + + def getContentLength(self): + val = self._response_headers.get("Content-Length") + return int(val) if val else -1 + + def getContentLengthLong(self): + return self.getContentLength() + + def getContentType(self): + return self._response_headers.get("Content-Type") + + def getDate(self): + val = self._response_headers.get("Date") + if val: + try: + return int(_datetime.strptime(val, "%a, %d %b %Y %H:%M:%S %Z").timestamp() * 1000) + except Exception: + pass + return 0 + + def getExpiration(self): + val = self._response_headers.get("Expires") + if val: + try: + return int(_datetime.strptime(val, "%a, %d %b %Y %H:%M:%S %Z").timestamp() * 1000) + except Exception: + pass + return 0 + + def getHeaderField(self, name): + return self._response_headers.get(name) + + def getHeaderFieldDate(self, name, default): + val = self._response_headers.get(name) + if val: + try: + return int(_datetime.strptime(val, "%a, %d %b %Y %H:%M:%S %Z").timestamp() * 1000) + except Exception: + pass + return default + + def getHeaderFieldInt(self, name, default): + val = self._response_headers.get(name) + return int(val) if val and val.isdigit() else default + + def getHeaderFieldKey(self, n): + keys = list(self._response_headers.keys()) + return keys[n] if n < len(keys) else None + + def getHeaderFields(self): + return self._response_headers + + def getInputStream(self): + raise UnknownServiceException("getInputStream not implemented") + + def getOutputStream(self): + raise UnknownServiceException("getOutputStream not implemented") + + def getPermission(self): + return None + + def getReadTimeout(self): + return self._read_timeout + + def setReadTimeout(self, timeout): + self._read_timeout = timeout + + def getConnectTimeout(self): + return self._connect_timeout + + def setConnectTimeout(self, timeout): + self._connect_timeout = timeout + + def getRequestProperty(self, key): + return self._request_properties.get(key) + + def setRequestProperty(self, key, value): + self._request_properties[key] = value + + def addRequestProperty(self, key, value): + if key in self._request_properties: + existing = self._request_properties[key] + if isinstance(existing, list): + existing.append(value) + else: + self._request_properties[key] = [existing, value] + else: + self._request_properties[key] = value + + def getDefaultUseCaches(self): + return self._default_use_caches + + def setDefaultUseCaches(self, use): + self._default_use_caches = use + + def getUseCaches(self): + return self._use_caches + + def setUseCaches(self, use): + self._use_caches = use + + def setDoInput(self, do_input): + self._do_input = do_input + + def getDoInput(self): + return self._do_input + + def setDoOutput(self, do_output): + self._do_output = do_output + + def getDoOutput(self): + return self._do_output + + def setAllowUserInteraction(self, allow): + self._allow_user_interaction = allow + + def getAllowUserInteraction(self): + return self._allow_user_interaction + + def setIfModifiedSince(self, time): + self._if_modified_since = time + + def getIfModifiedSince(self): + return self._if_modified_since + + +# ============================================================ +# HttpURLConnection +# ============================================================ + +class HttpURLConnection(URLConnection): + HTTP_OK = 200 + HTTP_CREATED = 201 + HTTP_ACCEPTED = 202 + HTTP_MOVED_PERM = 301 + HTTP_MOVED_TEMP = 302 + HTTP_NOT_MODIFIED = 304 + HTTP_BAD_REQUEST = 400 + HTTP_UNAUTHORIZED = 401 + HTTP_FORBIDDEN = 403 + HTTP_NOT_FOUND = 404 + HTTP_INTERNAL_ERROR = 500 + + def __init__(self, url_str, proxy=None): + super().__init__(url_str) + self._request_method = "GET" + self._instance_follow_redirects = True + self._chunked = False + self._fixed_length = -1 + self._input_stream = None + self._error_stream = None + self._response_code = -1 + self._response_message = "" + + def setRequestMethod(self, method): + method = method.upper() + valid = ["GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE", "PATCH"] + if method not in valid: + raise ProtocolException(f"Invalid HTTP method: {method}") + self._request_method = method + + def getRequestMethod(self): + return self._request_method + + def getResponseCode(self): + if self._response_code == -1: + self._do_request() + return self._response_code + + def getResponseMessage(self): + if self._response_code == -1: + self._do_request() + return self._response_message + + def getHeaderFields(self): + if self._response_code == -1: + self._do_request() + return self._response_headers + + def setInstanceFollowRedirects(self, follow): + self._instance_follow_redirects = follow + + def getInstanceFollowRedirects(self): + return self._instance_follow_redirects + + def getErrorStream(self): + if self._response_code == -1: + self._do_request() + if self._response_code >= 400: + return self._error_stream + return None + + def setChunkedStreamingMode(self, chunklen): + self._chunked = True + self._chunk_length = chunklen + + def setFixedLengthStreamingMode(self, content_length): + self._fixed_length = content_length + + def setFixedLengthStreamingModeLong(self, content_length): + self._fixed_length = content_length + + def disconnect(self): + self._input_stream = None + self._error_stream = None + self._connected = False + + def usingProxy(self): + return False + + def getInputStream(self): + if self._response_code == -1: + self._do_request() + if self._input_stream is None: + import io as _pyio_mod + self._input_stream = _pyio_mod.BytesIO(self._content.encode("utf-8") if isinstance(self._content, str) else self._content) + return self._input_stream + + def _do_request(self): + if HAS_REQUESTS: + self._do_requests() + elif HAS_HTTPX: + self._do_httpx() + else: + self._do_urllib() + + def _do_requests(self): + import requests + try: + kwargs = { + "headers": dict(self._request_properties), + "timeout": self._read_timeout / 1000.0 if self._read_timeout else None, + } + if self._instance_follow_redirects: + kwargs["allow_redirects"] = True + else: + kwargs["allow_redirects"] = False + method = self._request_method.lower() + if method == "get": + r = requests.get(self._url_str, **kwargs) + elif method == "post": + body = self._request_properties.pop("__body", None) + r = requests.post(self._url_str, data=body, **kwargs) + elif method == "put": + body = self._request_properties.pop("__body", None) + r = requests.put(self._url_str, data=body, **kwargs) + elif method == "delete": + r = requests.delete(self._url_str, **kwargs) + elif method == "head": + r = requests.head(self._url_str, **kwargs) + elif method == "options": + r = requests.options(self._url_str, **kwargs) + elif method == "patch": + body = self._request_properties.pop("__body", None) + r = requests.patch(self._url_str, data=body, **kwargs) + else: + r = requests.request(self._request_method, self._url_str, **kwargs) + self._response_code = r.status_code + self._response_message = r.reason + self._response_headers = dict(r.headers) + self._content = r.content + except Exception as e: + self._response_code = 0 + self._response_message = str(e) + + def _do_httpx(self): + import httpx + try: + client = httpx.Client(follow_redirects=self._instance_follow_redirects) + kwargs = { + "headers": dict(self._request_properties) if self._request_properties else None, + "timeout": self._read_timeout / 1000.0 if self._read_timeout else None, + } + method = self._request_method.lower() + if method == "get": + r = client.get(self._url_str, **kwargs) + elif method == "post": + body = self._request_properties.pop("__body", None) + r = client.post(self._url_str, content=body, **kwargs) + elif method == "put": + body = self._request_properties.pop("__body", None) + r = client.put(self._url_str, content=body, **kwargs) + elif method == "delete": + r = client.delete(self._url_str, **kwargs) + else: + r = client.request(self._request_method, self._url_str, **kwargs) + self._response_code = r.status_code + self._response_message = r.reason_phrase + self._response_headers = dict(r.headers) + self._content = r.content + except Exception as e: + self._response_code = 0 + self._response_message = str(e) + + def _do_urllib(self): + from urllib.request import urlopen, Request + from urllib.error import HTTPError + try: + req = Request(self._url_str, headers=dict(self._request_properties) if self._request_properties else {}, + method=self._request_method) + resp = urlopen(req, timeout=self._read_timeout / 1000.0 if self._read_timeout else None) + self._response_code = resp.status + self._response_message = resp.reason + self._response_headers = dict(resp.headers) + self._content = resp.read() + except HTTPError as e: + self._response_code = e.code + self._response_message = e.reason + self._response_headers = dict(e.headers) + self._content = e.read() + except Exception as e: + self._response_code = 0 + self._response_message = str(e) + self._content = b"" + + +# ============================================================ +# Socket +# ============================================================ + +class Socket(Stub): + def __init__(self, *args): + super().__init__() + self._socket = None + self._connected = False + self._bound = False + self._closed = False + self._input_shutdown = False + self._output_shutdown = False + self._input_stream = None + self._output_stream = None + self._timeout = 0 + self._keep_alive = False + self._tcp_no_delay = False + self._reuse_address = False + self._send_buffer_size = 0 + self._receive_buffer_size = 0 + self._linger = -1 + self._traffic_class = 0 + if len(args) == 2: + host, port = args + self.connect(InetSocketAddress(host, port)) + elif len(args) == 1 and isinstance(args[0], InetAddress): + addr, port = args[0], 0 + self.connect(InetSocketAddress(addr, port)) + + def connect(self, endpoint, timeout=None): + if isinstance(endpoint, InetSocketAddress): + host = endpoint.getHostName() + port = endpoint.getPort() + else: + host = str(endpoint) + port = 80 + self._socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) + if self._timeout > 0: + self._socket.settimeout(self._timeout / 1000.0) + try: + self._socket.connect((host, port)) + self._connected = True + except OSError as e: + raise ConnectException(str(e)) + + def bind(self, endpoint): + if isinstance(endpoint, InetSocketAddress): + host = endpoint.getHostName() + port = endpoint.getPort() + else: + host = "0.0.0.0" + port = 0 + self._socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) + self._socket.bind((host or "0.0.0.0", port)) + self._bound = True + + def close(self): + if not self._closed: + self._closed = True + if self._socket: + self._socket.close() + + def getInputStream(self): + if self._input_stream is None and self._socket: + import io as _pyio_mod + fd = self._socket.fileno() + self._input_stream = _pyio_mod.FileIO(fd, closefd=False) + return self._input_stream + + def getOutputStream(self): + if self._output_stream is None and self._socket: + import io as _pyio_mod + fd = self._socket.fileno() + self._output_stream = _pyio_mod.FileIO(fd, closefd=False, mode="ab") + return self._output_stream + + def getInetAddress(self): + if self._socket and self._connected: + host, _ = self._socket.getpeername() + return InetAddress.getByName(host) + return None + + def getPort(self): + if self._socket and self._connected: + _, port = self._socket.getpeername() + return port + return 0 + + def getLocalPort(self): + if self._socket: + _, port = self._socket.getsockname() + return port + return 0 + + def getLocalAddress(self): + if self._socket: + host, _ = self._socket.getsockname() + return InetAddress.getByName(host) + return InetAddress.getLoopbackAddress() + + def getRemoteSocketAddress(self): + if self._socket and self._connected: + host, port = self._socket.getpeername() + return InetSocketAddress(host, port) + return None + + def getLocalSocketAddress(self): + if self._socket: + host, port = self._socket.getsockname() + return InetSocketAddress(host, port) + return None + + def getSoTimeout(self): + return self._timeout + + def setSoTimeout(self, timeout): + self._timeout = timeout + if self._socket: + self._socket.settimeout(timeout / 1000.0 if timeout > 0 else None) + + def getTcpNoDelay(self): + return self._tcp_no_delay + + def setTcpNoDelay(self, on): + self._tcp_no_delay = on + if self._socket: + self._socket.setsockopt(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1 if on else 0) + + def getKeepAlive(self): + return self._keep_alive + + def setKeepAlive(self, on): + self._keep_alive = on + if self._socket: + self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_KEEPALIVE, 1 if on else 0) + + def getReuseAddress(self): + return self._reuse_address + + def setReuseAddress(self, on): + self._reuse_address = on + if self._socket: + self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1 if on else 0) + + def getSendBufferSize(self): + return self._send_buffer_size + + def setSendBufferSize(self, size): + self._send_buffer_size = size + if self._socket: + self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_SNDBUF, size) + + def getReceiveBufferSize(self): + return self._receive_buffer_size + + def setReceiveBufferSize(self, size): + self._receive_buffer_size = size + if self._socket: + self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_RCVBUF, size) + + def getSoLinger(self): + return self._linger + + def setSoLinger(self, on, linger): + if on: + self._linger = linger + if self._socket: + self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_LINGER, _struct.pack("ii", 1, linger)) + else: + self._linger = -1 + + def getTrafficClass(self): + return self._traffic_class + + def setTrafficClass(self, tc): + self._traffic_class = tc + + def shutdownInput(self): + if self._socket: + self._socket.shutdown(_socket.SHUT_RD) + self._input_shutdown = True + + def shutdownOutput(self): + if self._socket: + self._socket.shutdown(_socket.SHUT_WR) + self._output_shutdown = True + + def isConnected(self): + return self._connected + + def isBound(self): + return self._bound + + def isClosed(self): + return self._closed + + def isInputShutdown(self): + return self._input_shutdown + + def isOutputShutdown(self): + return self._output_shutdown + + def setPerformancePreferences(self, connection_time, latency, bandwidth): + pass + + +# ============================================================ +# ServerSocket +# ============================================================ + +class ServerSocket(Stub): + def __init__(self, port=0, backlog=50, bindAddr=None): + super().__init__() + self._socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) + self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) + self._port = port + self._backlog = backlog + self._bind_addr = bindAddr + self._closed = False + self._timeout = 0 + if port > 0 or bindAddr: + self.bind(InetSocketAddress(bindAddr or "0.0.0.0", port), backlog) + + def bind(self, endpoint, backlog=50): + host = endpoint.getHostName() if isinstance(endpoint, InetSocketAddress) else str(endpoint) + port = endpoint.getPort() if isinstance(endpoint, InetSocketAddress) else 0 + self._socket.bind((host or "0.0.0.0", port)) + self._socket.listen(backlog) + self._port = self._socket.getsockname()[1] + + def accept(self): + if self._closed: + raise SocketException("Socket closed") + if self._timeout > 0: + self._socket.settimeout(self._timeout / 1000.0) + try: + client_sock, addr = self._socket.accept() + sock = Socket() + sock._socket = client_sock + sock._connected = True + return sock + except _socket.timeout: + raise SocketTimeoutException("Accept timed out") + + def close(self): + if not self._closed: + self._closed = True + self._socket.close() + + def getInetAddress(self): + host, _ = self._socket.getsockname() + return InetAddress.getByName(host) + + def getLocalPort(self): + return self._socket.getsockname()[1] if not self._closed else -1 + + def getSoTimeout(self): + return self._timeout + + def setSoTimeout(self, timeout): + self._timeout = timeout + self._socket.settimeout(timeout / 1000.0 if timeout > 0 else None) + + def getReceiveBufferSize(self): + return self._socket.getsockopt(_socket.SOL_SOCKET, _socket.SO_RCVBUF) + + def setReceiveBufferSize(self, size): + self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_RCVBUF, size) + + def getReuseAddress(self): + return bool(self._socket.getsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR)) + + def setReuseAddress(self, on): + self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1 if on else 0) + + +# ============================================================ +# DatagramSocket / DatagramPacket +# ============================================================ + +class DatagramPacket(Stub): + def __init__(self, buf, length, address=None, port=None): + super().__init__() + if address is not None: + self._buf = bytearray(buf) + self._length = length + self._address = address + self._port = port + else: + self._buf = bytearray(buf) + self._length = length + self._address = None + self._port = -1 + + def getAddress(self): + return self._address + + def getPort(self): + return self._port + + def getData(self): + return bytes(self._buf) + + def getLength(self): + return self._length + + def setAddress(self, address): + self._address = address + + def setPort(self, port): + self._port = port + + def setData(self, buf, offset=None, length=None): + if offset is not None and length is not None: + self._buf = bytearray(buf[offset:offset+length]) + self._length = length + else: + self._buf = bytearray(buf) + self._length = len(buf) + + def setLength(self, length): + self._length = length + + +class DatagramSocket(Stub): + def __init__(self, port=None): + super().__init__() + self._socket = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) + self._bound = False + self._closed = False + if port is not None: + self.bind(port) + + def bind(self, port): + self._socket.bind(("0.0.0.0", port)) + self._bound = True + + def send(self, p): + addr = p.getAddress() + port = p.getPort() + self._socket.sendto(p.getData()[:p.getLength()], (addr.getHostAddress(), port)) + + def receive(self, p): + data, addr = self._socket.recvfrom(p.getLength()) + p.setData(data) + p.setAddress(InetAddress(addr[0], _socket.inet_pton(_socket.AF_INET, addr[0]))) + p.setPort(addr[1]) + + def close(self): + if not self._closed: + self._closed = True + self._socket.close() + + def getLocalPort(self): + return self._socket.getsockname()[1] if not self._closed else -1 + + def isBound(self): + return self._bound + + def isClosed(self): + return self._closed + + def setSoTimeout(self, timeout): + self._socket.settimeout(timeout / 1000.0 if timeout > 0 else None) + + def getSoTimeout(self): + return self._socket.gettimeout() * 1000 if self._socket.gettimeout() else 0 + + +# ============================================================ +# Proxy +# ============================================================ + +class Proxy(Stub): + class Type: + DIRECT = "DIRECT" + HTTP = "HTTP" + SOCKS = "SOCKS" + + NO_PROXY = None + + def __init__(self, typ, sa=None): + super().__init__() + self._type = typ + self._address = sa + + def type(self): + return self._type + + def address(self): + return self._address + + def toString(self): + return f"{self._type} proxy @ {self._address}" + + +class ProxySelector(Stub): + _default = None + + @staticmethod + def getDefault(): + return ProxySelector._default + + @staticmethod + def setDefault(sel): + ProxySelector._default = sel + + def select(self, uri): + return [Proxy.NO_PROXY] + + def connectFailed(self, uri, sa, ioe): + pass + + +# ============================================================ +# Authenticator / PasswordAuthentication +# ============================================================ + +class Authenticator(Stub): + _default = None + + @staticmethod + def getDefault(): + return Authenticator._default + + @staticmethod + def setDefault(auth): + Authenticator._default = auth + + def getPasswordAuthentication(self): + return self._pwd_auth + + def requestPasswordAuthentication(self, addr, port, protocol, prompt, scheme): + return self._pwd_auth + + +class PasswordAuthentication(Stub): + def __init__(self, user, password): + super().__init__() + self._user = user + self._password = password + + def getUserName(self): + return self._user + + def getPassword(self): + return self._password + + +# ============================================================ +# NetworkInterface +# ============================================================ + +class NetworkInterface(Stub): + _cache = {} + + def __init__(self, name, display_name, addrs): + super().__init__() + self._name = name + self._display_name = display_name + self._addrs = addrs + + @staticmethod + def getByName(name): + if name in NetworkInterface._cache: + return NetworkInterface._cache[name] + + @staticmethod + def getByInetAddress(addr): + for ni in NetworkInterface._cache.values(): + for a in ni._addrs: + if a.equals(addr): + return ni + return None + + @staticmethod + def getNetworkInterfaces(): + return list(NetworkInterface._cache.values()) + + def getName(self): + return self._name + + def getDisplayName(self): + return self._display_name + + def getInetAddresses(self): + return iter(self._addrs) + + def getInterfaceAddresses(self): + return self._addrs + + def isUp(self): + return True + + def isLoopback(self): + return self._name == "lo" + + def isPointToPoint(self): + return False + + def supportsMulticast(self): + return True + + def getHardwareAddress(self): + return None + + def getMTU(self): + return 1500 + + def getSubInterfaces(self): + return iter([]) + + def getParent(self): + return None + + def isVirtual(self): + return False + + def toString(self): + return f"{self._display_name} ({self._name})" + + @staticmethod + def _init_from_socket(): + import netifaces + for iface_name in netifaces.interfaces(): + addrs = [] + ifaddresses = netifaces.ifaddresses(iface_name) + for family in [netifaces.AF_INET, netifaces.AF_INET6]: + if family in ifaddresses: + for addr_info in ifaddresses[family]: + try: + addr = addr_info["addr"] + if "." in addr: + addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET, addr))) + else: + addrs.append(InetAddress(addr, _socket.inet_pton(_socket.AF_INET6, addr))) + except Exception: + pass + NetworkInterface._cache[iface_name] = NetworkInterface(iface_name, iface_name, addrs) + +try: + NetworkInterface._init_from_socket() +except Exception: + pass + + +# ============================================================ +# CookieHandler / CookieManager / HttpCookie +# ============================================================ + +class CookieHandler(Stub): + _default = None + + @staticmethod + def getDefault(): + return CookieHandler._default + + @staticmethod + def setDefault(handler): + CookieHandler._default = handler + + def get(self, uri, requestHeaders): + raise NotImplementedError + + def put(self, uri, responseHeaders): + raise NotImplementedError + + +class HttpCookie(Stub): + def __init__(self, name, value): + super().__init__() + self._name = name + self._value = value + self._comment = None + self._comment_url = None + self._discard = False + self._domain = None + self._max_age = -1 + self._path = None + self._portlist = None + self._secure = False + self._version = 0 + self._http_only = False + self._creation_time = _datetime.now() + + def getName(self): + return self._name + + def getValue(self): + return self._value + + def getComment(self): + return self._comment + + def getCommentURL(self): + return self._comment_url + + def getDiscard(self): + return self._discard + + def getDomain(self): + return self._domain + + def getMaxAge(self): + return self._max_age + + def getPath(self): + return self._path + + def getPortlist(self): + return self._portlist + + def getSecure(self): + return self._secure + + def getVersion(self): + return self._version + + def getHttpOnly(self): + return self._http_only + + def setComment(self, comment): + self._comment = comment + + def setCommentURL(self, url): + self._comment_url = url + + def setDiscard(self, discard): + self._discard = discard + + def setDomain(self, domain): + self._domain = domain + + def setMaxAge(self, max_age): + self._max_age = max_age + + def setPath(self, path): + self._path = path + + def setPortlist(self, portlist): + self._portlist = portlist + + def setSecure(self, secure): + self._secure = secure + + def setVersion(self, version): + self._version = version + + def setHttpOnly(self, http_only): + self._http_only = http_only + + def hasExpired(self): + if self._max_age < 0: + return False + elapsed = (_datetime.now() - self._creation_time).total_seconds() + return elapsed > self._max_age + + def equals(self, other): + if isinstance(other, HttpCookie): + return self._name == other._name + return False + + def toString(self): + return f"{self._name}={self._value}" + + def validate(self, name): + if not name or name.strip() == "": + raise ValueError("Cookie name cannot be empty") + + def __str__(self): + return self.toString() + + +class CookieManager(CookieHandler): + def __init__(self): + super().__init__() + self._cookie_store = {} + + def get(self, uri, requestHeaders): + cookies = [] + for domain, path, cookie in self._cookie_store.values(): + if not cookie.hasExpired(): + cookies.append(cookie.toString()) + return {"Cookie": "; ".join(cookies)} if cookies else {} + + def put(self, uri, responseHeaders): + set_cookie = responseHeaders.get("Set-Cookie") + if set_cookie: + parts = set_cookie.split(";") + name_val = parts[0].split("=", 1) + if len(name_val) == 2: + cookie = HttpCookie(name_val[0], name_val[1]) + for part in parts[1:]: + part = part.strip() + if "=" in part: + k, v = part.split("=", 1) + if k.lower() == "domain": + cookie.setDomain(v) + elif k.lower() == "path": + cookie.setPath(v) + elif k.lower() == "max-age": + cookie.setMaxAge(int(v)) + elif k.lower() == "version": + cookie.setVersion(int(v)) + else: + if part.lower() == "secure": + cookie.setSecure(True) + elif part.lower() == "httponly": + cookie.setHttpOnly(True) + elif part.lower() == "discard": + cookie.setDiscard(True) + self._cookie_store[(cookie.getDomain(), cookie.getPath(), cookie.getName())] = (cookie.getDomain(), cookie.getPath(), cookie) + + +# ============================================================ +# URLEncoder / URLDecoder +# ============================================================ + +class URLEncoder(Stub): + @staticmethod + def encode(s, charset="UTF-8"): + return _quote(str(s), safe="") + + @staticmethod + def encode(s): + return _quote(str(s), safe="") + + +class URLDecoder(Stub): + @staticmethod + def decode(s, charset="UTF-8"): + return _unquote(str(s), encoding=charset) + + @staticmethod + def decode(s): + return _unquote(str(s)) + + +# Initialize NO_PROXY +Proxy.NO_PROXY = Proxy(Proxy.Type.DIRECT, None) \ No newline at end of file diff --git a/android_pyapi/java/nio.py b/android_pyapi/java/nio.py new file mode 100644 index 0000000..9418716 --- /dev/null +++ b/android_pyapi/java/nio.py @@ -0,0 +1,622 @@ +""" +Java NIO standard library APIs implemented in Python. +java.nio package - Buffer, ByteBuffer, CharBuffer, etc. +""" + +from .._core import Stub +from .lang import RuntimeException +from .io import IOException +import struct as _struct +import sys as _sys + +IllegalStateException = type("IllegalStateException", (RuntimeException,), {}) +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "Buffer", "ByteBuffer", "CharBuffer", "ShortBuffer", + "IntBuffer", "LongBuffer", "FloatBuffer", "DoubleBuffer", + "MappedByteBuffer", "ByteOrder", + "BufferOverflowException", "BufferUnderflowException", + "ReadOnlyBufferException", "InvalidMarkException", +] + + +# ============================================================ +# Exception classes +# ============================================================ + +class BufferOverflowException(RuntimeException): + pass + +class BufferUnderflowException(RuntimeException): + pass + +class ReadOnlyBufferException(RuntimeException): + pass + +class InvalidMarkException(IllegalStateException): + pass + + +# ============================================================ +# ByteOrder +# ============================================================ + +class ByteOrder(Stub): + BIG_ENDIAN = None + LITTLE_ENDIAN = None + + def __init__(self, name): + super().__init__() + self._name = name + + @staticmethod + def nativeOrder(): + if _sys.byteorder == "little": + return ByteOrder.LITTLE_ENDIAN + return ByteOrder.BIG_ENDIAN + + def toString(self): + return self._name + + +ByteOrder.BIG_ENDIAN = ByteOrder("BIG_ENDIAN") +ByteOrder.LITTLE_ENDIAN = ByteOrder("LITTLE_ENDIAN") + + +# ============================================================ +# Buffer +# ============================================================ + +class Buffer(Stub): + def __init__(self, capacity, backing=None): + super().__init__() + self._capacity = capacity + self._position = 0 + self._limit = capacity + self._mark = -1 + self._backing = backing if backing is not None else bytearray(capacity) + self._read_only = False + + def capacity(self): + return self._capacity + + def position(self): + return self._position + + def position(self, new_position): + if new_position < 0 or new_position > self._limit: + raise IllegalArgumentException("Invalid position") + self._position = new_position + return self + + def limit(self): + return self._limit + + def limit(self, new_limit): + if new_limit < 0 or new_limit > self._capacity: + raise IllegalArgumentException("Invalid limit") + self._limit = new_limit + if self._position > self._limit: + self._position = self._limit + if self._mark > self._limit: + self._mark = -1 + return self + + def mark(self): + self._mark = self._position + return self + + def reset(self): + if self._mark < 0: + raise InvalidMarkException() + self._position = self._mark + return self + + def clear(self): + self._position = 0 + self._limit = self._capacity + self._mark = -1 + return self + + def flip(self): + self._limit = self._position + self._position = 0 + self._mark = -1 + return self + + def rewind(self): + self._position = 0 + self._mark = -1 + return self + + def remaining(self): + return self._limit - self._position + + def hasRemaining(self): + return self._position < self._limit + + def isReadOnly(self): + return self._read_only + + def isDirect(self): + return False + + def array(self): + if self._read_only: + raise ReadOnlyBufferException() + return self._backing + + def arrayOffset(self): + return 0 + + +# ============================================================ +# ByteBuffer +# ============================================================ + +class ByteBuffer(Buffer): + def __init__(self, capacity, backing=None, order=None): + super().__init__(capacity, backing) + self._order = order or ByteOrder.BIG_ENDIAN + + @staticmethod + def allocate(capacity): + return ByteBuffer(capacity, bytearray(capacity)) + + @staticmethod + def allocateDirect(capacity): + return ByteBuffer(capacity, bytearray(capacity)) + + @staticmethod + def wrap(array, offset=0, length=None): + if length is None: + length = len(array) - offset + buf = ByteBuffer(length, bytearray(array)) + buf._position = offset + buf._limit = offset + length + return buf + + def put(self, b, index=None): + if self._read_only: + raise ReadOnlyBufferException() + if index is None: + if isinstance(b, int): + if self._position >= self._limit: + raise BufferOverflowException() + self._backing[self._position] = b & 0xFF + self._position += 1 + elif isinstance(b, ByteBuffer): + n = b.remaining() + if self.remaining() < n: + raise BufferOverflowException() + self._backing[self._position:self._position+n] = b._backing[b._position:b._limit] + self._position += n + b._position = b._limit + elif isinstance(b, (bytes, bytearray)): + if self.remaining() < len(b): + raise BufferOverflowException() + self._backing[self._position:self._position+len(b)] = b + self._position += len(b) + else: + self._backing[index] = b & 0xFF + return self + + def get(self, index=None): + if index is None: + if self._position >= self._limit: + raise BufferUnderflowException() + val = self._backing[self._position] + self._position += 1 + return val & 0xFF + return self._backing[index] & 0xFF + + def putInt(self, value, index=None): + data = _struct.pack(">i" if self._order == ByteOrder.BIG_ENDIAN else "i" if self._order == ByteOrder.BIG_ENDIAN else "q" if self._order == ByteOrder.BIG_ENDIAN else "q" if self._order == ByteOrder.BIG_ENDIAN else "f" if self._order == ByteOrder.BIG_ENDIAN else "f" if self._order == ByteOrder.BIG_ENDIAN else "d" if self._order == ByteOrder.BIG_ENDIAN else "d" if self._order == ByteOrder.BIG_ENDIAN else "h" if self._order == ByteOrder.BIG_ENDIAN else "h" if self._order == ByteOrder.BIG_ENDIAN else " b) - (a < b) + return -1 + + +# ============================================================ +# IntBuffer +# ============================================================ + +class IntBuffer(Buffer): + def __init__(self, capacity, backing, offset=0, order=None): + super().__init__(capacity, backing) + self._int_order = order or ByteOrder.BIG_ENDIAN + self._int_offset = offset + + def get(self, index=None): + if index is not None: + idx = self._int_offset + index * 4 + return _struct.unpack(">i" if self._int_order == ByteOrder.BIG_ENDIAN else "i" if self._int_order == ByteOrder.BIG_ENDIAN else "i" if self._int_order == ByteOrder.BIG_ENDIAN else "q" if self._long_order == ByteOrder.BIG_ENDIAN else "q" if self._long_order == ByteOrder.BIG_ENDIAN else "q" if self._long_order == ByteOrder.BIG_ENDIAN else "f" if self._float_order == ByteOrder.BIG_ENDIAN else "f" if self._float_order == ByteOrder.BIG_ENDIAN else "f" if self._float_order == ByteOrder.BIG_ENDIAN else "d" if self._double_order == ByteOrder.BIG_ENDIAN else "d" if self._double_order == ByteOrder.BIG_ENDIAN else "d" if self._double_order == ByteOrder.BIG_ENDIAN else "h" if self._short_order == ByteOrder.BIG_ENDIAN else "h" if self._short_order == ByteOrder.BIG_ENDIAN else "h" if self._short_order == ByteOrder.BIG_ENDIAN else "h" if self._char_order == ByteOrder.BIG_ENDIAN else "h" if self._char_order == ByteOrder.BIG_ENDIAN else "h" if self._char_order == ByteOrder.BIG_ENDIAN else "= len(parts): + raise IllegalArgumentException("Invalid index") + return Path(parts[index]) + + def getNameCount(self): + parts = self._pypath.parts + if self.getRoot(): + parts = parts[1:] + return len(parts) + + def subpath(self, beginIndex, endIndex): + parts = self._pypath.parts + if self.getRoot(): + parts = parts[1:] + return Path(_os.path.join(*parts[beginIndex:endIndex])) + + def startsWith(self, other): + if isinstance(other, Path): + return str(self._pypath).startswith(str(other._pypath)) + return str(self._pypath).startswith(str(other)) + + def endsWith(self, other): + if isinstance(other, Path): + return str(self._pypath).endswith(str(other._pypath)) + return str(self._pypath).endswith(str(other)) + + def normalize(self): + return Path(_os.path.normpath(self._path)) + + def resolve(self, other): + if isinstance(other, Path): + other_path = str(other) + else: + other_path = str(other) + if _os.path.isabs(other_path): + return Path(other_path) + return Path(_os.path.join(self._path, other_path)) + + def resolveSibling(self, other): + parent = self.getParent() + if parent: + return parent.resolve(other) + return Path(other) + + def relativize(self, other): + if isinstance(other, Path): + other_str = str(other._pypath) + else: + other_str = str(other) + try: + rel = _os.path.relpath(other_str, self._path) + return Path(rel) + except ValueError: + return other + + def compareTo(self, other): + if isinstance(other, Path): + return (self._path > other._path) - (self._path < other._path) + return -1 + + def toString(self): + return self._path + + def toFile(self): + from ..io import File + return File(self._path) + + def isAbsolute(self): + return _os.path.isabs(self._path) + + def getFileSystem(self): + return FileSystems.getDefault() + + def iterator(self): + parts = self._pypath.parts + if self.getRoot(): + parts = parts[1:] + return iter([Path(p) for p in parts]) + + def register(self, watcher, events, *modifiers): + raise AndroidOnlyError("WatchService nio") + + def __str__(self): + return self._path + + def __eq__(self, other): + if isinstance(other, Path): + return self._path == other._path + return NotImplemented + + def __hash__(self): + return hash(self._path) + + +# ============================================================ +# Paths +# ============================================================ + +class Paths(Stub): + @staticmethod + def get(first, *more): + parts = [str(first)] + parts.extend(str(m) for m in more) + return Path(_os.path.join(*parts)) + + +# ============================================================ +# FileSystem +# ============================================================ + +class FileSystem(Stub): + def __init__(self): + super().__init__() + self._separator = _os.sep + self._open = True + + def getPath(self, first, *more): + return Paths.get(first, *more) + + def getSeparator(self): + return self._separator + + def getRootDirectories(self): + if _os.name == "nt": + import string + return [Path(d + ":\\") for d in string.ascii_uppercase if _os.path.exists(d + ":\\")] + return [Path("/")] + + def getUserPrincipalLookupService(self): + raise AndroidOnlyError("UserPrincipalLookupService") + + def newWatchService(self): + raise AndroidOnlyError("WatchService") + + def getFileStores(self): + return [FileStore("/")] + + def supportedFileAttributeViews(self): + return ["basic", "owner", "posix"] + + def close(self): + self._open = False + + def isOpen(self): + return self._open + + def isReadOnly(self): + return False + + +class FileSystems(Stub): + _default = None + + @staticmethod + def getDefault(): + if FileSystems._default is None: + FileSystems._default = FileSystem() + return FileSystems._default + + +# ============================================================ +# FileStore +# ============================================================ + +class FileStore(Stub): + def __init__(self, path): + super().__init__() + self._path = path + + def name(self): + return _os.path.basename(self._path) or "/" + + def type(self): + return "ext4" + + def isReadOnly(self): + return False + + def getTotalSpace(self): + try: + st = _os.statvfs(self._path) + return st.f_frsize * st.f_blocks + except OSError: + return 0 + + def getUsableSpace(self): + try: + st = _os.statvfs(self._path) + return st.f_frsize * st.f_bavail + except OSError: + return 0 + + def getUnallocatedSpace(self): + try: + st = _os.statvfs(self._path) + return st.f_frsize * st.f_bfree + except OSError: + return 0 + + def toString(self): + return f"{self.name()} ({self.type()})" + + +# ============================================================ +# FileVisitor / SimpleFileVisitor +# ============================================================ + +class FileVisitor(Stub): + def preVisitDirectory(self, dir, attrs): + return FileVisitResult.CONTINUE + + def postVisitDirectory(self, dir, exc): + return FileVisitResult.CONTINUE + + def visitFile(self, file, attrs): + return FileVisitResult.CONTINUE + + def visitFileFailed(self, file, exc): + return FileVisitResult.CONTINUE + + +class SimpleFileVisitor(FileVisitor): + pass + + +# ============================================================ +# DirectoryStream / Filter +# ============================================================ + +class DirectoryStream(Stub): + def __init__(self, path, filter=None): + super().__init__() + self._path = path + self._filter = filter + + def __iter__(self): + try: + entries = _os.listdir(str(self._path)) + for entry in entries: + p = Path(self._path, entry) + if self._filter is None or self._filter.accept(p): + yield p + except OSError: + raise IOException(f"Failed to list directory: {self._path}") + + def close(self): + pass + + +class DirectoryStreamFilter(Stub): + def accept(self, entry): + raise NotImplementedError + + +# ============================================================ +# PathMatcher +# ============================================================ + +class PathMatcher(Stub): + def __init__(self, pattern): + super().__init__() + self._pattern = pattern + + def matches(self, path): + import fnmatch + return fnmatch.fnmatch(str(path), self._pattern) + + +# ============================================================ +# WatchService / WatchKey / WatchEvent / Watchable (stubs) +# ============================================================ + +class Watchable(Stub): + def register(self, watcher, events, *modifiers): + raise AndroidOnlyError("WatchService") + +class WatchService(Stub): + def close(self): + pass + def poll(self): + return None + def take(self): + raise AndroidOnlyError("WatchService.take") + def poll(self, timeout, unit): + return None + +class WatchKey(Stub): + def isValid(self): + return True + def pollEvents(self): + return [] + def reset(self): + return True + def cancel(self): + pass + def watchable(self): + return None + +class WatchEvent(Stub): + class Kind(Stub): + def name(self): + return "" + def type(self): + return None + + def kind(self): + return None + def count(self): + return 0 + def context(self): + return None + + +# ============================================================ +# Files +# ============================================================ + +class Files(Stub): + @staticmethod + def copy(source, target, *options): + src = str(source) + dst = str(target) + replace = any(isinstance(o, StandardCopyOption) and o == StandardCopyOption.REPLACE_EXISTING for o in options) + try: + if replace and _os.path.exists(dst): + _os.remove(dst) + if _os.path.isdir(src): + _shutil.copytree(src, dst) + else: + _shutil.copy2(src, dst) + except OSError as e: + raise IOException(str(e)) + + @staticmethod + def move(source, target, *options): + src = str(source) + dst = str(target) + replace = any(isinstance(o, StandardCopyOption) and o == StandardCopyOption.REPLACE_EXISTING for o in options) + try: + if replace and _os.path.exists(dst): + _os.remove(dst) + _shutil.move(src, dst) + except OSError as e: + raise IOException(str(e)) + + @staticmethod + def delete(path): + try: + p = str(path) + if _os.path.isdir(p): + _os.rmdir(p) + else: + _os.remove(p) + except OSError as e: + raise NoSuchFileException(str(path)) + + @staticmethod + def deleteIfExists(path): + try: + Files.delete(path) + return True + except NoSuchFileException: + return False + + @staticmethod + def createDirectories(path): + _os.makedirs(str(path), exist_ok=True) + return path + + @staticmethod + def createDirectory(path, attrs=None): + _os.makedirs(str(path), exist_ok=True) + return path + + @staticmethod + def createFile(path, attrs=None): + p = str(path) + if _os.path.exists(p): + raise FileAlreadyExistsException(str(path)) + open(p, "a").close() + return path + + @staticmethod + def createTempFile(prefix, suffix, dir=None): + f = _tempfile.NamedTemporaryFile(prefix=prefix, suffix=suffix, + dir=str(dir) if dir else None, delete=False) + p = f.name + f.close() + return Path(p) + + @staticmethod + def createTempDirectory(prefix, attrs=None): + d = _tempfile.mkdtemp(prefix=prefix) + return Path(d) + + @staticmethod + def exists(path, *options): + return _os.path.exists(str(path)) + + @staticmethod + def notExists(path, *options): + return not _os.path.exists(str(path)) + + @staticmethod + def isReadable(path): + return _os.access(str(path), _os.R_OK) + + @staticmethod + def isWritable(path): + return _os.access(str(path), _os.W_OK) + + @staticmethod + def isExecutable(path): + return _os.access(str(path), _os.X_OK) + + @staticmethod + def isDirectory(path, *options): + return _os.path.isdir(str(path)) + + @staticmethod + def isRegularFile(path, *options): + return _os.path.isfile(str(path)) + + @staticmethod + def isSymbolicLink(path): + return _os.path.islink(str(path)) + + @staticmethod + def isHidden(path): + return _os.path.basename(str(path)).startswith(".") + + @staticmethod + def getLastModifiedTime(path, *options): + return int(_os.path.getmtime(str(path)) * 1000) + + @staticmethod + def setLastModifiedTime(path, time): + _os.utime(str(path), (time / 1000, time / 1000)) + + @staticmethod + def getAttribute(path, attribute, *options): + return None + + @staticmethod + def setAttribute(path, attribute, value, *options): + pass + + @staticmethod + def getPosixFilePermissions(path, *options): + return [] + + @staticmethod + def setPosixFilePermissions(path, perms, *options): + pass + + @staticmethod + def newInputStream(path, *options): + from ..io import FileInputStream + return FileInputStream(str(path)) + + @staticmethod + def newOutputStream(path, *options): + from ..io import FileOutputStream + append = any(isinstance(o, StandardOpenOption) and o == StandardOpenOption.APPEND for o in options) + return FileOutputStream(str(path), append) + + @staticmethod + def newBufferedReader(path, charset=None): + from ..io import BufferedReader, InputStreamReader, FileInputStream + inp = FileInputStream(str(path)) + reader = InputStreamReader(inp, charset or "utf-8") + return BufferedReader(reader) + + @staticmethod + def newBufferedWriter(path, charset=None, *options): + from ..io import BufferedWriter, OutputStreamWriter, FileOutputStream + append = any(isinstance(o, StandardOpenOption) and o == StandardOpenOption.APPEND for o in options) + out = FileOutputStream(str(path), append) + writer = OutputStreamWriter(out, charset or "utf-8") + return BufferedWriter(writer) + + @staticmethod + def readAllBytes(path): + with open(str(path), "rb") as f: + return f.read() + + @staticmethod + def readAllLines(path, charset=None): + with open(str(path), "r", encoding=charset or "utf-8") as f: + return f.read().splitlines() + + @staticmethod + def readString(path, charset=None): + with open(str(path), "r", encoding=charset or "utf-8") as f: + return f.read() + + @staticmethod + def write(path, content, *options): + mode = "wb" if isinstance(content, bytes) else "w" + open_mode = mode + if any(isinstance(o, StandardOpenOption) and o == StandardOpenOption.APPEND for o in options): + open_mode = "ab" if mode == "wb" else "a" + with open(str(path), open_mode) as f: + f.write(content) + + @staticmethod + def writeString(path, content, *options): + open_mode = "w" + if any(isinstance(o, StandardOpenOption) and o == StandardOpenOption.APPEND for o in options): + open_mode = "a" + with open(str(path), open_mode, encoding="utf-8") as f: + f.write(content) + + @staticmethod + def list(path): + try: + return [Path(_os.path.join(str(path), entry)) for entry in _os.listdir(str(path))] + except OSError: + raise IOException(f"Failed to list directory: {path}") + + @staticmethod + def walk(path, maxDepth=None): + for root, dirs, files in _os.walk(str(path)): + yield Path(root) + + @staticmethod + def find(start, maxDepth, matcher, *options): + for root, dirs, files in _os.walk(str(start)): + for f in files: + p = Path(_os.path.join(root, f)) + if matcher.matches(p): + yield p + + @staticmethod + def lines(path, charset=None): + with open(str(path), "r", encoding=charset or "utf-8") as f: + for line in f: + yield line.rstrip("\n") + + @staticmethod + def getFileStore(path): + return FileStore(str(path)) + + @staticmethod + def probeContentType(path): + return "application/octet-stream" + + @staticmethod + def getOwner(path, *options): + raise AndroidOnlyError("getOwner") + + @staticmethod + def setOwner(path, owner, *options): + raise AndroidOnlyError("setOwner") \ No newline at end of file diff --git a/android_pyapi/java/security.py b/android_pyapi/java/security.py new file mode 100644 index 0000000..2a7e131 --- /dev/null +++ b/android_pyapi/java/security.py @@ -0,0 +1,1145 @@ +""" +Java security standard library APIs implemented in Python. +java.security package - MessageDigest, SecureRandom, KeyStore, Signature, etc. +""" + +from .._core import Stub, AndroidOnlyError +from .lang import RuntimeException +import hashlib as _hashlib +import os as _os + +GeneralSecurityException = type("GeneralSecurityException", (RuntimeException,), {}) +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) +DigestException = type("DigestException", (GeneralSecurityException,), {}) +InvalidAlgorithmParameterException = type("InvalidAlgorithmParameterException", (GeneralSecurityException,), {}) +InvalidKeyException = type("InvalidKeyException", (GeneralSecurityException,), {}) +InvalidKeySpecException = type("InvalidKeySpecException", (GeneralSecurityException,), {}) +InvalidParameterException = type("InvalidParameterException", (IllegalArgumentException,), {}) +KeyException = type("KeyException", (GeneralSecurityException,), {}) +KeyManagementException = type("KeyManagementException", (KeyException,), {}) +KeyStoreException = type("KeyStoreException", (GeneralSecurityException,), {}) +NoSuchAlgorithmException = type("NoSuchAlgorithmException", (GeneralSecurityException,), {}) +NoSuchProviderException = type("NoSuchProviderException", (GeneralSecurityException,), {}) +ProviderException = type("ProviderException", (RuntimeException,), {}) +SignatureException = type("SignatureException", (GeneralSecurityException,), {}) + +__all__ = [ + "MessageDigest", "MessageDigestSpi", + "SecureRandom", "SecureRandomSpi", + "KeyStore", "KeyStoreSpi", + "Key", "PrivateKey", "PublicKey", "KeyPair", "KeyPairGenerator", + "KeyPairGeneratorSpi", "KeyFactory", "KeyFactorySpi", + "KeySpec", "SecretKeySpec", + "RSAPublicKeySpec", "RSAPrivateKeySpec", + "PKCS8EncodedKeySpec", "X509EncodedKeySpec", + "AlgorithmParameters", "AlgorithmParameterGenerator", + "AlgorithmParameterGeneratorSpi", "AlgorithmParametersSpi", + "Provider", "Security", + "DigestInputStream", "DigestOutputStream", + "Signature", "SignatureSpi", + "CodeSigner", "CodeSource", "Policy", "Permission", + "AllPermission", "BasicPermission", "PermissionCollection", + "Principal", + "CertPath", "CertPathBuilder", "CertPathBuilderSpi", + "CertPathValidator", "CertPathValidatorSpi", + "CertificateFactory", "CertificateFactorySpi", + "CertStore", "CertStoreSpi", + "CRL", "X509CRL", "X509Certificate", "X509Extension", + "Guard", "GuardedObject", "Identity", "IdentityScope", "Signer", + "Timestamp", "UnresolvedPermission", + "GeneralSecurityException", "DigestException", + "InvalidAlgorithmParameterException", "InvalidKeyException", + "InvalidKeySpecException", "InvalidParameterException", + "KeyException", "KeyManagementException", "KeyStoreException", + "NoSuchAlgorithmException", "NoSuchProviderException", + "ProviderException", "SignatureException", +] + + +# ============================================================ +# Key interfaces +# ============================================================ + +class Key(Stub): + def getAlgorithm(self): + return "" + + def getFormat(self): + return "" + + def getEncoded(self): + return b"" + + +class PrivateKey(Key): + pass + + +class PublicKey(Key): + pass + + +# ============================================================ +# KeySpec / SecretKeySpec / RSA specs / EncodedKeySpecs +# ============================================================ + +class KeySpec(Stub): + pass + + +class SecretKeySpec(KeySpec, Key): + def __init__(self, key_bytes, algorithm): + super().__init__() + self._key = key_bytes + self._algorithm = algorithm + + def getAlgorithm(self): + return self._algorithm + + def getFormat(self): + return "RAW" + + def getEncoded(self): + return self._key + + def hashCode(self): + return hash(self._key) ^ hash(self._algorithm) + + +class RSAPublicKeySpec(KeySpec): + def __init__(self, modulus, public_exponent): + super().__init__() + self._modulus = modulus + self._public_exponent = public_exponent + + def getModulus(self): + return self._modulus + + def getPublicExponent(self): + return self._public_exponent + + +class RSAPrivateKeySpec(KeySpec): + def __init__(self, modulus, private_exponent): + super().__init__() + self._modulus = modulus + self._private_exponent = private_exponent + + def getModulus(self): + return self._modulus + + def getPrivateExponent(self): + return self._private_exponent + + +class PKCS8EncodedKeySpec(KeySpec): + def __init__(self, encoded_key): + super().__init__() + self._encoded_key = encoded_key + + def getEncoded(self): + return self._encoded_key + + def getFormat(self): + return "PKCS#8" + + +class X509EncodedKeySpec(KeySpec): + def __init__(self, encoded_key): + super().__init__() + self._encoded_key = encoded_key + + def getEncoded(self): + return self._encoded_key + + def getFormat(self): + return "X.509" + + +# ============================================================ +# KeyPair +# ============================================================ + +class KeyPair(Stub): + def __init__(self, public_key, private_key): + super().__init__() + self._public = public_key + self._private = private_key + + def getPublic(self): + return self._public + + def getPrivate(self): + return self._private + + +# ============================================================ +# KeyPairGenerator +# ============================================================ + +class KeyPairGenerator(Stub): + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + self._keysize = 2048 + + @staticmethod + def getInstance(algorithm, provider=None): + return KeyPairGenerator(algorithm) + + def initialize(self, keysize_or_spec, random=None): + if isinstance(keysize_or_spec, int): + self._keysize = keysize_or_spec + + def generateKeyPair(self): + return KeyPair(PublicKey(), PrivateKey()) + + def getAlgorithm(self): + return self._algorithm + + def getProvider(self): + return Provider("SUN", 1.0, "Default provider") + + +# ============================================================ +# KeyFactory +# ============================================================ + +class KeyFactory(Stub): + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + + @staticmethod + def getInstance(algorithm, provider=None): + return KeyFactory(algorithm) + + def generatePublic(self, key_spec): + return PublicKey() + + def generatePrivate(self, key_spec): + return PrivateKey() + + def getKeySpec(self, key, key_spec_cls): + if key_spec_cls == X509EncodedKeySpec: + return X509EncodedKeySpec(key.getEncoded()) + raise InvalidKeySpecException("Unsupported key spec") + + def translateKey(self, key): + return key + + def getAlgorithm(self): + return self._algorithm + + +# ============================================================ +# KeyFactorySpi / KeyPairGeneratorSpi (stubs) +# ============================================================ + +class KeyFactorySpi(Stub): + pass + +class KeyPairGeneratorSpi(Stub): + pass + + +# ============================================================ +# MessageDigest +# ============================================================ + +class MessageDigest(Stub): + _MD_MAP = { + "MD5": "md5", + "SHA-1": "sha1", + "SHA": "sha1", + "SHA-224": "sha224", + "SHA-256": "sha256", + "SHA-384": "sha384", + "SHA-512": "sha512", + } + + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + algo_lower = algorithm.replace("-", "").lower() + if algo_lower in ("md5", "sha1", "sha224", "sha256", "sha384", "sha512"): + self._impl = _hashlib.new(algo_lower) + else: + self._impl = _hashlib.new("sha256") + + @staticmethod + def getInstance(algorithm, provider=None): + if algorithm not in MessageDigest._MD_MAP: + raise NoSuchAlgorithmException(f"{algorithm} not available") + return MessageDigest(algorithm) + + def update(self, data, offset=0, length=-1): + if length < 0: + self._impl.update(data) + else: + self._impl.update(data[offset:offset+length]) + + def digest(self, data=None): + if data is not None: + self._impl.update(data) + return self._impl.digest() + + def toString(self): + return f"MessageDigest: {self._algorithm}" + + def getAlgorithm(self): + return self._algorithm + + def getProvider(self): + return Provider("SUN", 1.0, "Default provider") + + def getDigestLength(self): + return self._impl.digest_size + + @staticmethod + def isEqual(digest_a, digest_b): + return digest_a == digest_b + + def clone(self): + md = MessageDigest(self._algorithm) + md._impl = self._impl.copy() + return md + + def reset(self): + self._impl = _hashlib.new(self._algorithm.replace("-", "").lower()) + + +class MessageDigestSpi(Stub): + pass + + +# ============================================================ +# SecureRandom +# ============================================================ + +class SecureRandom(Stub): + def __init__(self, seed=None): + super().__init__() + import random as _random + self._random = _random.Random() + if seed is not None: + self._random.seed(seed) + + def nextBytes(self, bytes_arr): + data = _os.urandom(len(bytes_arr)) + for i in range(len(bytes_arr)): + bytes_arr[i] = data[i] + + def nextInt(self, bound=None): + if bound is None: + return int.from_bytes(_os.urandom(4), "big") + return int.from_bytes(_os.urandom(4), "big") % bound + + def nextLong(self): + return int.from_bytes(_os.urandom(8), "big") + + def nextBoolean(self): + return int.from_bytes(_os.urandom(1), "big") % 2 == 0 + + def nextFloat(self): + return int.from_bytes(_os.urandom(4), "big") / 4294967296.0 + + def nextDouble(self): + return int.from_bytes(_os.urandom(8), "big") / 18446744073709551616.0 + + def nextGaussian(self): + import random as _random + return _random.gauss(0, 1) + + def generateSeed(self, num_bytes): + return _os.urandom(num_bytes) + + def getAlgorithm(self): + return "SHA1PRNG" + + def getProvider(self): + return Provider("SUN", 1.0, "Default provider") + + def setSeed(self, seed): + self._random.seed(seed) + + def getSeed(self, num_bytes): + return _os.urandom(num_bytes) + + +class SecureRandomSpi(Stub): + pass + + +# ============================================================ +# Provider / Security +# ============================================================ + +class Provider(Stub): + def __init__(self, name, version, info): + super().__init__() + self._name = name + self._version = version + self._info = info + self._properties = {} + + def getName(self): + return self._name + + def getVersion(self): + return self._version + + def getInfo(self): + return self._info + + def getProperty(self, key): + return self._properties.get(key) + + def setProperty(self, key, value): + self._properties[key] = value + + def toString(self): + return f"{self._name} v{self._version}" + + +class Security(Stub): + _providers = [Provider("SUN", 1.0, "SUN provider")] + + @staticmethod + def getProvider(name): + for p in Security._providers: + if p.getName() == name: + return p + return None + + @staticmethod + def getProviders(): + return Security._providers + + @staticmethod + def addProvider(provider): + Security._providers.append(provider) + return len(Security._providers) - 1 + + @staticmethod + def removeProvider(name): + for i, p in enumerate(Security._providers): + if p.getName() == name: + Security._providers.pop(i) + return + + @staticmethod + def getAlgorithms(service_name): + if service_name == "MessageDigest": + return list(MessageDigest._MD_MAP.keys()) + return [] + + +# ============================================================ +# DigestInputStream / DigestOutputStream +# ============================================================ + +class DigestInputStream(Stub): + def __init__(self, stream, digest): + super().__init__() + self._stream = stream + self._digest = digest + + def getMessageDigest(self): + return self._digest + + def setMessageDigest(self, digest): + self._digest = digest + + def read(self, b=None, off=0, length=0): + if b is None: + data = self._stream.read() + else: + data = self._stream.read(length) + if data: + self._digest.update(data) + return data + + def toString(self): + return f"DigestInputStream: {self._digest}" + + +class DigestOutputStream(Stub): + def __init__(self, stream, digest): + super().__init__() + self._stream = stream + self._digest = digest + + def getMessageDigest(self): + return self._digest + + def setMessageDigest(self, digest): + self._digest = digest + + def write(self, b, off=0, length=0): + if isinstance(b, (bytes, bytearray)): + data = b if length == 0 else b[off:off+length] + else: + data = bytes([b]) + self._digest.update(data) + self._stream.write(data) + + def toString(self): + return f"DigestOutputStream: {self._digest}" + + +# ============================================================ +# Signature +# ============================================================ + +class Signature(Stub): + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + self._state = "UNINITIALIZED" + self._impl = _hashlib.new("sha256") + + @staticmethod + def getInstance(algorithm, provider=None): + return Signature(algorithm) + + def initSign(self, private_key, random=None): + self._state = "SIGN" + + def initVerify(self, public_key): + self._state = "VERIFY" + + def update(self, data, off=0, length=-1): + if length < 0: + self._impl.update(data) + else: + self._impl.update(data[off:off+length]) + + def sign(self): + self._state = "UNINITIALIZED" + return self._impl.digest() + + def verify(self, signature): + self._state = "UNINITIALIZED" + return self._impl.digest() == signature + + def setParameter(self, param, value=None): + pass + + def getParameters(self): + return None + + def getAlgorithm(self): + return self._algorithm + + def getProvider(self): + return Provider("SUN", 1.0, "SUN provider") + + +class SignatureSpi(Stub): + pass + + +# ============================================================ +# KeyStore +# ============================================================ + +class KeyStore(Stub): + class PasswordProtection(Stub): + def __init__(self, password): + super().__init__() + self._password = password + + def getPassword(self): + return self._password + + class Entry(Stub): + pass + + class PrivateKeyEntry(Entry): + def __init__(self, private_key, cert_chain): + super().__init__() + self._private_key = private_key + self._cert_chain = cert_chain + + def getPrivateKey(self): + return self._private_key + + def getCertificateChain(self): + return self._cert_chain + + class SecretKeyEntry(Entry): + def __init__(self, secret_key): + super().__init__() + self._secret_key = secret_key + + def getSecretKey(self): + return self._secret_key + + class TrustedCertificateEntry(Entry): + def __init__(self, cert): + super().__init__() + self._cert = cert + + def getTrustedCertificate(self): + return self._cert + + def __init__(self, type_name="JKS"): + super().__init__() + self._type = type_name + self._entries = {} + self._loaded = False + + @staticmethod + def getInstance(type_name, provider=None): + return KeyStore(type_name) + + def load(self, stream=None, password=None): + self._loaded = True + + def store(self, stream, password): + pass + + def setKeyEntry(self, alias, key, password, chain): + self._entries[alias] = (key, chain) + + def setCertificateEntry(self, alias, cert): + self._entries[alias] = cert + + def getKey(self, alias, password): + entry = self._entries.get(alias) + if isinstance(entry, tuple): + return entry[0] + return None + + def getCertificate(self, alias): + entry = self._entries.get(alias) + if isinstance(entry, tuple): + return entry[1][0] if entry[1] else None + return entry + + def getCertificateChain(self, alias): + entry = self._entries.get(alias) + if isinstance(entry, tuple): + return entry[1] + return None + + def getCertificateAliases(self, cert): + return [] + + def aliases(self): + return list(self._entries.keys()) + + def containsAlias(self, alias): + return alias in self._entries + + def deleteEntry(self, alias): + if alias in self._entries: + del self._entries[alias] + + def size(self): + return len(self._entries) + + def isCertificateEntry(self, alias): + return alias in self._entries and not isinstance(self._entries[alias], tuple) + + def isKeyEntry(self, alias): + return alias in self._entries and isinstance(self._entries[alias], tuple) + + def getType(self): + return self._type + + def getProvider(self): + return Provider("SUN", 1.0, "SUN provider") + + def getCreationDate(self, alias): + from datetime import datetime + return datetime.now() + + +class KeyStoreSpi(Stub): + pass + + +# ============================================================ +# X509Certificate +# ============================================================ + +class X509Certificate(Stub): + def __init__(self): + super().__init__() + self._encoded = b"" + + def getSubjectX500Principal(self): + return Principal() + + def getIssuerX500Principal(self): + return Principal() + + def getSerialNumber(self): + return 0 + + def getNotBefore(self): + from datetime import datetime + return datetime.now() + + def getNotAfter(self): + from datetime import datetime + return datetime.now() + + def getSigAlgName(self): + return "SHA256withRSA" + + def getSigAlgOID(self): + return "1.2.840.113549.1.1.11" + + def getVersion(self): + return 3 + + def getPublicKey(self): + return PublicKey() + + def getEncoded(self): + return self._encoded + + def verify(self, key): + return True + + def checkValidity(self, date=None): + return True + + def toString(self): + return "X509Certificate" + + +# ============================================================ +# AlgorithmParameters / AlgorithmParameterGenerator (stubs) +# ============================================================ + +class AlgorithmParameters(Stub): + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + + @staticmethod + def getInstance(algorithm, provider=None): + return AlgorithmParameters(algorithm) + + def init(self, params, format=None): + pass + + def getEncoded(self, format=None): + return b"" + + def getAlgorithm(self): + return self._algorithm + + def getProvider(self): + return Provider("SUN", 1.0, "SUN provider") + + def toString(self): + return f"{self._algorithm} parameters" + + +class AlgorithmParametersSpi(Stub): + pass + + +class AlgorithmParameterGenerator(Stub): + @staticmethod + def getInstance(algorithm, provider=None): + return AlgorithmParameterGenerator() + + def init(self, size_or_genparam, random=None): + pass + + def generateParameters(self): + return AlgorithmParameters("DSA") + + +class AlgorithmParameterGeneratorSpi(Stub): + pass + + +# ============================================================ +# Permission / AllPermission / BasicPermission (stubs) +# ============================================================ + +class Permission(Stub): + def __init__(self, name): + super().__init__() + self._name = name + + def getName(self): + return self._name + + def getActions(self): + return "" + + def implies(self, permission): + return True + + def equals(self, obj): + return isinstance(obj, Permission) and self._name == obj._name + + def hashCode(self): + return hash(self._name) + + def toString(self): + return f"({self.__class__.__name__} {self._name})" + + +class AllPermission(Permission): + def __init__(self): + super().__init__("") + + +class BasicPermission(Permission): + def __init__(self, name, actions=None): + super().__init__(name) + + +class PermissionCollection(Stub): + def __init__(self): + super().__init__() + self._permissions = [] + + def add(self, permission): + self._permissions.append(permission) + + def implies(self, permission): + return True + + def elements(self): + return iter(self._permissions) + + def toString(self): + return str(self._permissions) + + +class UnresolvedPermission(Permission): + def __init__(self, type_name, name, actions, certs): + super().__init__(name) + self._unresolved_type = type_name + self._unresolved_actions = actions + self._certs = certs + + def getUnresolvedType(self): + return self._unresolved_type + + def getUnresolvedActions(self): + return self._unresolved_actions + + def getUnresolvedCerts(self): + return self._certs + + +# ============================================================ +# Principal / CodeSource / Policy (stubs) +# ============================================================ + +class Principal(Stub): + def __init__(self, name=""): + super().__init__() + self._name = name + + def getName(self): + return self._name + + def toString(self): + return self._name + + def equals(self, other): + return isinstance(other, Principal) and self._name == other._name + + def hashCode(self): + return hash(self._name) + + +class CodeSource(Stub): + def __init__(self, location, certs): + super().__init__() + self._location = location + + def getLocation(self): + return self._location + + def getCertificates(self): + return [] + + def implies(self, codesource): + return True + + +class Policy(Stub): + _policy = None + + @staticmethod + def getPolicy(): + if Policy._policy is None: + Policy._policy = Policy() + return Policy._policy + + @staticmethod + def setPolicy(policy): + Policy._policy = policy + + def getPermissions(self, codesource): + return PermissionCollection() + + def implies(self, cs, perm): + return True + + def refresh(self): + pass + + +# ============================================================ +# CertPath / CertPathBuilder / CertPathValidator (stubs) +# ============================================================ + +class CertPath(Stub): + def __init__(self, algorithm): + super().__init__() + self._algorithm = algorithm + self._certs = [] + + def getCertificates(self): + return self._certs + + def getType(self): + return self._algorithm + + def getEncoded(self): + return b"" + + def toString(self): + return f"CertPath: {self._algorithm}" + + +class CertPathBuilder(Stub): + @staticmethod + def getInstance(algorithm, provider=None): + return CertPathBuilder() + + def build(self, params): + return CertPath("X.509") + + +class CertPathBuilderSpi(Stub): + pass + + +class CertPathValidator(Stub): + @staticmethod + def getInstance(algorithm, provider=None): + return CertPathValidator() + + def validate(self, cert_path, params): + return None + + +class CertPathValidatorSpi(Stub): + pass + + +class CertificateFactory(Stub): + @staticmethod + def getInstance(algorithm, provider=None): + return CertificateFactory() + + def generateCertificate(self, in_stream): + return X509Certificate() + + def generateCertPath(self, in_stream): + return CertPath("X.509") + + def getCertPathEncodings(self): + return ["PKCS7"] + + +class CertificateFactorySpi(Stub): + pass + + +class CertStore(Stub): + @staticmethod + def getInstance(type_name, params, provider=None): + return CertStore() + + def getCertificates(self, selector): + return [] + + def getCRLs(self, selector): + return [] + + +class CertStoreSpi(Stub): + pass + + +# ============================================================ +# CRL / X509CRL (stubs) +# ============================================================ + +class CRL(Stub): + def __init__(self): + super().__init__() + + def isRevoked(self, cert): + return False + + def toString(self): + return "CRL" + + +class X509CRL(CRL): + def __init__(self): + super().__init__() + + def getEncoded(self): + return b"" + + def verify(self, key): + return True + + def getIssuerX500Principal(self): + return Principal() + + def toString(self): + return "X509CRL" + + +class X509Extension(Stub): + pass + + +# ============================================================ +# Guard / GuardedObject (stubs) +# ============================================================ + +class Guard(Stub): + def checkGuard(self, object): + pass + + +class GuardedObject(Stub): + def __init__(self, object, guard): + super().__init__() + self._object = object + + def getObject(self): + return self._object + + +# ============================================================ +# Identity / IdentityScope / Signer (stubs) +# ============================================================ + +class Identity(Stub): + def __init__(self, name, scope=None): + super().__init__() + self._name = name + + def getName(self): + return self._name + + def getPublicKey(self): + return PublicKey() + + def setPublicKey(self, key): + pass + + def toString(self): + return self._name + + def getScope(self): + return None + + +class IdentityScope(Identity): + def __init__(self, name): + super().__init__(name) + + def getIdentity(self, name): + return None + + def addIdentity(self, identity): + pass + + def removeIdentity(self, identity): + pass + + def identities(self): + return [] + + @staticmethod + def getSystemScope(): + return None + + def size(self): + return 0 + + def toString(self): + return self._name + + +class Signer(Identity): + def __init__(self, name): + super().__init__(name) + + def getPrivateKey(self): + return PrivateKey() + + def setKeyPair(self, pair): + pass + + def toString(self): + return self._name + + +# ============================================================ +# CodeSigner / Timestamp (stubs) +# ============================================================ + +class Timestamp(Stub): + def __init__(self, timestamp, signer_cert_path): + super().__init__() + self._timestamp = timestamp + + def getTimestamp(self): + return self._timestamp + + def getSignerCertPath(self): + return CertPath("X.509") + + def equals(self, other): + return isinstance(other, Timestamp) + + def hashCode(self): + return 0 + + def toString(self): + return "Timestamp" + + +class CodeSigner(Stub): + def __init__(self, signer_cert_path, timestamp): + super().__init__() + self._signer_cert_path = signer_cert_path + self._timestamp = timestamp + + def getSignerCertPath(self): + return self._signer_cert_path + + def getTimestamp(self): + return self._timestamp + + def equals(self, other): + return isinstance(other, CodeSigner) + + def hashCode(self): + return 0 + + def toString(self): + return "CodeSigner" \ No newline at end of file diff --git a/android_pyapi/java/text.py b/android_pyapi/java/text.py new file mode 100644 index 0000000..3e36ffa --- /dev/null +++ b/android_pyapi/java/text.py @@ -0,0 +1,1041 @@ +""" +Java text standard library APIs implemented in Python. +java.text package - DateFormat, SimpleDateFormat, DecimalFormat, NumberFormat, etc. +""" + +from .._core import Stub +from .lang import RuntimeException +import datetime as _datetime +import time as _time +import re as _re +import math as _py_math +import locale as _locale +from decimal import Decimal as _Decimal + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "SimpleDateFormat", "DateFormat", "DateFormatSymbols", + "DecimalFormat", "DecimalFormatSymbols", "NumberFormat", + "ChoiceFormat", "MessageFormat", "FieldPosition", "ParsePosition", + "ParseException", "Normalizer", + "StringCharacterIterator", "CharacterIterator", + "BreakIterator", "Collator", "CollationKey", "RuleBasedCollator", + "Annotation", "AttributedCharacterIterator", "AttributedString", + "Bidi", "Format", +] + + +# ============================================================ +# ParseException +# ============================================================ + +class ParseException(Exception): + def __init__(self, s, error_offset): + super().__init__(s) + self._error_offset = error_offset + + def getErrorOffset(self): + return self._error_offset + + +# ============================================================ +# ParsePosition +# ============================================================ + +class ParsePosition(Stub): + def __init__(self, index): + super().__init__() + self._index = index + self._error_index = -1 + + def getIndex(self): + return self._index + + def setIndex(self, index): + self._index = index + + def getErrorIndex(self): + return self._error_index + + def setErrorIndex(self, ei): + self._error_index = ei + + +# ============================================================ +# FieldPosition +# ============================================================ + +class FieldPosition(Stub): + def __init__(self, field): + super().__init__() + self._field = field + self._begin = 0 + self._end = 0 + + def getField(self): + return self._field + + def getBeginIndex(self): + return self._begin + + def getEndIndex(self): + return self._end + + def setBeginIndex(self, bi): + self._begin = bi + + def setEndIndex(self, ei): + self._end = ei + + +# ============================================================ +# Format (base) +# ============================================================ + +class Format(Stub): + def format(self, obj, to_append_to=None, pos=None): + raise NotImplementedError + + def parseObject(self, source, pos=None): + raise NotImplementedError + + +# ============================================================ +# DateFormat +# ============================================================ + +class DateFormat(Format): + SHORT = 3 + MEDIUM = 2 + LONG = 1 + FULL = 0 + DEFAULT = 2 + + def __init__(self): + super().__init__() + self._calendar = None + self._number_format = None + self._lenient = False + self._timezone = None + + @staticmethod + def getDateInstance(style=2, locale=None): + return SimpleDateFormat("yyyy-MM-dd") + + @staticmethod + def getTimeInstance(style=2, locale=None): + return SimpleDateFormat("HH:mm:ss") + + @staticmethod + def getDateTimeInstance(date_style=2, time_style=2, locale=None): + return SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + + @staticmethod + def getInstance(): + return SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + + def format(self, date, to_append_to=None, pos=None): + raise NotImplementedError + + def parse(self, source, pos=None): + raise NotImplementedError + + def setLenient(self, lenient): + self._lenient = lenient + + def isLenient(self): + return self._lenient + + def getCalendar(self): + return self._calendar + + def setCalendar(self, cal): + self._calendar = cal + + def getNumberFormat(self): + return self._number_format + + def setNumberFormat(self, nf): + self._number_format = nf + + def getTimeZone(self): + return self._timezone + + def setTimeZone(self, tz): + self._timezone = tz + + +# ============================================================ +# DateFormatSymbols +# ============================================================ + +class DateFormatSymbols(Stub): + def __init__(self, locale=None): + super().__init__() + self._months = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", + ] + self._short_months = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ] + self._weekdays = [ + "", "Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday", + ] + self._short_weekdays = [ + "", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + ] + self._ampm = ["AM", "PM"] + self._eras = ["BC", "AD"] + self._zone_strings = [] + self._local_pattern_chars = "GyMdkHmsSEDFwWahKzZ" + + def getMonths(self): + return self._months + + def setMonths(self, months): + self._months = months + + def getShortMonths(self): + return self._short_months + + def setShortMonths(self, months): + self._short_months = months + + def getWeekdays(self): + return self._weekdays + + def setWeekdays(self, wd): + self._weekdays = wd + + def getShortWeekdays(self): + return self._short_weekdays + + def setShortWeekdays(self, wd): + self._short_weekdays = wd + + def getAmPmStrings(self): + return self._ampm + + def setAmPmStrings(self, ap): + self._ampm = ap + + def getZoneStrings(self): + return self._zone_strings + + def setZoneStrings(self, zs): + self._zone_strings = zs + + def getLocalPatternChars(self): + return self._local_pattern_chars + + def setLocalPatternChars(self, chars): + self._local_pattern_chars = chars + + def getEras(self): + return self._eras + + def setEras(self, eras): + self._eras = eras + + +# ============================================================ +# SimpleDateFormat +# ============================================================ + +_JAVA_TO_PYTHON_FORMAT = { + "yyyy": "%Y", "yy": "%y", + "MM": "%m", + "dd": "%d", + "HH": "%H", + "mm": "%M", + "ss": "%S", + "SSS": "%f", + "a": "%p", + "EEE": "%a", "EEEE": "%A", + "D": "%j", + "zzz": "%Z", "z": "%Z", + "Z": "%z", +} + +class SimpleDateFormat(DateFormat): + def __init__(self, pattern, locale=None): + super().__init__() + self._pattern = pattern + self._locale = locale + self._dfs = DateFormatSymbols(locale) + self._lenient = False + + def applyPattern(self, pattern): + self._pattern = pattern + + def toPattern(self): + return self._pattern + + def toLocalizedPattern(self): + return self._pattern + + def applyLocalizedPattern(self, pattern): + self._pattern = pattern + + def _java_to_py(self, pattern): + py_pattern = pattern + sorted_keys = sorted(_JAVA_TO_PYTHON_FORMAT.keys(), key=lambda x: -len(x)) + for java_fmt in sorted_keys: + py_fmt = _JAVA_TO_PYTHON_FORMAT[java_fmt] + py_pattern = py_pattern.replace(java_fmt, py_fmt) + py_pattern = py_pattern.replace("'", "") + return py_pattern + + def _to_datetime(self, date): + if isinstance(date, _datetime.datetime): + return date + if isinstance(date, _datetime.date): + return _datetime.datetime.combine(date, _datetime.time()) + try: + ts = date.getTime() / 1000.0 + return _datetime.datetime.fromtimestamp(ts) + except AttributeError: + return _datetime.datetime.now() + + def format(self, date, to_append_to=None, pos=None): + py_pattern = self._java_to_py(self._pattern) + dt = self._to_datetime(date) + result = dt.strftime(py_pattern) + if to_append_to is not None: + sb = str(to_append_to) + idx = len(sb) + to_append_to.append(result) + if pos is not None: + pos.setBeginIndex(idx) + pos.setEndIndex(idx + len(result)) + return to_append_to + return result + + def parse(self, source, pos=None): + py_pattern = self._java_to_py(self._pattern) + try: + dt = _datetime.datetime.strptime(source, py_pattern) + if pos is not None: + pos.setIndex(len(source)) + return dt + except ValueError: + if pos is not None: + pos.setErrorIndex(0) + raise ParseException(f"Unparseable date: {source}", 0) + + def setLenient(self, lenient): + self._lenient = lenient + + def isLenient(self): + return self._lenient + + def setTimeZone(self, tz): + self._timezone = tz + + def getTimeZone(self): + return self._timezone + + def setCalendar(self, cal): + self._calendar = cal + + def getCalendar(self): + return self._calendar + + def setNumberFormat(self, nf): + self._number_format = nf + + def getNumberFormat(self): + return self._number_format + + def getDateFormatSymbols(self): + return self._dfs + + def setDateFormatSymbols(self, dfs): + self._dfs = dfs + + +# ============================================================ +# DecimalFormatSymbols +# ============================================================ + +class DecimalFormatSymbols(Stub): + def __init__(self, locale=None): + super().__init__() + self._decimal_sep = "." + self._grouping_sep = "," + self._pattern_sep = ";" + self._minus_sign = "-" + self._infinity = "\u221e" + self._nan = "NaN" + self._percent = "%" + self._per_mill = "\u2030" + self._zero_digit = "0" + self._digit = "#" + self._exponential_sep = "E" + self._monetary_decimal_sep = "." + self._intl_currency_symbol = "USD" + self._currency = "USD" + + def getDecimalSeparator(self): return self._decimal_sep + def setDecimalSeparator(self, c): self._decimal_sep = c + def getGroupingSeparator(self): return self._grouping_sep + def setGroupingSeparator(self, c): self._grouping_sep = c + def getPatternSeparator(self): return self._pattern_sep + def setPatternSeparator(self, c): self._pattern_sep = c + def getMinusSign(self): return self._minus_sign + def setMinusSign(self, c): self._minus_sign = c + def getInfinity(self): return self._infinity + def setInfinity(self, s): self._infinity = s + def getNaN(self): return self._nan + def setNaN(self, s): self._nan = s + def getPercent(self): return self._percent + def setPercent(self, c): self._percent = c + def getPerMill(self): return self._per_mill + def setPerMill(self, c): self._per_mill = c + def getZeroDigit(self): return self._zero_digit + def setZeroDigit(self, c): self._zero_digit = c + def getDigit(self): return self._digit + def setDigit(self, c): self._digit = c + def getExponentialSeparator(self): return self._exponential_sep + def setExponentialSeparator(self, s): self._exponential_sep = s + def getMonetaryDecimalSeparator(self): return self._monetary_decimal_sep + def setMonetaryDecimalSeparator(self, c): self._monetary_decimal_sep = c + def getInternationalCurrencySymbol(self): return self._intl_currency_symbol + def setInternationalCurrencySymbol(self, s): self._intl_currency_symbol = s + def getCurrency(self): return self._currency + def setCurrency(self, c): self._currency = c + + +# ============================================================ +# DecimalFormat +# ============================================================ + +class NumberFormat(Format): + def __init__(self): + super().__init__() + self._grouping_used = True + self._max_int_digits = 2000000000 + self._min_int_digits = 1 + self._max_frac_digits = 3 + self._min_frac_digits = 0 + self._parse_integer_only = False + self._rounding_mode = 4 + + @staticmethod + def getInstance(locale=None): + return DecimalFormat("#,##0.###") + + @staticmethod + def getNumberInstance(locale=None): + return DecimalFormat("#,##0.###") + + @staticmethod + def getCurrencyInstance(locale=None): + return DecimalFormat("#,##0.00") + + @staticmethod + def getPercentInstance(locale=None): + return DecimalFormat("#,##0%") + + @staticmethod + def getIntegerInstance(locale=None): + nf = DecimalFormat("#,##0") + nf.setMaximumFractionDigits(0) + return nf + + @staticmethod + def getCompactNumberInstance(locale=None, style=None): + return DecimalFormat("#,##0") + + def format(self, number, to_append_to=None, pos=None): + raise NotImplementedError + + def parse(self, source, pos=None): + raise NotImplementedError + + def isGroupingUsed(self): return self._grouping_used + def setGroupingUsed(self, v): self._grouping_used = v + def getMaximumIntegerDigits(self): return self._max_int_digits + def setMaximumIntegerDigits(self, v): self._max_int_digits = v + def getMinimumIntegerDigits(self): return self._min_int_digits + def setMinimumIntegerDigits(self, v): self._min_int_digits = v + def getMaximumFractionDigits(self): return self._max_frac_digits + def setMaximumFractionDigits(self, v): self._max_frac_digits = v + def getMinimumFractionDigits(self): return self._min_frac_digits + def setMinimumFractionDigits(self, v): self._min_frac_digits = v + def setParseIntegerOnly(self, v): self._parse_integer_only = v + def isParseIntegerOnly(self): return self._parse_integer_only + def setRoundingMode(self, v): self._rounding_mode = v + def getRoundingMode(self): return self._rounding_mode + def getCurrency(self): return "USD" + def setCurrency(self, c): pass + + @staticmethod + def getAvailableLocales(): + return list(_locale.locale_alias.keys()) + + +# ============================================================ +# ChoiceFormat +# ============================================================ + +class ChoiceFormat(NumberFormat): + def __init__(self, formats=None, limits=None): + super().__init__() + self._formats = formats if formats is not None else [] + self._limits = limits if limits is not None else [] + + def format(self, number, to_append_to=None, pos=None): + val = float(number) + idx = 0 + for i, lim in enumerate(self._limits): + if val >= lim: + idx = i + result = str(self._formats[idx]) if idx < len(self._formats) else "" + if to_append_to is not None: + sb = str(to_append_to) + idx2 = len(sb) + to_append_to.append(result) + if pos is not None: + pos.setBeginIndex(idx2) + pos.setEndIndex(idx2 + len(result)) + return to_append_to + return result + + def parse(self, source, pos=None): + raise NotImplementedError + + @staticmethod + def nextDouble(d, upwards=True): + if upwards: + return d + _py_math.ulp(d) + return d - _py_math.ulp(d) + + @staticmethod + def previousDouble(d): + return ChoiceFormat.nextDouble(d, False) + + def getLimits(self): + return self._limits + + def getFormats(self): + return self._formats + + +# ============================================================ +# MessageFormat +# ============================================================ +# NumberFormat +# ============================================================ + +# ============================================================ +# DecimalFormat +# ============================================================ + +class DecimalFormat(NumberFormat): + def __init__(self, pattern=None, symbols=None): + super().__init__() + self._pattern = pattern or "#,##0.###" + self._symbols = symbols or DecimalFormatSymbols() + self._decimal_sep_always = False + self._grouping_size = 3 + self._grouping_used = True + self._max_int_digits = 2000000000 + self._min_int_digits = 1 + self._max_frac_digits = 2000000000 + self._min_frac_digits = 0 + self._neg_prefix = "-" + self._neg_suffix = "" + self._pos_prefix = "" + self._pos_suffix = "" + self._multiplier = 1 + self._rounding_mode = 4 + self._parse_bigdecimal = False + + def applyPattern(self, pattern): + self._pattern = pattern + + def toPattern(self): + return self._pattern + + def toLocalizedPattern(self): + return self._pattern + + def applyLocalizedPattern(self, pattern): + self._pattern = pattern + + def format(self, number, to_append_to=None, pos=None): + if isinstance(number, (int, float)): + val = number * self._multiplier + elif isinstance(number, _Decimal): + val = float(str(number)) * self._multiplier + else: + val = float(str(number)) * self._multiplier + if self._grouping_used and self._grouping_size > 0: + result = f"{val:,.{self._max_frac_digits}f}" + else: + fmt = "{{:.{}f}}".format(self._max_frac_digits) + result = fmt.format(val) + prefix = self._pos_prefix if val >= 0 else self._neg_prefix + suffix = self._pos_suffix if val >= 0 else self._neg_suffix + result = prefix + result + suffix + if to_append_to is not None: + sb = str(to_append_to) + idx = len(sb) + to_append_to.append(result) + if pos is not None: + pos.setBeginIndex(idx) + pos.setEndIndex(idx + len(result)) + return to_append_to + return result + + def parse(self, source, pos=None): + text = source.strip() + try: + val = float(text) + if pos is not None: + pos.setIndex(len(source)) + if self._parse_bigdecimal: + return _Decimal(str(val)) + return val + except ValueError: + if pos is not None: + pos.setErrorIndex(0) + raise Exception(f"Unparseable: {source}") + + def setDecimalSeparatorAlwaysShown(self, v): self._decimal_sep_always = v + def getDecimalSeparatorAlwaysShown(self): return self._decimal_sep_always + def setGroupingSize(self, v): self._grouping_size = v + def getGroupingSize(self): return self._grouping_size + def setGroupingUsed(self, v): self._grouping_used = v + def isGroupingUsed(self): return self._grouping_used + def setMaximumIntegerDigits(self, v): self._max_int_digits = v + def getMaximumIntegerDigits(self): return self._max_int_digits + def setMinimumIntegerDigits(self, v): self._min_int_digits = v + def getMinimumIntegerDigits(self): return self._min_int_digits + def setMaximumFractionDigits(self, v): self._max_frac_digits = v + def getMaximumFractionDigits(self): return self._max_frac_digits + def setMinimumFractionDigits(self, v): self._min_frac_digits = v + def getMinimumFractionDigits(self): return self._min_frac_digits + def setNegativePrefix(self, v): self._neg_prefix = v + def getNegativePrefix(self): return self._neg_prefix + def setNegativeSuffix(self, v): self._neg_suffix = v + def getNegativeSuffix(self): return self._neg_suffix + def setPositivePrefix(self, v): self._pos_prefix = v + def getPositivePrefix(self): return self._pos_prefix + def setPositiveSuffix(self, v): self._pos_suffix = v + def getPositiveSuffix(self): return self._pos_suffix + def setDecimalFormatSymbols(self, v): self._symbols = v + def getDecimalFormatSymbols(self): return self._symbols + def setParseBigDecimal(self, v): self._parse_bigdecimal = v + def isParseBigDecimal(self): return self._parse_bigdecimal + def setMultiplier(self, v): self._multiplier = v + def getMultiplier(self): return self._multiplier + def setRoundingMode(self, v): self._rounding_mode = v + def getRoundingMode(self): return self._rounding_mode + + +# ============================================================ +# MessageFormat +# ============================================================ + +_MSG_PATTERN_RE = _re.compile(r"\{(\d+)(?:,(\w+)(?:,(.+))?)?\}") + +class MessageFormat(Format): + def __init__(self, pattern, locale=None): + super().__init__() + self._pattern = pattern + self._locale = locale + self._formats = {} + + def applyPattern(self, pattern): + self._pattern = pattern + + def toPattern(self): + return self._pattern + + def _format_impl(self, objects): + def replacer(m): + idx = int(m.group(1)) + if idx < len(objects): + return str(objects[idx]) + return m.group(0) + return _MSG_PATTERN_RE.sub(replacer, self._pattern) + + def format(self, objects, to_append_to=None, pos=None): + result = self._format_impl(objects) + if to_append_to is not None: + sb = str(to_append_to) + idx = len(sb) + to_append_to.append(result) + if pos is not None: + pos.setBeginIndex(idx) + pos.setEndIndex(idx + len(result)) + return to_append_to + return result + + @staticmethod + def format_static(pattern, *args): + mf = MessageFormat(pattern) + return mf.format(args) + + def parse(self, source, pos=None): + parts = _MSG_PATTERN_RE.split(self._pattern) + result = [] + idx = 0 + for i, part in enumerate(parts): + if i % 2 == 0: + if not source[idx:].startswith(part): + if pos is not None: + pos.setErrorIndex(idx) + return result + idx += len(part) + else: + arg_idx = int(part) + remaining = source[idx:] + next_part = parts[i + 1] if i + 1 < len(parts) else "" + end = remaining.find(next_part) if next_part else len(remaining) + if end < 0: + end = len(remaining) + result.append(remaining[:end]) + idx += end + if pos is not None: + pos.setIndex(idx) + return result + + def setLocale(self, locale): + self._locale = locale + + def getLocale(self): + return self._locale + + def setFormats(self, formats): + self._formats = formats + + def getFormats(self): + return self._formats + + def setFormat(self, idx, fmt): + self._formats[idx] = fmt + + def getFormat(self, idx): + return self._formats.get(idx) + + +# ============================================================ +# CharacterIterator +# ============================================================ + +class CharacterIterator(Stub): + DONE = chr(0xFFFF) + + def first(self): + raise NotImplementedError + + def last(self): + raise NotImplementedError + + def current(self): + raise NotImplementedError + + def next(self): + raise NotImplementedError + + def previous(self): + raise NotImplementedError + + def setIndex(self, position): + raise NotImplementedError + + def getBeginIndex(self): + raise NotImplementedError + + def getEndIndex(self): + raise NotImplementedError + + def getIndex(self): + raise NotImplementedError + + def clone(self): + raise NotImplementedError + + +# ============================================================ +# StringCharacterIterator +# ============================================================ + +class StringCharacterIterator(CharacterIterator): + def __init__(self, text): + super().__init__() + self._text = text + self._begin = 0 + self._end = len(text) + self._pos = 0 + + def first(self): + self._pos = self._begin + return self.current() + + def last(self): + self._pos = self._end - 1 if self._end > self._begin else self._begin + return self.current() + + def current(self): + if self._pos >= self._begin and self._pos < self._end: + return self._text[self._pos] + return CharacterIterator.DONE + + def next(self): + self._pos += 1 + if self._pos >= self._end: + self._pos = self._end + return CharacterIterator.DONE + return self._text[self._pos] + + def previous(self): + self._pos -= 1 + if self._pos < self._begin: + self._pos = self._begin + return CharacterIterator.DONE + return self._text[self._pos] + + def setIndex(self, pos): + self._pos = max(self._begin, min(pos, self._end)) + return self.current() + + def getBeginIndex(self): + return self._begin + + def getEndIndex(self): + return self._end + + def getIndex(self): + return self._pos + + def clone(self): + return StringCharacterIterator(self._text) + + +# ============================================================ +# BreakIterator +# ============================================================ + +class BreakIterator(Stub): + DONE = -1 + + @staticmethod + def getWordInstance(locale=None): + return BreakIterator() + + @staticmethod + def getLineInstance(locale=None): + return BreakIterator() + + @staticmethod + def getCharacterInstance(locale=None): + return BreakIterator() + + @staticmethod + def getSentenceInstance(locale=None): + return BreakIterator() + + def setText(self, text): + self._text = text + + def first(self): + return 0 + + def last(self): + return len(self._text) if hasattr(self, "_text") else 0 + + def next(self, n=1): + return BreakIterator.DONE + + def previous(self): + return BreakIterator.DONE + + def current(self): + return 0 + + def following(self, offset): + return BreakIterator.DONE + + def preceding(self, offset): + return BreakIterator.DONE + + def isBoundary(self, offset): + return True + + +# ============================================================ +# Collator / CollationKey / RuleBasedCollator +# ============================================================ + +class CollationKey(Stub): + def __init__(self, source, key): + super().__init__() + self._source = source + self._key = key + + def compareTo(self, other): + if isinstance(other, CollationKey): + return (self._key > other._key) - (self._key < other._key) + return -1 + + def getSourceString(self): + return self._source + + def toByteArray(self): + return self._key.encode("utf-8") + + +class Collator(Stub): + PRIMARY = 0 + SECONDARY = 1 + TERTIARY = 2 + IDENTICAL = 3 + NO_DECOMPOSITION = 0 + CANONICAL_DECOMPOSITION = 1 + FULL_DECOMPOSITION = 2 + + @staticmethod + def getInstance(locale=None): + return RuleBasedCollator() + + def compare(self, source, target): + return (source > target) - (source < target) + + def getCollationKey(self, source): + return CollationKey(source, source) + + def setStrength(self, strength): + self._strength = strength + + def getStrength(self): + return getattr(self, "_strength", Collator.TERTIARY) + + def setDecomposition(self, decomposition): + self._decomposition = decomposition + + def getDecomposition(self): + return getattr(self, "_decomposition", Collator.NO_DECOMPOSITION) + + +class RuleBasedCollator(Collator): + def __init__(self, rules=None): + super().__init__() + self._rules = rules or "" + + +# ============================================================ +# Normalizer +# ============================================================ + +class Normalizer(Stub): + class Form(Stub): + NFC = "NFC" + NFD = "NFD" + NFKC = "NFKC" + NFKD = "NFKD" + + @staticmethod + def normalize(src, form): + import unicodedata + form_map = { + Normalizer.Form.NFC: "NFC", + Normalizer.Form.NFD: "NFD", + Normalizer.Form.NFKC: "NFKC", + Normalizer.Form.NFKD: "NFKD", + } + return unicodedata.normalize(form_map.get(form, "NFC"), src) + + @staticmethod + def isNormalized(src, form): + return Normalizer.normalize(src, form) == src + + +# ============================================================ +# Annotation / AttributedCharacterIterator / AttributedString +# ============================================================ + +class Annotation(Stub): + def __init__(self, value): + super().__init__() + self._value = value + + def getValue(self): + return self._value + + def toString(self): + return str(self._value) + + +class AttributedCharacterIterator(CharacterIterator): + pass + + +class AttributedString(Stub): + def __init__(self, text, attributes=None): + super().__init__() + self._text = text + + def getIterator(self): + return StringCharacterIterator(self._text) + + +# ============================================================ +# Bidi +# ============================================================ + +class Bidi(Stub): + DIRECTION_LEFT_TO_RIGHT = 0 + DIRECTION_RIGHT_TO_LEFT = 1 + DIRECTION_DEFAULT_LEFT_TO_RIGHT = -2 + DIRECTION_DEFAULT_RIGHT_TO_LEFT = -1 + + def __init__(self, text, flags=0): + super().__init__() + self._text = text + self._base_level = 0 if flags >= 0 else 1 + + def isLeftToRight(self): + return self._base_level == 0 + + def isRightToLeft(self): + return self._base_level == 1 + + def getLength(self): + return len(self._text) + + def getLevelAt(self, offset): + return self._base_level + + def getRunCount(self): + return 1 + + def getRunLevel(self, run): + return self._base_level + + def getRunLimit(self, run): + return len(self._text) + + def getRunStart(self, run): + return 0 + + def requiresBidi(self, text): + for ch in text: + if ord(ch) > 0x590: + return True + return False + + @staticmethod + def reorderVisually(levels, objects, index, count): + pass + + @staticmethod + def baseIsLeftToRight(): + return True \ No newline at end of file diff --git a/android_pyapi/java/time.py b/android_pyapi/java/time.py new file mode 100644 index 0000000..c68f1cc --- /dev/null +++ b/android_pyapi/java/time.py @@ -0,0 +1,1619 @@ +""" +Java time standard library APIs implemented in Python. +java.time package - LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period, etc. +""" + +from .._core import Stub +from .lang import RuntimeException, Comparable +import datetime as _dt +import re as _re +import math as _py_math + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +__all__ = [ + "LocalDate", "LocalTime", "LocalDateTime", + "ZonedDateTime", "OffsetDateTime", "OffsetTime", + "Instant", "Duration", "Period", + "ZoneId", "ZoneOffset", "ZoneRules", + "DayOfWeek", "Month", "Year", "YearMonth", "MonthDay", + "Clock", + "DateTimeFormatter", "DateTimeFormatterBuilder", + "TemporalAdjusters", + "Temporal", "TemporalAccessor", "TemporalUnit", + "ChronoUnit", "ChronoField", +] + + +# ============================================================ +# TemporalAccessor / Temporal / TemporalUnit (base interfaces) +# ============================================================ + +class TemporalAccessor(Stub): + def get(self, field): + raise NotImplementedError + + def getLong(self, field): + raise NotImplementedError + + def isSupported(self, field): + raise NotImplementedError + + def range(self, field): + raise NotImplementedError + + +class Temporal(TemporalAccessor): + def plus(self, amount, unit=None): + raise NotImplementedError + + def minus(self, amount, unit=None): + raise NotImplementedError + + def with_(self, field, new_value): + raise NotImplementedError + + def until(self, end_exclusive, unit): + raise NotImplementedError + + +class TemporalUnit(Stub): + def getDuration(self): + raise NotImplementedError + + def isDurationEstimated(self): + raise NotImplementedError + + def isDateBased(self): + raise NotImplementedError + + def isTimeBased(self): + raise NotImplementedError + + def between(self, temporal1, temporal2): + raise NotImplementedError + + +# ============================================================ +# ChronoField +# ============================================================ + +class ChronoField(Stub): + NANO_OF_SECOND = "NanoOfSecond" + NANO_OF_DAY = "NanoOfDay" + MICRO_OF_SECOND = "MicroOfSecond" + MICRO_OF_DAY = "MicroOfDay" + MILLI_OF_SECOND = "MilliOfSecond" + MILLI_OF_DAY = "MilliOfDay" + SECOND_OF_MINUTE = "SecondOfMinute" + SECOND_OF_DAY = "SecondOfDay" + MINUTE_OF_HOUR = "MinuteOfHour" + MINUTE_OF_DAY = "MinuteOfDay" + HOUR_OF_AMPM = "HourOfAmPm" + CLOCK_HOUR_OF_AMPM = "ClockHourOfAmPm" + HOUR_OF_DAY = "HourOfDay" + CLOCK_HOUR_OF_DAY = "ClockHourOfDay" + AMPM_OF_DAY = "AmPmOfDay" + DAY_OF_WEEK = "DayOfWeek" + ALIGNED_DAY_OF_WEEK_IN_MONTH = "AlignedDayOfWeekInMonth" + ALIGNED_DAY_OF_WEEK_IN_YEAR = "AlignedDayOfWeekInYear" + DAY_OF_MONTH = "DayOfMonth" + DAY_OF_YEAR = "DayOfYear" + EPOCH_DAY = "EpochDay" + ALIGNED_WEEK_OF_MONTH = "AlignedWeekOfMonth" + ALIGNED_WEEK_OF_YEAR = "AlignedWeekOfYear" + MONTH_OF_YEAR = "MonthOfYear" + PROLEPTIC_MONTH = "ProlepticMonth" + YEAR_OF_ERA = "YearOfEra" + YEAR = "Year" + ERA = "Era" + INSTANT_SECONDS = "InstantSeconds" + OFFSET_SECONDS = "OffsetSeconds" + + def __init__(self, name): + super().__init__() + self._name = name + + def toString(self): + return self._name + + def getDisplayName(self, locale=None): + return self._name + + +# ============================================================ +# ChronoUnit +# ============================================================ + +class ChronoUnit(Stub): + NANOS = None + MICROS = None + MILLIS = None + SECONDS = None + MINUTES = None + HOURS = None + HALF_DAYS = None + DAYS = None + WEEKS = None + MONTHS = None + YEARS = None + DECADES = None + CENTURIES = None + MILLENNIA = None + ERAS = None + FOREVER = None + + def __init__(self, name, duration, estimated=False): + self._name = name + self._duration = duration + self._estimated = estimated + + def toString(self): + return self._name + + def getDuration(self): + return self._duration + + def isDurationEstimated(self): + return self._estimated + + def isDateBased(self): + return self in (ChronoUnit.WEEKS, ChronoUnit.MONTHS, ChronoUnit.YEARS, + ChronoUnit.DECADES, ChronoUnit.CENTURIES, + ChronoUnit.MILLENNIA, ChronoUnit.ERAS) + + def isTimeBased(self): + return self in (ChronoUnit.NANOS, ChronoUnit.MICROS, ChronoUnit.MILLIS, + ChronoUnit.SECONDS, ChronoUnit.MINUTES, ChronoUnit.HOURS, + ChronoUnit.HALF_DAYS) + + def between(self, temporal1, temporal2): + return temporal1.until(temporal2, self) + + @staticmethod + def from_(temporal): + if isinstance(temporal, ChronoUnit): + return temporal + raise Exception(f"Cannot convert to ChronoUnit: {temporal}") + + +# ChronoUnit 常量在 Duration 定义之后初始化(见下方) + + +# ============================================================ +# DayOfWeek +# ============================================================ + +class DayOfWeek(Stub): + MONDAY = None + TUESDAY = None + WEDNESDAY = None + THURSDAY = None + FRIDAY = None + SATURDAY = None + SUNDAY = None + + def __init__(self, value, name): + self._value = value + self._name = name + + def getValue(self): + return self._value + + def getDisplayName(self, style, locale=None): + return self._name + + def plus(self, days): + idx = ((self._value - 1 + days) % 7) + 1 + return DayOfWeek._from_value(idx) + + def minus(self, days): + return self.plus(-days) + + @staticmethod + def _from_value(v): + mapping = {1: DayOfWeek.MONDAY, 2: DayOfWeek.TUESDAY, 3: DayOfWeek.WEDNESDAY, + 4: DayOfWeek.THURSDAY, 5: DayOfWeek.FRIDAY, 6: DayOfWeek.SATURDAY, + 7: DayOfWeek.SUNDAY} + return mapping.get(v) + + def toString(self): + return self._name + + def compareTo(self, other): + if isinstance(other, DayOfWeek): + return self._value - other._value + return -1 + + +DayOfWeek.MONDAY = DayOfWeek(1, "MONDAY") +DayOfWeek.TUESDAY = DayOfWeek(2, "TUESDAY") +DayOfWeek.WEDNESDAY = DayOfWeek(3, "WEDNESDAY") +DayOfWeek.THURSDAY = DayOfWeek(4, "THURSDAY") +DayOfWeek.FRIDAY = DayOfWeek(5, "FRIDAY") +DayOfWeek.SATURDAY = DayOfWeek(6, "SATURDAY") +DayOfWeek.SUNDAY = DayOfWeek(7, "SUNDAY") + + +# ============================================================ +# Month +# ============================================================ + +class Month(Stub): + JANUARY = None + FEBRUARY = None + MARCH = None + APRIL = None + MAY = None + JUNE = None + JULY = None + AUGUST = None + SEPTEMBER = None + OCTOBER = None + NOVEMBER = None + DECEMBER = None + + def __init__(self, value, name): + self._value = value + self._name = name + + def getValue(self): + return self._value + + def getDisplayName(self, style, locale=None): + return self._name + + def plus(self, months): + idx = ((self._value - 1 + months) % 12) + 1 + return Month._from_value(idx) + + def minus(self, months): + return self.plus(-months) + + def length(self, leap_year=False): + if self._value == 2: + return 29 if leap_year else 28 + if self._value in (4, 6, 9, 11): + return 30 + return 31 + + def minLength(self): + return 29 if self._value == 2 else (30 if self._value in (4, 6, 9, 11) else 31) + + def maxLength(self): + return 29 if self._value == 2 else (30 if self._value in (4, 6, 9, 11) else 31) + + def firstDayOfYear(self, leap_year=False): + days = 0 + for m in range(1, self._value): + days += Month._from_value(m).length(leap_year) + return days + 1 + + def firstMonthOfQuarter(self): + q = (self._value - 1) // 3 + return Month._from_value(q * 3 + 1) + + @staticmethod + def _from_value(v): + values = [None, Month.JANUARY, Month.FEBRUARY, Month.MARCH, Month.APRIL, + Month.MAY, Month.JUNE, Month.JULY, Month.AUGUST, + Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER, Month.DECEMBER] + return values[v] if 0 < v <= 12 else None + + def toString(self): + return self._name + + def compareTo(self, other): + if isinstance(other, Month): + return self._value - other._value + return -1 + + +Month.JANUARY = Month(1, "JANUARY") +Month.FEBRUARY = Month(2, "FEBRUARY") +Month.MARCH = Month(3, "MARCH") +Month.APRIL = Month(4, "APRIL") +Month.MAY = Month(5, "MAY") +Month.JUNE = Month(6, "JUNE") +Month.JULY = Month(7, "JULY") +Month.AUGUST = Month(8, "AUGUST") +Month.SEPTEMBER = Month(9, "SEPTEMBER") +Month.OCTOBER = Month(10, "OCTOBER") +Month.NOVEMBER = Month(11, "NOVEMBER") +Month.DECEMBER = Month(12, "DECEMBER") + + +# ============================================================ +# ZoneId / ZoneOffset / ZoneRules +# ============================================================ + +class ZoneId(Stub): + SHORT_IDS = { + "ACT": "Australia/Darwin", + "AET": "Australia/Sydney", + "AGT": "America/Argentina/Buenos_Aires", + "ART": "Africa/Cairo", + "AST": "America/Anchorage", + "BET": "America/Sao_Paulo", + "BST": "Asia/Dhaka", + "CAT": "Africa/Harare", + "CNT": "America/St_Johns", + "CST": "America/Chicago", + "CTT": "Asia/Shanghai", + "EAT": "Africa/Addis_Ababa", + "ECT": "Europe/Paris", + "EST": "America/New_York", + "HST": "Pacific/Honolulu", + "IET": "America/Indianapolis", + "IST": "Asia/Kolkata", + "JST": "Asia/Tokyo", + "MIT": "Pacific/Apia", + "MST": "America/Denver", + "NET": "Asia/Yerevan", + "NST": "Pacific/Auckland", + "PLT": "Asia/Karachi", + "PNT": "America/Phoenix", + "PRT": "America/Puerto_Rico", + "PST": "America/Los_Angeles", + "SST": "Pacific/Guadalcanal", + "VST": "Asia/Ho_Chi_Minh", + } + + _cache = {} + + def __init__(self, zone_id): + super().__init__() + self._id = zone_id + + @staticmethod + def systemDefault(): + import time as _t + idx = 1 if _t.daylight else 0 + tz_name = _t.tzname[idx] if idx < len(_t.tzname) and _t.tzname[idx] else _t.tzname[0] + return ZoneId.of(tz_name) if tz_name else ZoneId("UTC") + + @staticmethod + def of(zone_id): + if zone_id in ZoneId.SHORT_IDS: + zone_id = ZoneId.SHORT_IDS[zone_id] + if zone_id not in ZoneId._cache: + ZoneId._cache[zone_id] = ZoneId(zone_id) + return ZoneId._cache[zone_id] + + @staticmethod + def getAvailableZoneIds(): + import time as _t + return set(_t.tzname) + + def getDisplayName(self, style, locale=None): + return self._id + + def getRules(self): + return ZoneRules() + + def toString(self): + return self._id + + def equals(self, other): + if isinstance(other, ZoneId): + return self._id == other._id + return False + + def hashCode(self): + return hash(self._id) + + +class ZoneOffset(ZoneId): + MAX = None + MIN = None + UTC = None + + _cache = {} + + def __init__(self, total_seconds): + hours = total_seconds // 3600 + mins = (abs(total_seconds) % 3600) // 60 + sign = "+" if total_seconds >= 0 else "-" + if mins == 0: + zone_id = f"{sign}{abs(hours):02d}:00" + else: + zone_id = f"{sign}{abs(hours):02d}:{mins:02d}" + super().__init__(zone_id) + self._total_seconds = total_seconds + + @staticmethod + def of(offset_id): + if offset_id in ZoneOffset._cache: + return ZoneOffset._cache[offset_id] + if offset_id == "Z" or offset_id == "UTC" or offset_id == "GMT": + z = ZoneOffset(0) + ZoneOffset._cache[offset_id] = z + return z + sign = 1 if offset_id[0] == "+" else -1 + parts = offset_id[1:].split(":") + h = int(parts[0]) + m = int(parts[1]) if len(parts) > 1 else 0 + total = sign * (h * 3600 + m * 60) + z = ZoneOffset(total) + ZoneOffset._cache[offset_id] = z + return z + + @staticmethod + def ofHours(hours): + return ZoneOffset(hours * 3600) + + @staticmethod + def ofHoursMinutes(hours, minutes): + sign = 1 if hours >= 0 else -1 + return ZoneOffset(sign * (abs(hours) * 3600 + minutes * 60)) + + @staticmethod + def ofTotalSeconds(total_seconds): + if total_seconds in ZoneOffset._cache: + return ZoneOffset._cache[total_seconds] + z = ZoneOffset(total_seconds) + ZoneOffset._cache[total_seconds] = z + return z + + def getTotalSeconds(self): + return self._total_seconds + + def toString(self): + if self._total_seconds == 0: + return "Z" + return super().toString() + + def equals(self, other): + if isinstance(other, ZoneOffset): + return self._total_seconds == other._total_seconds + return False + + def hashCode(self): + return self._total_seconds + + +ZoneOffset.MAX = ZoneOffset(18 * 3600) +ZoneOffset.MIN = ZoneOffset(-18 * 3600) +ZoneOffset.UTC = ZoneOffset(0) + + +class ZoneRules(Stub): + def isFixedOffset(self): + return True + + def getOffset(self, instant): + return ZoneOffset.UTC + + def getDaylightSavings(self, instant): + return Duration.ZERO + + def isDaylightSavings(self, instant): + return False + + def isValidOffset(self, local_date_time, offset): + return True + + def nextTransition(self, instant): + return None + + def previousTransition(self, instant): + return None + + def getTransitions(self): + return [] + + def getTransitionRules(self): + return [] + + +# ============================================================ +# Instant +# ============================================================ + +class Instant(Temporal, Comparable): + EPOCH = None + + def __init__(self, epoch_second, nano=0): + super().__init__() + self._epoch_second = int(epoch_second) + self._nano = int(nano) + + @staticmethod + def now(): + import time as _t + t = _t.time() + sec = int(t) + nano = int((t - sec) * 1_000_000_000) + return Instant(sec, nano) + + @staticmethod + def ofEpochMilli(epoch_milli): + return Instant(epoch_milli // 1000, (epoch_milli % 1000) * 1_000_000) + + @staticmethod + def ofEpochSecond(epoch_second, nano_adjustment=0): + return Instant(epoch_second + nano_adjustment // 1_000_000_000, + nano_adjustment % 1_000_000_000) + + def getEpochSecond(self): + return self._epoch_second + + def getNano(self): + return self._nano + + def toEpochMilli(self): + return self._epoch_second * 1000 + self._nano // 1_000_000 + + def plus(self, seconds, nano=0): + return Instant(self._epoch_second + seconds, self._nano + nano) + + def minus(self, seconds, nano=0): + return self.plus(-seconds, -nano) + + def isBefore(self, other): + if isinstance(other, Instant): + return (self._epoch_second, self._nano) < (other._epoch_second, other._nano) + return False + + def isAfter(self, other): + if isinstance(other, Instant): + return (self._epoch_second, self._nano) > (other._epoch_second, other._nano) + return False + + def compareTo(self, other): + if isinstance(other, Instant): + diff = self._epoch_second - other._epoch_second + if diff != 0: + return -1 if diff < 0 else 1 + diff = self._nano - other._nano + return -1 if diff < 0 else (1 if diff > 0 else 0) + return -1 + + def equals(self, other): + if isinstance(other, Instant): + return self._epoch_second == other._epoch_second and self._nano == other._nano + return False + + def hashCode(self): + return hash((self._epoch_second, self._nano)) + + def toString(self): + return f"Instant({self._epoch_second},{self._nano})" + + +Instant.EPOCH = Instant(0, 0) + + +# ============================================================ +# Duration +# ============================================================ + +class Duration(Temporal, Comparable): + ZERO = None + + def __init__(self, seconds, nanos=0): + super().__init__() + self._seconds = int(seconds) + self._nanos = int(nanos) + + @staticmethod + def ofDays(days): + return Duration(days * 86400) + + @staticmethod + def ofHours(hours): + return Duration(hours * 3600) + + @staticmethod + def ofMinutes(minutes): + return Duration(minutes * 60) + + @staticmethod + def ofSeconds(seconds): + return Duration(seconds) + + @staticmethod + def ofMillis(millis): + return Duration(millis // 1000, (millis % 1000) * 1_000_000) + + @staticmethod + def ofNanos(nanos): + return Duration(nanos // 1_000_000_000, nanos % 1_000_000_000) + + @staticmethod + def between(start, end): + if isinstance(start, Instant) and isinstance(end, Instant): + return Duration(end.getEpochSecond() - start.getEpochSecond(), + end.getNano() - start.getNano()) + return Duration.ZERO + + def getSeconds(self): + return self._seconds + + def getNano(self): + return self._nanos + + def toDays(self): + return self._seconds // 86400 + + def toHours(self): + return self._seconds // 3600 + + def toMinutes(self): + return self._seconds // 60 + + def toSeconds(self): + return self._seconds + + def toMillis(self): + return self._seconds * 1000 + self._nanos // 1_000_000 + + def toNanos(self): + return self._seconds * 1_000_000_000 + self._nanos + + def plus(self, duration): + if isinstance(duration, Duration): + return Duration(self._seconds + duration._seconds, + self._nanos + duration._nanos) + return self + + def minus(self, duration): + if isinstance(duration, Duration): + return Duration(self._seconds - duration._seconds, + self._nanos - duration._nanos) + return self + + def isNegative(self): + return self._seconds < 0 or (self._seconds == 0 and self._nanos < 0) + + def isZero(self): + return self._seconds == 0 and self._nanos == 0 + + def compareTo(self, other): + if isinstance(other, Duration): + diff = self._seconds - other._seconds + if diff != 0: + return -1 if diff < 0 else 1 + diff = self._nanos - other._nanos + return -1 if diff < 0 else (1 if diff > 0 else 0) + return -1 + + def equals(self, other): + if isinstance(other, Duration): + return self._seconds == other._seconds and self._nanos == other._nanos + return False + + def toString(self): + return f"PT{self._seconds}S" + + def hashCode(self): + return hash((self._seconds, self._nanos)) + + +Duration.ZERO = Duration(0, 0) + + +# === 延迟初始化 ChronoUnit 常量(必须在 Duration 定义之后) === +ChronoUnit.NANOS = ChronoUnit("Nanos", Duration.ofNanos(1)) +ChronoUnit.MICROS = ChronoUnit("Micros", Duration.ofNanos(1000)) +ChronoUnit.MILLIS = ChronoUnit("Millis", Duration.ofNanos(1_000_000)) +ChronoUnit.SECONDS = ChronoUnit("Seconds", Duration.ofSeconds(1)) +ChronoUnit.MINUTES = ChronoUnit("Minutes", Duration.ofMinutes(1)) +ChronoUnit.HOURS = ChronoUnit("Hours", Duration.ofHours(1)) +ChronoUnit.HALF_DAYS = ChronoUnit("HalfDays", Duration.ofHours(12)) +ChronoUnit.DAYS = ChronoUnit("Days", Duration.ofDays(1)) +ChronoUnit.WEEKS = ChronoUnit("Weeks", Duration.ofDays(7)) +ChronoUnit.MONTHS = ChronoUnit("Months", Duration.ofDays(30), estimated=True) +ChronoUnit.YEARS = ChronoUnit("Years", Duration.ofDays(365), estimated=True) +ChronoUnit.DECADES = ChronoUnit("Decades", Duration.ofDays(365 * 10), estimated=True) +ChronoUnit.CENTURIES = ChronoUnit("Centuries", Duration.ofDays(365 * 100), estimated=True) +ChronoUnit.MILLENNIA = ChronoUnit("Millennia", Duration.ofDays(365 * 1000), estimated=True) +ChronoUnit.ERAS = ChronoUnit("Eras", Duration.ofDays(365 * 1_000_000_000), estimated=True) +ChronoUnit.FOREVER = ChronoUnit("Forever", Duration.ofSeconds(1_000_000_000 * 86400 * 365), estimated=True) + + +# ============================================================ +# Period +# ============================================================ + +class Period(Stub): + ZERO = None + + def __init__(self, years, months, days): + super().__init__() + self._years = years + self._months = months + self._days = days + + @staticmethod + def ofDays(days): + return Period(0, 0, days) + + @staticmethod + def ofWeeks(weeks): + return Period(0, 0, weeks * 7) + + @staticmethod + def ofMonths(months): + return Period(0, months, 0) + + @staticmethod + def ofYears(years): + return Period(years, 0, 0) + + @staticmethod + def between(start, end): + if isinstance(start, LocalDate) and isinstance(end, LocalDate): + return Period(end._year - start._year, + end._month - start._month, + end._day - start._day) + return Period.ZERO + + def getYears(self): + return self._years + + def getMonths(self): + return self._months + + def getDays(self): + return self._days + + def plus(self, period): + if isinstance(period, Period): + return Period(self._years + period._years, + self._months + period._months, + self._days + period._days) + return self + + def minus(self, period): + if isinstance(period, Period): + return Period(self._years - period._years, + self._months - period._months, + self._days - period._days) + return self + + def isZero(self): + return self._years == 0 and self._months == 0 and self._days == 0 + + def isNegative(self): + return self._years < 0 or self._months < 0 or self._days < 0 + + def equals(self, other): + if isinstance(other, Period): + return (self._years == other._years and + self._months == other._months and + self._days == other._days) + return False + + def toString(self): + return f"P{self._years}Y{self._months}M{self._days}D" + + def hashCode(self): + return hash((self._years, self._months, self._days)) + + +Period.ZERO = Period(0, 0, 0) + + +# ============================================================ +# LocalDate +# ============================================================ + +class LocalDate(Temporal, Comparable): + MIN = None + MAX = None + + def __init__(self, year, month, day): + super().__init__() + self._year = year + self._month = month + self._day = day + + @staticmethod + def now(zone=None): + today = _dt.date.today() + return LocalDate(today.year, today.month, today.day) + + @staticmethod + def of(year, month, dayOfMonth): + if isinstance(month, Month): + month = month.getValue() + return LocalDate(year, month, dayOfMonth) + + @staticmethod + def parse(text, formatter=None): + if formatter is None: + parts = text.split("-") + return LocalDate(int(parts[0]), int(parts[1]), int(parts[2])) + return formatter.parse(text, LocalDate) + + def getYear(self): + return self._year + + def getMonth(self): + return Month._from_value(self._month) + + def getMonthValue(self): + return self._month + + def getDayOfMonth(self): + return self._day + + def getDayOfWeek(self): + import time as _t + import datetime as _dt2 + d = _dt2.date(self._year, self._month, self._day) + return DayOfWeek._from_value(d.isoweekday()) + + def getDayOfYear(self): + d = _dt.date(self._year, self._month, self._day) + return d.timetuple().tm_yday + + def getEra(self): + return "CE" if self._year > 0 else "BCE" + + def isLeapYear(self): + return _dt.date(self._year, 3, 1) - _dt.date(self._year, 2, 28) == _dt.timedelta(days=1) + + def lengthOfMonth(self): + m = self._month + if m == 2: + return 29 if self.isLeapYear() else 28 + if m in (4, 6, 9, 11): + return 30 + return 31 + + def lengthOfYear(self): + return 366 if self.isLeapYear() else 365 + + def toEpochDay(self): + d = _dt.date(self._year, self._month, self._day) + return d.toordinal() + + def plusDays(self, days): + d = _dt.date(self._year, self._month, self._day) + _dt.timedelta(days=days) + return LocalDate(d.year, d.month, d.day) + + def plusWeeks(self, weeks): + return self.plusDays(weeks * 7) + + def plusMonths(self, months): + total = self._year * 12 + (self._month - 1) + months + y = total // 12 + m = (total % 12) + 1 + d = min(self._day, 29 if m == 2 and _dt.date(y, 3, 1) - _dt.date(y, 2, 28) == _dt.timedelta(days=1) else + 30 if m in (4, 6, 9, 11) else 31) + return LocalDate(y, m, d) + + def plusYears(self, years): + return self.plusMonths(years * 12) + + def minusDays(self, days): + return self.plusDays(-days) + + def minusWeeks(self, weeks): + return self.plusDays(-weeks * 7) + + def minusMonths(self, months): + return self.plusMonths(-months) + + def minusYears(self, years): + return self.plusYears(-years) + + def isBefore(self, other): + if isinstance(other, LocalDate): + return (self._year, self._month, self._day) < (other._year, other._month, other._day) + return False + + def isAfter(self, other): + if isinstance(other, LocalDate): + return (self._year, self._month, self._day) > (other._year, other._month, other._day) + return False + + def isEqual(self, other): + if isinstance(other, LocalDate): + return (self._year, self._month, self._day) == (other._year, other._month, other._day) + return False + + def withDayOfMonth(self, day): + return LocalDate(self._year, self._month, day) + + def withMonth(self, month): + if isinstance(month, Month): + month = month.getValue() + return LocalDate(self._year, month, self._day) + + def withYear(self, year): + return LocalDate(year, self._month, self._day) + + def atTime(self, hour, minute=0, second=0, nano=0): + return LocalDateTime(self, LocalTime(hour, minute, second, nano)) + + def atStartOfDay(self, zone=None): + return LocalDateTime(self, LocalTime.MIDNIGHT) + + def format(self, formatter): + return formatter.format(self) + + def compareTo(self, other): + if isinstance(other, LocalDate): + v = self._year - other._year + if v != 0: + return -1 if v < 0 else 1 + v = self._month - other._month + if v != 0: + return -1 if v < 0 else 1 + v = self._day - other._day + return -1 if v < 0 else (1 if v > 0 else 0) + return -1 + + def equals(self, other): + if isinstance(other, LocalDate): + return (self._year, self._month, self._day) == (other._year, other._month, other._day) + return False + + def toString(self): + return f"{self._year:04d}-{self._month:02d}-{self._day:02d}" + + def hashCode(self): + return hash((self._year, self._month, self._day)) + + +LocalDate.MIN = LocalDate(-999999999, 1, 1) +LocalDate.MAX = LocalDate(999999999, 12, 31) + + +# ============================================================ +# LocalTime +# ============================================================ + +class LocalTime(Temporal, Comparable): + MIDNIGHT = None + NOON = None + MIN = None + MAX = None + + def __init__(self, hour, minute=0, second=0, nano=0): + super().__init__() + self._hour = hour + self._minute = minute + self._second = second + self._nano = nano + + @staticmethod + def now(zone=None): + t = _dt.datetime.now().time() + return LocalTime(t.hour, t.minute, t.second, t.microsecond * 1000) + + @staticmethod + def of(hour, minute=0, second=0, nano=0): + return LocalTime(hour, minute, second, nano) + + @staticmethod + def parse(text, formatter=None): + parts = text.split(":") + h = int(parts[0]) + m = int(parts[1]) + s = int(parts[2]) if len(parts) > 2 else 0 + return LocalTime(h, m, s) + + def getHour(self): + return self._hour + + def getMinute(self): + return self._minute + + def getSecond(self): + return self._second + + def getNano(self): + return self._nano + + def toSecondOfDay(self): + return self._hour * 3600 + self._minute * 60 + self._second + + def toNanoOfDay(self): + return self.toSecondOfDay() * 1_000_000_000 + self._nano + + def plusHours(self, h): + total = self._hour + h + return LocalTime(total % 24, self._minute, self._second, self._nano) + + def plusMinutes(self, m): + total = self.toSecondOfDay() + m * 60 + total %= 86400 + h = total // 3600 + total %= 3600 + mi = total // 60 + s = total % 60 + return LocalTime(h, mi, s, self._nano) + + def plusSeconds(self, s): + total = self.toSecondOfDay() + s + total %= 86400 + h = total // 3600 + total %= 3600 + mi = total // 60 + sec = total % 60 + return LocalTime(h, mi, sec, self._nano) + + def plusNanos(self, n): + return LocalTime(self._hour, self._minute, self._second, self._nano + n) + + def minusHours(self, h): + return self.plusHours(-h) + + def minusMinutes(self, m): + return self.plusMinutes(-m) + + def minusSeconds(self, s): + return self.plusSeconds(-s) + + def withHour(self, h): + return LocalTime(h, self._minute, self._second, self._nano) + + def withMinute(self, m): + return LocalTime(self._hour, m, self._second, self._nano) + + def withSecond(self, s): + return LocalTime(self._hour, self._minute, s, self._nano) + + def withNano(self, n): + return LocalTime(self._hour, self._minute, self._second, n) + + def atDate(self, date): + return LocalDateTime(date, self) + + def isBefore(self, other): + if isinstance(other, LocalTime): + return self.toNanoOfDay() < other.toNanoOfDay() + return False + + def isAfter(self, other): + if isinstance(other, LocalTime): + return self.toNanoOfDay() > other.toNanoOfDay() + return False + + def format(self, formatter): + return formatter.format(self) + + def compareTo(self, other): + if isinstance(other, LocalTime): + diff = self.toNanoOfDay() - other.toNanoOfDay() + return -1 if diff < 0 else (1 if diff > 0 else 0) + return -1 + + def equals(self, other): + if isinstance(other, LocalTime): + return (self._hour, self._minute, self._second, self._nano) == \ + (other._hour, other._minute, other._second, other._nano) + return False + + def toString(self): + return f"{self._hour:02d}:{self._minute:02d}:{self._second:02d}" + + def hashCode(self): + return hash((self._hour, self._minute, self._second, self._nano)) + + +LocalTime.MIDNIGHT = LocalTime(0, 0, 0, 0) +LocalTime.NOON = LocalTime(12, 0, 0, 0) +LocalTime.MIN = LocalTime(0, 0, 0, 0) +LocalTime.MAX = LocalTime(23, 59, 59, 999999999) + + +# ============================================================ +# LocalDateTime +# ============================================================ + +class LocalDateTime(Temporal, Comparable): + MIN = None + MAX = None + + def __init__(self, date, time): + super().__init__() + self._date = date + self._time = time + + @staticmethod + def now(zone=None): + now = _dt.datetime.now() + date = LocalDate(now.year, now.month, now.day) + time = LocalTime(now.hour, now.minute, now.second, now.microsecond * 1000) + return LocalDateTime(date, time) + + @staticmethod + def of(date_or_year, month=None, dayOfMonth=None, hour=None, minute=0, second=0, nano=0): + if isinstance(date_or_year, LocalDate): + return LocalDateTime(date_or_year, LocalTime(month or 0, dayOfMonth or 0, hour or 0)) + return LocalDateTime(LocalDate(date_or_year, month, dayOfMonth), + LocalTime(hour, minute, second, nano)) + + @staticmethod + def parse(text, formatter=None): + parts = text.split("T") + date = LocalDate.parse(parts[0]) + time = LocalTime.parse(parts[1]) if len(parts) > 1 else LocalTime.MIDNIGHT + return LocalDateTime(date, time) + + def toLocalDate(self): + return self._date + + def toLocalTime(self): + return self._time + + def getYear(self): + return self._date.getYear() + + def getMonth(self): + return self._date.getMonth() + + def getMonthValue(self): + return self._date.getMonthValue() + + def getDayOfMonth(self): + return self._date.getDayOfMonth() + + def getDayOfWeek(self): + return self._date.getDayOfWeek() + + def getDayOfYear(self): + return self._date.getDayOfYear() + + def getHour(self): + return self._time.getHour() + + def getMinute(self): + return self._time.getMinute() + + def getSecond(self): + return self._time.getSecond() + + def getNano(self): + return self._time.getNano() + + def plusDays(self, d): + return LocalDateTime(self._date.plusDays(d), self._time) + + def plusHours(self, h): + return LocalDateTime(self._date, self._time.plusHours(h)) + + def plusMinutes(self, m): + return LocalDateTime(self._date, self._time.plusMinutes(m)) + + def plusSeconds(self, s): + return LocalDateTime(self._date, self._time.plusSeconds(s)) + + def minusDays(self, d): + return self.plusDays(-d) + + def minusHours(self, h): + return self.plusHours(-h) + + def minusMinutes(self, m): + return self.plusMinutes(-m) + + def minusSeconds(self, s): + return self.plusSeconds(-s) + + def withYear(self, y): + return LocalDateTime(self._date.withYear(y), self._time) + + def withMonth(self, m): + return LocalDateTime(self._date.withMonth(m), self._time) + + def withDayOfMonth(self, d): + return LocalDateTime(self._date.withDayOfMonth(d), self._time) + + def atZone(self, zone): + return ZonedDateTime(self, zone) + + def isBefore(self, other): + if isinstance(other, LocalDateTime): + if not self._date.isEqual(other._date): + return self._date.isBefore(other._date) + return self._time.isBefore(other._time) + return False + + def isAfter(self, other): + if isinstance(other, LocalDateTime): + if not self._date.isEqual(other._date): + return self._date.isAfter(other._date) + return self._time.isAfter(other._time) + return False + + def format(self, formatter): + return formatter.format(self) + + def compareTo(self, other): + if isinstance(other, LocalDateTime): + c = self._date.compareTo(other._date) + return c if c != 0 else self._time.compareTo(other._time) + return -1 + + def equals(self, other): + if isinstance(other, LocalDateTime): + return self._date.equals(other._date) and self._time.equals(other._time) + return False + + def toString(self): + return f"{self._date}T{self._time}" + + def hashCode(self): + return hash((self._date, self._time)) + + +LocalDateTime.MIN = LocalDateTime(LocalDate.MIN, LocalTime.MIN) +LocalDateTime.MAX = LocalDateTime(LocalDate.MAX, LocalTime.MAX) + + +# ============================================================ +# ZonedDateTime +# ============================================================ + +class ZonedDateTime(Temporal, Comparable): + def __init__(self, date_time, zone): + super().__init__() + self._date_time = date_time + self._zone = zone + + @staticmethod + def now(zone=None): + ldt = LocalDateTime.now() + z = zone or ZoneId.systemDefault() + return ZonedDateTime(ldt, z) + + @staticmethod + def of(date, time=None, zone=None, offset=None): + if isinstance(date, LocalDateTime): + return ZonedDateTime(date, time or ZoneId.systemDefault()) + return ZonedDateTime(LocalDateTime.of(date, time), zone) + + @staticmethod + def parse(text, formatter=None): + return ZonedDateTime(LocalDateTime.now(), ZoneId.systemDefault()) + + def getZone(self): + return self._zone + + def getOffset(self): + return ZoneOffset.UTC + + def withZoneSameInstant(self, zone): + return ZonedDateTime(self._date_time, zone) + + def withZoneSameLocal(self, zone): + return ZonedDateTime(self._date_time, zone) + + def toLocalDate(self): + return self._date_time.toLocalDate() + + def toLocalTime(self): + return self._date_time.toLocalTime() + + def toLocalDateTime(self): + return self._date_time + + def toInstant(self): + return Instant.now() + + def plusDays(self, d): + return ZonedDateTime(self._date_time.plusDays(d), self._zone) + + def plusHours(self, h): + return ZonedDateTime(self._date_time.plusHours(h), self._zone) + + def minusDays(self, d): + return self.plusDays(-d) + + def minusHours(self, h): + return self.plusHours(-h) + + def format(self, formatter): + return formatter.format(self) + + def compareTo(self, other): + if isinstance(other, ZonedDateTime): + return self._date_time.compareTo(other._date_time) + return -1 + + def equals(self, other): + if isinstance(other, ZonedDateTime): + return self._date_time.equals(other._date_time) and str(self._zone) == str(other._zone) + return False + + def toString(self): + return f"{self._date_time}[{self._zone}]" + + def hashCode(self): + return hash((self._date_time, str(self._zone))) + + +# ============================================================ +# OffsetDateTime / OffsetTime (stubs) +# ============================================================ + +class OffsetDateTime(Temporal): + def __init__(self, date_time, offset): + super().__init__() + self._date_time = date_time + self._offset = offset + + @staticmethod + def now(): + return OffsetDateTime(LocalDateTime.now(), ZoneOffset.UTC) + + def toLocalDate(self): + return self._date_time.toLocalDate() + + def toLocalTime(self): + return self._date_time.toLocalTime() + + def toString(self): + return f"{self._date_time}{self._offset}" + + +class OffsetTime(Stub): + def __init__(self, time, offset): + super().__init__() + self._time = time + self._offset = offset + + @staticmethod + def now(): + return OffsetTime(LocalTime.now(), ZoneOffset.UTC) + + def toString(self): + return f"{self._time}{self._offset}" + + +# ============================================================ +# Year / YearMonth / MonthDay +# ============================================================ + +class Year(Temporal, Comparable): + def __init__(self, year): + super().__init__() + self._year = year + + @staticmethod + def now(): + return Year(_dt.date.today().year) + + @staticmethod + def of(year): + return Year(year) + + def getValue(self): + return self._year + + def isLeap(self): + return _dt.date(self._year, 3, 1) - _dt.date(self._year, 2, 28) == _dt.timedelta(days=1) + + def length(self): + return 366 if self.isLeap() else 365 + + def atMonth(self, month): + if isinstance(month, Month): + month = month.getValue() + return YearMonth(self._year, month) + + def compareTo(self, other): + if isinstance(other, Year): + return -1 if self._year < other._year else (1 if self._year > other._year else 0) + return -1 + + def toString(self): + return str(self._year) + + +class YearMonth(Temporal, Comparable): + def __init__(self, year, month): + super().__init__() + self._year = year + self._month = month + + @staticmethod + def now(): + d = _dt.date.today() + return YearMonth(d.year, d.month) + + @staticmethod + def of(year, month): + return YearMonth(year, month) + + def getYear(self): + return self._year + + def getMonth(self): + return self._month + + def getMonthValue(self): + return self._month if isinstance(self._month, int) else self._month.getValue() + + def lengthOfMonth(self): + m = self.getMonthValue() + if m == 2: + return 29 if _dt.date(self._year, 3, 1) - _dt.date(self._year, 2, 28) == _dt.timedelta(days=1) else 28 + return 30 if m in (4, 6, 9, 11) else 31 + + def lengthOfYear(self): + return 366 if _dt.date(self._year, 3, 1) - _dt.date(self._year, 2, 28) == _dt.timedelta(days=1) else 365 + + def atDay(self, day): + return LocalDate(self._year, self.getMonthValue(), day) + + def compareTo(self, other): + if isinstance(other, YearMonth): + diff = self._year - other._year + if diff != 0: + return -1 if diff < 0 else 1 + diff = self.getMonthValue() - other.getMonthValue() + return -1 if diff < 0 else (1 if diff > 0 else 0) + return -1 + + def toString(self): + return f"{self._year}-{self.getMonthValue():02d}" + + +class MonthDay(Stub): + def __init__(self, month, day): + super().__init__() + self._month = month + self._day = day + + @staticmethod + def now(): + d = _dt.date.today() + return MonthDay(d.month, d.day) + + @staticmethod + def of(month, day): + return MonthDay(month, day) + + def getMonth(self): + return self._month + + def getMonthValue(self): + return self._month if isinstance(self._month, int) else self._month.getValue() + + def getDayOfMonth(self): + return self._day + + def atYear(self, year): + return LocalDate.of(year, self.getMonthValue(), self._day) + + def toString(self): + return f"--{self.getMonthValue():02d}-{self._day:02d}" + + +# ============================================================ +# Clock +# ============================================================ + +class Clock(Stub): + @staticmethod + def systemUTC(): + return Clock() + + @staticmethod + def systemDefaultZone(): + return Clock() + + @staticmethod + def system(zone): + return Clock() + + def millis(self): + import time as _t + return int(_t.time() * 1000) + + def instant(self): + return Instant.now() + + def getZone(self): + return ZoneId.systemDefault() + + +# ============================================================ +# DateTimeFormatter +# ============================================================ + +class DateTimeFormatter(Stub): + ISO_LOCAL_DATE = None + ISO_LOCAL_TIME = None + ISO_LOCAL_DATE_TIME = None + ISO_INSTANT = None + ISO_ZONED_DATE_TIME = None + ISO_OFFSET_DATE_TIME = None + + def __init__(self, pattern): + super().__init__() + self._pattern = pattern + + @staticmethod + def ofPattern(pattern): + return DateTimeFormatter(pattern) + + def format(self, temporal): + if isinstance(temporal, LocalDate): + return temporal.toString() + if isinstance(temporal, LocalTime): + return temporal.toString() + if isinstance(temporal, LocalDateTime): + return temporal.toString() + if isinstance(temporal, ZonedDateTime): + return temporal.toString() + if isinstance(temporal, Instant): + return temporal.toString() + return str(temporal) + + def parse(self, text, query=None): + if query is LocalDate or query is None: + return LocalDate.parse(text) + if query is LocalTime: + return LocalTime.parse(text) + if query is LocalDateTime: + return LocalDateTime.parse(text) + return text + + +DateTimeFormatter.ISO_LOCAL_DATE = DateTimeFormatter("yyyy-MM-dd") +DateTimeFormatter.ISO_LOCAL_TIME = DateTimeFormatter("HH:mm:ss") +DateTimeFormatter.ISO_LOCAL_DATE_TIME = DateTimeFormatter("yyyy-MM-ddTHH:mm:ss") +DateTimeFormatter.ISO_INSTANT = DateTimeFormatter("yyyy-MM-ddTHH:mm:ssZ") +DateTimeFormatter.ISO_ZONED_DATE_TIME = DateTimeFormatter("yyyy-MM-ddTHH:mm:ssZ") +DateTimeFormatter.ISO_OFFSET_DATE_TIME = DateTimeFormatter("yyyy-MM-ddTHH:mm:ssZ") + + +# ============================================================ +# DateTimeFormatterBuilder (stub) +# ============================================================ + +class DateTimeFormatterBuilder(Stub): + def __init__(self): + super().__init__() + self._pattern_parts = [] + + def appendValue(self, field, width=1): + return self + + def appendLiteral(self, literal): + return self + + def appendPattern(self, pattern): + self._pattern_parts.append(pattern) + return self + + def toFormatter(self): + return DateTimeFormatter("".join(self._pattern_parts)) + + +# ============================================================ +# TemporalAdjusters (stub) +# ============================================================ + +class TemporalAdjusters(Stub): + @staticmethod + def firstDayOfMonth(): + return lambda d: d.withDayOfMonth(1) + + @staticmethod + def lastDayOfMonth(): + return lambda d: d.withDayOfMonth(d.lengthOfMonth()) + + @staticmethod + def firstDayOfYear(): + return lambda d: d.withDayOfMonth(1).withMonth(1) + + @staticmethod + def lastDayOfYear(): + return lambda d: d.withDayOfMonth(31).withMonth(12) + + @staticmethod + def firstInMonth(day_of_week): + return lambda d: d + + @staticmethod + def next(day_of_week): + return lambda d: d.plusDays(1) \ No newline at end of file diff --git a/android_pyapi/java/util.py b/android_pyapi/java/util.py new file mode 100644 index 0000000..5273186 --- /dev/null +++ b/android_pyapi/java/util.py @@ -0,0 +1,3693 @@ +""" +Java java.util standard library APIs implemented in Python. +Android-pyApi: desktop Python translation of Android Java APIs. +""" + +from .._core import Stub +from .lang import RuntimeException +import heapq as _heapq +import bisect as _bisect +import datetime as _datetime +import uuid as _uuid +import random as _py_random +import math as _py_math +import copy as _copy +import re as _re +import io as _io +import threading as _threading +import time as _time +from typing import Any, Dict, List, Callable, Tuple, Iterable as _IterableType +import typing as _typing +from collections.abc import Iterable as _IterableABC + +__all__ = [ + "ArrayList", "LinkedList", "HashMap", "LinkedHashMap", "TreeMap", + "HashSet", "TreeSet", "LinkedHashSet", + "Collections", "Arrays", "Date", "Calendar", "GregorianCalendar", + "UUID", "StringTokenizer", "Optional", "Random", + "Properties", "EnumSet", "EnumMap", + "Iterator", "ListIterator", "Enumeration", + "Objects", "Locale", "BitSet", "PriorityQueue", + "Vector", "Stack", + "ConcurrentModificationException", "NoSuchElementException", + "EmptyStackException", "TooManyListenersException", + "MissingResourceException", "IllegalFormatException", + "DuplicateFormatFlagsException", "FormatFlagsConversionMismatchException", + "FormatterClosedException", +] + +# ============================================================ +# Exceptions +# ============================================================ + +# Define exception classes in correct dependency order. +# IllegalArgumentException comes before IllegalFormatException. +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) + +ConcurrentModificationException = type( + "ConcurrentModificationException", (RuntimeException,), {} +) +NoSuchElementException = type("NoSuchElementException", (RuntimeException,), {}) +EmptyStackException = type("EmptyStackException", (RuntimeException,), {}) +TooManyListenersException = type("TooManyListenersException", (Exception,), {}) +MissingResourceException = type("MissingResourceException", (RuntimeException,), {}) +IllegalFormatException = type("IllegalFormatException", (IllegalArgumentException,), {}) +DuplicateFormatFlagsException = type( + "DuplicateFormatFlagsException", (IllegalFormatException,), {} +) +FormatFlagsConversionMismatchException = type( + "FormatFlagsConversionMismatchException", (IllegalFormatException,), {} +) + +# We need IllegalStateException for FormatterClosedException +from .lang import RuntimeException as _RuntimeException +IllegalStateException = type("IllegalStateException", (_RuntimeException,), {}) + +FormatterClosedException = type("FormatterClosedException", (IllegalStateException,), {}) +UnsupportedOperationException = type("UnsupportedOperationException", (RuntimeException,), {}) + +# ============================================================ +# Iterator / ListIterator / Enumeration +# ============================================================ + +class Iterator(Stub): + """java.util.Iterator""" + def __init__(self, data): + super().__init__() + self._data = list(data) + self._index = 0 + self._mod_count = 0 + + def hasNext(self) -> bool: + return self._index < len(self._data) + + def next(self): + if not self.hasNext(): + raise NoSuchElementException() + val = self._data[self._index] + self._index += 1 + return val + + def remove(self): + raise UnsupportedOperationException("remove") + + def forEachRemaining(self, action): + while self.hasNext(): + action(self.next()) + + +class ListIterator(Iterator): + """java.util.ListIterator""" + def __init__(self, data): + super().__init__(data) + + def hasPrevious(self) -> bool: + return self._index > 0 + + def previous(self): + if not self.hasPrevious(): + raise NoSuchElementException() + self._index -= 1 + return self._data[self._index] + + def nextIndex(self) -> int: + return self._index + + def previousIndex(self) -> int: + return self._index - 1 + + def set(self, obj): + self._data[self._index - 1] = obj + + def add(self, obj): + self._data.insert(self._index, obj) + self._index += 1 + + +class Enumeration(Stub): + """java.util.Enumeration""" + def __init__(self, data): + super().__init__() + self._it = iter(data) + + def hasMoreElements(self) -> bool: + try: + self._next = next(self._it) + return True + except StopIteration: + return False + + def nextElement(self): + if hasattr(self, '_next'): + val = self._next + del self._next + return val + return next(self._it) + + def asIterator(self) -> Iterator: + return Iterator(self._it) + + +# ============================================================ +# ArrayList +# ============================================================ + +class ArrayList(Stub): + """java.util.ArrayList — backed by Python list""" + def __init__(self, initial_capacity: Any = None): + super().__init__() + if initial_capacity is None: + self._data = [] + elif isinstance(initial_capacity, int): + self._data = [] + elif isinstance(initial_capacity, _IterableABC): + self._data = list(initial_capacity) + else: + self._data = [] + + def add(self, obj) -> bool: + self._data.append(obj) + return True + + def addAt(self, index: int, obj) -> None: + self._data.insert(index, obj) + + def addAll(self, c) -> bool: + if isinstance(c, ArrayList): + self._data.extend(c._data) + else: + self._data.extend(c) + return True + + def addAllAt(self, index: int, c) -> bool: + if isinstance(c, ArrayList): + self._data[index:index] = c._data + else: + self._data[index:index] = list(c) + return True + + def clear(self) -> None: + self._data.clear() + + def clone(self) -> 'ArrayList': + return ArrayList(self._data) + + def contains(self, obj) -> bool: + return obj in self._data + + def containsAll(self, c) -> bool: + if isinstance(c, ArrayList): + return all(x in self._data for x in c._data) + return all(x in self._data for x in c) + + def ensureCapacity(self, min_capacity: int) -> None: + pass # Python list handles resizing automatically + + def get(self, index: int): + return self._data[index] + + def indexOf(self, obj) -> int: + try: + return self._data.index(obj) + except ValueError: + return -1 + + def isEmpty(self) -> bool: + return len(self._data) == 0 + + def iterator(self) -> Iterator: + return Iterator(self._data) + + def listIterator(self) -> ListIterator: + return ListIterator(self._data) + + def lastIndexOf(self, obj) -> int: + try: + return len(self._data) - 1 - self._data[::-1].index(obj) + except ValueError: + return -1 + + def remove(self, obj) -> bool: + if obj in self._data: + self._data.remove(obj) + return True + return False + + def removeAt(self, index: int): + return self._data.pop(index) + + def removeAll(self, c) -> bool: + if isinstance(c, ArrayList): + items = c._data + else: + items = list(c) + old_len = len(self._data) + self._data = [x for x in self._data if x not in items] + return len(self._data) != old_len + + def retainAll(self, c) -> bool: + if isinstance(c, ArrayList): + items = set(c._data) + else: + items = set(c) + old_len = len(self._data) + self._data = [x for x in self._data if x in items] + return len(self._data) != old_len + + def set(self, index: int, obj): + old = self._data[index] + self._data[index] = obj + return old + + def size(self) -> int: + return len(self._data) + + def sort(self, key=None, reverse: bool = False) -> None: + self._data.sort(key=key, reverse=reverse) + + def subList(self, from_index: int, to_index: int) -> 'ArrayList': + return ArrayList(self._data[from_index:to_index]) + + def toArray(self, a=None): + if a is None: + return list(self._data) + if len(a) < len(self._data): + return list(self._data) + for i, v in enumerate(self._data): + a[i] = v + if len(self._data) < len(a): + a[len(self._data)] = None + return a + + def trimToSize(self) -> None: + pass + + def forEach(self, action) -> None: + for item in self._data: + action(item) + + def removeIf(self, predicate) -> bool: + old_len = len(self._data) + self._data = [x for x in self._data if not predicate(x)] + return len(self._data) != old_len + + def replaceAll(self, operator) -> None: + self._data = [operator(x) for x in self._data] + + def equals(self, other) -> bool: + if isinstance(other, ArrayList): + return self._data == other._data + return False + + def hashCode(self) -> int: + h = 1 + for v in self._data: + h = (31 * h + hash(v)) & 0x7FFFFFFF + return h + + def toString(self) -> str: + items = ", ".join(str(x) for x in self._data) + return f"[{items}]" + + def __len__(self): + return len(self._data) + + def __getitem__(self, idx): + return self._data[idx] + + def __setitem__(self, idx, val): + self._data[idx] = val + + def __delitem__(self, idx): + del self._data[idx] + + def __iter__(self): + return iter(self._data) + + def __repr__(self): + return self.toString() + + def __contains__(self, obj): + return obj in self._data + + def __add__(self, other): + if isinstance(other, ArrayList): + result = ArrayList(self._data) + result._data.extend(other._data) + return result + return NotImplemented + + def __eq__(self, other): + return self.equals(other) + + +# ============================================================ +# LinkedList +# ============================================================ + +class LinkedList(Stub): + """java.util.LinkedList — backed by Python list""" + def __init__(self, c=None): + super().__init__() + if c is None: + self._data = [] + elif isinstance(c, _IterableABC): + self._data = list(c) + else: + self._data = [] + + def add(self, obj) -> bool: + self._data.append(obj) + return True + + def addFirst(self, obj) -> None: + self._data.insert(0, obj) + + def addLast(self, obj) -> None: + self._data.append(obj) + + def addAt(self, index: int, obj) -> None: + self._data.insert(index, obj) + + def addAll(self, c) -> bool: + if isinstance(c, LinkedList): + self._data.extend(c._data) + else: + self._data.extend(c) + return True + + def clear(self) -> None: + self._data.clear() + + def clone(self) -> 'LinkedList': + return LinkedList(self._data) + + def contains(self, obj) -> bool: + return obj in self._data + + def descendingIterator(self) -> Iterator: + return Iterator(reversed(self._data)) + + def element(self): + if not self._data: + raise NoSuchElementException() + return self._data[0] + + def get(self, index: int): + return self._data[index] + + def getFirst(self): + if not self._data: + raise NoSuchElementException() + return self._data[0] + + def getLast(self): + if not self._data: + raise NoSuchElementException() + return self._data[-1] + + def indexOf(self, obj) -> int: + try: + return self._data.index(obj) + except ValueError: + return -1 + + def isEmpty(self) -> bool: + return len(self._data) == 0 + + def iterator(self) -> Iterator: + return Iterator(self._data) + + def lastIndexOf(self, obj) -> int: + try: + return len(self._data) - 1 - self._data[::-1].index(obj) + except ValueError: + return -1 + + def listIterator(self, index: int = 0) -> ListIterator: + li = ListIterator(self._data) + li._index = index + return li + + def offer(self, obj) -> bool: + self._data.append(obj) + return True + + def offerFirst(self, obj) -> bool: + self._data.insert(0, obj) + return True + + def offerLast(self, obj) -> bool: + self._data.append(obj) + return True + + def peek(self): + return self._data[0] if self._data else None + + def peekFirst(self): + return self._data[0] if self._data else None + + def peekLast(self): + return self._data[-1] if self._data else None + + def poll(self): + return self._data.pop(0) if self._data else None + + def pollFirst(self): + return self._data.pop(0) if self._data else None + + def pollLast(self): + return self._data.pop() if self._data else None + + def pop(self): + if not self._data: + raise NoSuchElementException() + return self._data.pop() + + def push(self, obj) -> None: + self._data.insert(0, obj) + + def remove(self, obj) -> bool: + if obj in self._data: + self._data.remove(obj) + return True + return False + + def removeAt(self, index: int): + return self._data.pop(index) + + def removeFirst(self): + if not self._data: + raise NoSuchElementException() + return self._data.pop(0) + + def removeFirstOccurrence(self, obj) -> bool: + try: + idx = self._data.index(obj) + self._data.pop(idx) + return True + except ValueError: + return False + + def removeLast(self): + if not self._data: + raise NoSuchElementException() + return self._data.pop() + + def removeLastOccurrence(self, obj) -> bool: + try: + idx = len(self._data) - 1 - self._data[::-1].index(obj) + self._data.pop(idx) + return True + except ValueError: + return False + + def set(self, index: int, obj): + old = self._data[index] + self._data[index] = obj + return old + + def size(self) -> int: + return len(self._data) + + def toArray(self, a=None): + if a is None: + return list(self._data) + if len(a) < len(self._data): + return list(self._data) + for i, v in enumerate(self._data): + a[i] = v + if len(self._data) < len(a): + a[len(self._data)] = None + return a + + def forEach(self, action) -> None: + for item in self._data: + action(item) + + def equals(self, other) -> bool: + if isinstance(other, LinkedList): + return self._data == other._data + return False + + def hashCode(self) -> int: + h = 1 + for v in self._data: + h = (31 * h + hash(v)) & 0x7FFFFFFF + return h + + def toString(self) -> str: + items = ", ".join(str(x) for x in self._data) + return f"[{items}]" + + def __len__(self): + return len(self._data) + + def __getitem__(self, idx): + return self._data[idx] + + def __iter__(self): + return iter(self._data) + + def __repr__(self): + return self.toString() + + def __eq__(self, other): + return self.equals(other) + + +# ============================================================ +# HashMap +# ============================================================ + +class HashMap(Stub): + """java.util.HashMap — backed by Python dict""" + def __init__(self, initial_capacity: Any = None, load_factor: float = 0.75): + super().__init__() + self._data: Dict = {} + if initial_capacity is not None and not isinstance(initial_capacity, (int, float)): + if isinstance(initial_capacity, HashMap): + self._data.update(initial_capacity._data) + elif isinstance(initial_capacity, dict): + self._data.update(initial_capacity) + elif isinstance(initial_capacity, _IterableABC): + for item in initial_capacity: + if hasattr(item, 'getKey') and hasattr(item, 'getValue'): + k, v = item.getKey(), item.getValue() + elif isinstance(item, tuple) and len(item) >= 2: + k, v = item[0], item[1] + else: + continue + self._data[k] = v + + def clear(self) -> None: + self._data.clear() + + def clone(self) -> 'HashMap': + return HashMap(self._data) + + def compute(self, key, remapping_function): + old = self._data.get(key) + new_val = remapping_function(key, old) + if new_val is not None: + self._data[key] = new_val + return new_val + self._data.pop(key, None) + return None + + def computeIfAbsent(self, key, mapping_function): + if key not in self._data: + new_val = mapping_function(key) + if new_val is not None: + self._data[key] = new_val + return new_val + return self._data.get(key) + + def computeIfPresent(self, key, remapping_function): + if key in self._data: + new_val = remapping_function(key, self._data[key]) + if new_val is not None: + self._data[key] = new_val + return new_val + self._data.pop(key, None) + return None + + def containsKey(self, key) -> bool: + return key in self._data + + def containsValue(self, value) -> bool: + return value in self._data.values() + + def entrySet(self) -> set: + return self._data.items() + + def forEach(self, action) -> None: + for k, v in self._data.items(): + action(k, v) + + def get(self, key): + return self._data.get(key) + + def getOrDefault(self, key, default): + return self._data.get(key, default) + + def isEmpty(self) -> bool: + return len(self._data) == 0 + + def keySet(self) -> set: + return set(self._data.keys()) + + def merge(self, key, value, remapping_function): + old = self._data.get(key) + if old is None: + self._data[key] = value + return value + new_val = remapping_function(old, value) + if new_val is not None: + self._data[key] = new_val + return new_val + self._data.pop(key, None) + return None + + def put(self, key, value): + old = self._data.get(key) + self._data[key] = value + return old + + def putAll(self, m) -> None: + if isinstance(m, HashMap): + self._data.update(m._data) + else: + self._data.update(m) + + def putIfAbsent(self, key, value): + if key not in self._data: + self._data[key] = value + return None + return self._data[key] + + def remove(self, key, value=None) -> bool: + if value is None: + return self._data.pop(key, None) is not None or key in self._data + if key in self._data and self._data[key] == value: + del self._data[key] + return True + return False + + def replace(self, key, old_value=None, new_value=None): + if old_value is None and new_value is None: + return None + if new_value is None: + # replace(key, value) form + if key in self._data: + old = self._data[key] + self._data[key] = old_value # old_value is actually new value here + return old + return None + # replace(key, oldValue, newValue) form + if key in self._data and self._data[key] == old_value: + self._data[key] = new_value + return True + return False + + def replaceAll(self, function) -> None: + for k in list(self._data.keys()): + self._data[k] = function(k, self._data[k]) + + def size(self) -> int: + return len(self._data) + + def values(self): + return list(self._data.values()) + + def equals(self, other) -> bool: + if isinstance(other, HashMap): + return self._data == other._data + return False + + def hashCode(self) -> int: + h = 0 + for k, v in self._data.items(): + h = (h + (hash(k) ^ hash(v))) & 0x7FFFFFFF + return h + + def toString(self) -> str: + items = ", ".join(f"{k}={v}" for k, v in self._data.items()) + return f"{{{items}}}" + + def __len__(self): + return len(self._data) + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, value): + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def __iter__(self): + return iter(self._data) + + def __contains__(self, key): + return key in self._data + + def __repr__(self): + return self.toString() + + def __eq__(self, other): + return self.equals(other) + + +# ============================================================ +# LinkedHashMap +# ============================================================ + +class LinkedHashMap(HashMap): + """java.util.LinkedHashMap — backed by dict (preserves order in Python 3.7+)""" + def __init__(self, initial_capacity: Any = None, load_factor: float = 0.75, + access_order: bool = False): + super().__init__(initial_capacity, load_factor) + self._access_order = access_order + + def get(self, key): + val = self._data.get(key) + if val is not None and self._access_order: + # Move to end (most recently accessed) by delete/reinsert + del self._data[key] + self._data[key] = val + return val + + def getOrDefault(self, key, default): + val = self._data.get(key, default) + if key in self._data and self._access_order: + del self._data[key] + self._data[key] = val + return val + + def put(self, key, value): + old = self._data.get(key) + if key in self._data: + del self._data[key] + self._data[key] = value + return old + + def removeEldestEntry(self, eldest) -> bool: + return False + + def eldest(self): + if not self._data: + return None + key = next(iter(self._data)) + return (key, self._data[key]) + + +# ============================================================ +# TreeMap +# ============================================================ + +class TreeMap(Stub): + """java.util.TreeMap — sorted map using Python's sorted()""" + def __init__(self, comparator=None, m=None): + super().__init__() + self._data: Dict = {} + self._comparator = comparator + if m is not None: + if isinstance(m, TreeMap): + self._data.update(m._data) + elif isinstance(m, dict): + self._data.update(m) + elif isinstance(m, _IterableABC): + for item in m: + if isinstance(item, tuple) and len(item) >= 2: + self._data[item[0]] = item[1] + + def _sorted_keys(self): + key_fn = self._comparator if self._comparator else None + return sorted(self._data.keys(), key=key_fn) + + def _get_entry_by_key(self, key): + if key in self._data: + return (key, self._data[key]) + return None + + def clear(self) -> None: + self._data.clear() + + def clone(self) -> 'TreeMap': + return TreeMap(self._comparator, self._data) + + def comparator(self): + return self._comparator + + def containsKey(self, key) -> bool: + return key in self._data + + def containsValue(self, value) -> bool: + return value in self._data.values() + + def ceilingKey(self, key): + keys = self._sorted_keys() + for k in keys: + if (self._comparator(k, key) >= 0 if self._comparator else k >= key): + return k + return None + + def ceilingEntry(self, key): + k = self.ceilingKey(key) + return (k, self._data[k]) if k is not None else None + + def floorKey(self, key): + keys = self._sorted_keys() + for k in reversed(keys): + if (self._comparator(k, key) <= 0 if self._comparator else k <= key): + return k + return None + + def floorEntry(self, key): + k = self.floorKey(key) + return (k, self._data[k]) if k is not None else None + + def higherKey(self, key): + keys = self._sorted_keys() + for k in keys: + if (self._comparator(k, key) > 0 if self._comparator else k > key): + return k + return None + + def higherEntry(self, key): + k = self.higherKey(key) + return (k, self._data[k]) if k is not None else None + + def lowerKey(self, key): + keys = self._sorted_keys() + for k in reversed(keys): + if (self._comparator(k, key) < 0 if self._comparator else k < key): + return k + return None + + def lowerEntry(self, key): + k = self.lowerKey(key) + return (k, self._data[k]) if k is not None else None + + def firstKey(self): + if not self._data: + raise NoSuchElementException() + return self._sorted_keys()[0] + + def firstEntry(self): + k = self.firstKey() + return (k, self._data[k]) + + def lastKey(self): + if not self._data: + raise NoSuchElementException() + return self._sorted_keys()[-1] + + def lastEntry(self): + k = self.lastKey() + return (k, self._data[k]) + + def pollFirstEntry(self): + if not self._data: + return None + k = self.firstKey() + v = self._data.pop(k) + return (k, v) + + def pollLastEntry(self): + if not self._data: + return None + k = self.lastKey() + v = self._data.pop(k) + return (k, v) + + def entrySet(self): + return [(k, self._data[k]) for k in self._sorted_keys()] + + def forEach(self, action) -> None: + for k in self._sorted_keys(): + action(k, self._data[k]) + + def get(self, key): + return self._data.get(key) + + def getOrDefault(self, key, default): + return self._data.get(key, default) + + def headMap(self, to_key, inclusive: bool = False) -> 'TreeMap': + result = TreeMap(self._comparator) + for k in self._sorted_keys(): + cmp = self._comparator(k, to_key) if self._comparator else (k < to_key) + if cmp or (inclusive and (self._comparator(k, to_key) == 0 if self._comparator else k == to_key)): + result._data[k] = self._data[k] + elif not inclusive and (self._comparator(k, to_key) < 0 if self._comparator else k < to_key): + result._data[k] = self._data[k] + else: + break + return result + + def tailMap(self, from_key, inclusive: bool = True) -> 'TreeMap': + result = TreeMap(self._comparator) + started = False + for k in self._sorted_keys(): + cmp = self._comparator(k, from_key) if self._comparator else (k >= from_key) + if cmp or (inclusive and (self._comparator(k, from_key) == 0 if self._comparator else k == from_key)): + started = True + if started: + result._data[k] = self._data[k] + return result + + def subMap(self, from_key, to_key, from_inclusive: bool = True, to_inclusive: bool = False) -> 'TreeMap': + result = TreeMap(self._comparator) + for k in self._sorted_keys(): + cmp_from = self._comparator(k, from_key) if self._comparator else (k >= from_key if from_inclusive else k > from_key) + cmp_to = self._comparator(k, to_key) if self._comparator else (k <= to_key if to_inclusive else k < to_key) + if cmp_from and cmp_to: + result._data[k] = self._data[k] + return result + + def isEmpty(self) -> bool: + return len(self._data) == 0 + + def keySet(self): + return self._sorted_keys() + + def put(self, key, value): + old = self._data.get(key) + self._data[key] = value + return old + + def putAll(self, m) -> None: + if isinstance(m, TreeMap): + self._data.update(m._data) + else: + self._data.update(m) + + def putIfAbsent(self, key, value): + if key not in self._data: + self._data[key] = value + return None + return self._data[key] + + def remove(self, key): + return self._data.pop(key, None) + + def replace(self, key, old_value=None, new_value=None): + if new_value is None: + if key in self._data: + old = self._data[key] + self._data[key] = old_value + return old + return None + if key in self._data and self._data[key] == old_value: + self._data[key] = new_value + return True + return False + + def size(self) -> int: + return len(self._data) + + def values(self): + return [self._data[k] for k in self._sorted_keys()] + + def navigableKeySet(self): + return self._sorted_keys() + + def descendingKeySet(self): + return list(reversed(self._sorted_keys())) + + def descendingMap(self) -> 'TreeMap': + result = TreeMap(self._comparator) + for k in reversed(self._sorted_keys()): + result._data[k] = self._data[k] + return result + + def equals(self, other) -> bool: + if isinstance(other, TreeMap): + return self._data == other._data + return False + + def hashCode(self) -> int: + h = 0 + for k, v in self._data.items(): + h = (h + (hash(k) ^ hash(v))) & 0x7FFFFFFF + return h + + def toString(self) -> str: + items = ", ".join(f"{k}={v}" for k in self._data.items()) + return f"{{{items}}}" + + def __len__(self): + return len(self._data) + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, value): + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def __iter__(self): + return iter(self._sorted_keys()) + + def __repr__(self): + return self.toString() + + def __eq__(self, other): + return self.equals(other) + + +# ============================================================ +# HashSet +# ============================================================ + +class HashSet(Stub): + """java.util.HashSet — backed by Python set""" + def __init__(self, c=None): + super().__init__() + if c is None: + self._data: set = set() + elif isinstance(c, _IterableABC): + self._data = set(c) + else: + self._data = set() + + def add(self, obj) -> bool: + if obj in self._data: + return False + self._data.add(obj) + return True + + def addAll(self, c) -> bool: + old_size = len(self._data) + if isinstance(c, HashSet): + self._data.update(c._data) + elif isinstance(c, set): + self._data.update(c) + else: + self._data.update(c) + return len(self._data) != old_size + + def clear(self) -> None: + self._data.clear() + + def clone(self) -> 'HashSet': + return HashSet(self._data) + + def contains(self, obj) -> bool: + return obj in self._data + + def containsAll(self, c) -> bool: + if isinstance(c, HashSet): + return all(x in self._data for x in c._data) + return all(x in self._data for x in c) + + def isEmpty(self) -> bool: + return len(self._data) == 0 + + def iterator(self) -> Iterator: + return Iterator(list(self._data)) + + def remove(self, obj) -> bool: + if obj in self._data: + self._data.remove(obj) + return True + return False + + def removeAll(self, c) -> bool: + old_size = len(self._data) + if isinstance(c, HashSet): + self._data.difference_update(c._data) + else: + self._data.difference_update(c) + return len(self._data) != old_size + + def retainAll(self, c) -> bool: + old_size = len(self._data) + if isinstance(c, HashSet): + self._data.intersection_update(c._data) + else: + self._data.intersection_update(c) + return len(self._data) != old_size + + def size(self) -> int: + return len(self._data) + + def toArray(self, a=None): + items = list(self._data) + if a is None: + return items + if len(a) < len(items): + return items + for i, v in enumerate(items): + a[i] = v + if len(items) < len(a): + a[len(items)] = None + return a + + def forEach(self, action) -> None: + for item in list(self._data): + action(item) + + def equals(self, other) -> bool: + if isinstance(other, HashSet): + return self._data == other._data + return False + + def hashCode(self) -> int: + h = 0 + for v in self._data: + h = (h + hash(v)) & 0x7FFFFFFF + return h + + def toString(self) -> str: + items = ", ".join(str(x) for x in self._data) + return f"[{items}]" + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __contains__(self, obj): + return obj in self._data + + def __repr__(self): + return self.toString() + + def __eq__(self, other): + return self.equals(other) + + +# ============================================================ +# LinkedHashSet +# ============================================================ + +class LinkedHashSet(HashSet): + """java.util.LinkedHashSet — ordered set (preserves insertion order)""" + def __init__(self, c=None): + # dict keys preserve insertion order in Python 3.7+ + self._order: Dict[Any, None] = {} + if c is not None: + if isinstance(c, _IterableABC): + for item in c: + self._order[item] = None + self._data = set(self._order.keys()) + + def add(self, obj) -> bool: + if obj in self._order: + return False + self._order[obj] = None + self._data.add(obj) + return True + + def remove(self, obj) -> bool: + if obj in self._order: + del self._order[obj] + self._data.discard(obj) + return True + return False + + def clear(self) -> None: + self._order.clear() + self._data.clear() + + def iterator(self) -> Iterator: + return Iterator(list(self._order.keys())) + + def toArray(self, a=None): + items = list(self._order.keys()) + if a is None: + return items + if len(a) < len(items): + return items + for i, v in enumerate(items): + a[i] = v + if len(items) < len(a): + a[len(items)] = None + return a + + def forEach(self, action) -> None: + for item in list(self._order.keys()): + action(item) + + def __iter__(self): + return iter(self._order.keys()) + + +# ============================================================ +# TreeSet +# ============================================================ + +class TreeSet(Stub): + """java.util.TreeSet — sorted set""" + def __init__(self, comparator=None, c=None): + super().__init__() + self._data: set = set() + self._comparator = comparator + if c is not None: + if isinstance(c, _IterableABC): + self._data = set(c) + + def _sorted(self): + key_fn = self._comparator if self._comparator else None + return sorted(self._data, key=key_fn) + + def add(self, obj) -> bool: + if obj in self._data: + return False + self._data.add(obj) + return True + + def addAll(self, c) -> bool: + old_size = len(self._data) + if isinstance(c, TreeSet): + self._data.update(c._data) + else: + self._data.update(c) + return len(self._data) != old_size + + def ceiling(self, obj): + sorted_items = self._sorted() + for item in sorted_items: + if (self._comparator(item, obj) >= 0 if self._comparator else item >= obj): + return item + return None + + def floor(self, obj): + sorted_items = self._sorted() + for item in reversed(sorted_items): + if (self._comparator(item, obj) <= 0 if self._comparator else item <= obj): + return item + return None + + def higher(self, obj): + sorted_items = self._sorted() + for item in sorted_items: + if (self._comparator(item, obj) > 0 if self._comparator else item > obj): + return item + return None + + def lower(self, obj): + sorted_items = self._sorted() + for item in reversed(sorted_items): + if (self._comparator(item, obj) < 0 if self._comparator else item < obj): + return item + return None + + def clear(self) -> None: + self._data.clear() + + def clone(self) -> 'TreeSet': + return TreeSet(self._comparator, self._data) + + def comparator(self): + return self._comparator + + def contains(self, obj) -> bool: + return obj in self._data + + def containsAll(self, c) -> bool: + if isinstance(c, TreeSet): + return all(x in self._data for x in c._data) + return all(x in self._data for x in c) + + def first(self): + if not self._data: + raise NoSuchElementException() + return self._sorted()[0] + + def last(self): + if not self._data: + raise NoSuchElementException() + return self._sorted()[-1] + + def pollFirst(self): + if not self._data: + return None + val = self._sorted()[0] + self._data.discard(val) + return val + + def pollLast(self): + if not self._data: + return None + val = self._sorted()[-1] + self._data.discard(val) + return val + + def headSet(self, to_element, inclusive: bool = False) -> 'TreeSet': + result = TreeSet(self._comparator) + for item in self._data: + cmp = self._comparator(item, to_element) if self._comparator else (item < to_element) + if cmp or (inclusive and item == to_element): + result._data.add(item) + return result + + def tailSet(self, from_element, inclusive: bool = True) -> 'TreeSet': + result = TreeSet(self._comparator) + for item in self._data: + cmp = self._comparator(item, from_element) if self._comparator else (item > from_element) + if cmp or (inclusive and item == from_element): + result._data.add(item) + return result + + def subSet(self, from_element, to_element, + from_inclusive: bool = True, to_inclusive: bool = False) -> 'TreeSet': + result = TreeSet(self._comparator) + for item in self._data: + cmp_from = self._comparator(item, from_element) if self._comparator else ( + item >= from_element if from_inclusive else item > from_element) + cmp_to = self._comparator(item, to_element) if self._comparator else ( + item <= to_element if to_inclusive else item < to_element) + if cmp_from and cmp_to: + result._data.add(item) + return result + + def descendingIterator(self) -> Iterator: + return Iterator(reversed(self._sorted())) + + def descendingSet(self) -> 'TreeSet': + result = TreeSet(self._comparator) + result._data = set(self._data) + return result + + def isEmpty(self) -> bool: + return len(self._data) == 0 + + def iterator(self) -> Iterator: + return Iterator(self._sorted()) + + def remove(self, obj) -> bool: + if obj in self._data: + self._data.discard(obj) + return True + return False + + def removeAll(self, c) -> bool: + old_size = len(self._data) + if isinstance(c, TreeSet): + self._data.difference_update(c._data) + else: + self._data.difference_update(c) + return len(self._data) != old_size + + def retainAll(self, c) -> bool: + old_size = len(self._data) + if isinstance(c, TreeSet): + self._data.intersection_update(c._data) + else: + self._data.intersection_update(c) + return len(self._data) != old_size + + def size(self) -> int: + return len(self._data) + + def toArray(self, a=None): + items = self._sorted() + if a is None: + return items + if len(a) < len(items): + return items + for i, v in enumerate(items): + a[i] = v + if len(items) < len(a): + a[len(items)] = None + return a + + def forEach(self, action) -> None: + for item in self._sorted(): + action(item) + + def equals(self, other) -> bool: + if isinstance(other, TreeSet): + return self._data == other._data + return False + + def hashCode(self) -> int: + h = 0 + for v in self._data: + h = (h + hash(v)) & 0x7FFFFFFF + return h + + def toString(self) -> str: + items = ", ".join(str(x) for x in self._sorted()) + return f"[{items}]" + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._sorted()) + + def __contains__(self, obj): + return obj in self._data + + def __repr__(self): + return self.toString() + + def __eq__(self, other): + return self.equals(other) + + +# ============================================================ +# Collections +# ============================================================ + +class Collections: + """java.util.Collections — static utility methods""" + EMPTY_LIST = [] + EMPTY_SET = set() + EMPTY_MAP = {} + + @staticmethod + def sort(data, key=None, reverse: bool = False) -> None: + if key is not None: + if isinstance(data, ArrayList): + data._data.sort(key=key, reverse=reverse) + elif isinstance(data, Vector): + data._data.sort(key=key, reverse=reverse) + else: + data.sort(key=key, reverse=reverse) + else: + data.sort(reverse=reverse) + + @staticmethod + def reverse(data) -> None: + if isinstance(data, ArrayList): + data._data.reverse() + elif isinstance(data, Vector): + data._data.reverse() + else: + data.reverse() + + @staticmethod + def shuffle(data, rnd: _typing.Optional['Random'] = None) -> None: + if isinstance(data, ArrayList): + lst = data._data + elif isinstance(data, Vector): + lst = data._data + else: + lst = data + if rnd: + _py_random.shuffle(lst) + else: + _py_random.shuffle(lst) + + @staticmethod + def swap(data, i: int, j: int) -> None: + if isinstance(data, ArrayList): + data._data[i], data._data[j] = data._data[j], data._data[i] + elif isinstance(data, Vector): + data._data[i], data._data[j] = data._data[j], data._data[i] + else: + data[i], data[j] = data[j], data[i] + + @staticmethod + def fill(data, obj) -> None: + if isinstance(data, ArrayList): + for i in range(len(data._data)): + data._data[i] = obj + elif isinstance(data, Vector): + for i in range(len(data._data)): + data._data[i] = obj + else: + for i in range(len(data)): + data[i] = obj + + @staticmethod + def copy(dest, src) -> None: + if isinstance(dest, ArrayList) and isinstance(src, ArrayList): + dest._data[:len(src._data)] = src._data[:] + elif isinstance(dest, Vector) and isinstance(src, Vector): + dest._data[:len(src._data)] = src._data[:] + else: + dest[:len(src)] = src[:] + + @staticmethod + def min(coll, comp=None): + if comp: + return min(coll, key=comp) + return min(coll) + + @staticmethod + def max(coll, comp=None): + if comp: + return max(coll, key=comp) + return max(coll) + + @staticmethod + def rotate(data, distance: int) -> None: + if isinstance(data, ArrayList): + lst = data._data + elif isinstance(data, Vector): + lst = data._data + else: + lst = data + n = len(lst) + if n == 0: + return + d = distance % n + lst[:] = lst[-d:] + lst[:-d] + + @staticmethod + def indexOfSubList(source, target) -> int: + if isinstance(source, ArrayList): + src = source._data + else: + src = list(source) + if isinstance(target, ArrayList): + tgt = target._data + else: + tgt = list(target) + for i in range(len(src) - len(tgt) + 1): + if src[i:i + len(tgt)] == tgt: + return i + return -1 + + @staticmethod + def lastIndexOfSubList(source, target) -> int: + if isinstance(source, ArrayList): + src = source._data + else: + src = list(source) + if isinstance(target, ArrayList): + tgt = target._data + else: + tgt = list(target) + for i in range(len(src) - len(tgt), -1, -1): + if src[i:i + len(tgt)] == tgt: + return i + return -1 + + @staticmethod + def replaceAll(data, old_val, new_val) -> bool: + if isinstance(data, ArrayList): + lst = data._data + elif isinstance(data, Vector): + lst = data._data + else: + lst = data + found = False + for i, v in enumerate(lst): + if v == old_val: + lst[i] = new_val + found = True + return found + + @staticmethod + def binarySearch(data, key): + if isinstance(data, ArrayList): + lst = data._data + else: + lst = list(data) + idx = _bisect.bisect_left(lst, key) + if idx < len(lst) and lst[idx] == key: + return idx + return -1 + + @staticmethod + def disjoint(c1, c2) -> bool: + set1 = set(c1) + set2 = set(c2) + return set1.isdisjoint(set2) + + @staticmethod + def frequency(c, obj) -> int: + count = 0 + for item in c: + if item == obj: + count += 1 + return count + + @staticmethod + def nCopies(n: int, obj) -> list: + return [obj] * n + + @staticmethod + def singleton(obj) -> set: + return {obj} + + @staticmethod + def singletonList(obj) -> list: + return [obj] + + @staticmethod + def singletonMap(key, value) -> dict: + return {key: value} + + @staticmethod + def unmodifiableList(data) -> list: + if isinstance(data, ArrayList): + return tuple(data._data) + return tuple(data) + + @staticmethod + def unmodifiableSet(data) -> frozenset: + if isinstance(data, HashSet): + return frozenset(data._data) + return frozenset(data) + + @staticmethod + def unmodifiableMap(data) -> dict: + if isinstance(data, HashMap): + return dict(data._data) + return dict(data) + + @staticmethod + def synchronizedList(data): + lock = _threading.Lock() + + class _SyncList: + def __init__(self, lst): + self._lock = lock + self._lst = lst + + def __getattr__(self, name): + with self._lock: + return getattr(self._lst, name) + + def add(self, obj): + with self._lock: + return self._lst.add(obj) + + def get(self, idx): + with self._lock: + return self._lst[idx] + + def set(self, idx, val): + with self._lock: + old = self._lst[idx] + self._lst[idx] = val + return old + + def remove(self, obj): + with self._lock: + return self._lst.remove(obj) + + def size(self): + with self._lock: + return len(self._lst) + + def __len__(self): + with self._lock: + return len(self._lst) + + def __getitem__(self, idx): + with self._lock: + return self._lst[idx] + + def __setitem__(self, idx, val): + with self._lock: + self._lst[idx] = val + + def __iter__(self): + with self._lock: + return iter(list(self._lst)) + + return _SyncList(data) + + @staticmethod + def synchronizedSet(data): + lock = _threading.Lock() + + class _SyncSet: + def __init__(self, s): + self._lock = lock + self._s = s + + def add(self, obj): + with self._lock: + return self._s.add(obj) + + def remove(self, obj): + with self._lock: + return self._s.remove(obj) + + def __contains__(self, obj): + with self._lock: + return obj in self._s + + def __iter__(self): + with self._lock: + return iter(list(self._s)) + + def __len__(self): + with self._lock: + return len(self._s) + + return _SyncSet(data) + + @staticmethod + def synchronizedMap(data): + lock = _threading.Lock() + + class _SyncMap: + def __init__(self, m): + self._lock = lock + self._m = m + + def put(self, k, v): + with self._lock: + return self._m.put(k, v) + + def get(self, k): + with self._lock: + return self._m.get(k) + + def remove(self, k): + with self._lock: + return self._m.remove(k) + + def __getitem__(self, k): + with self._lock: + return self._m[k] + + def __setitem__(self, k, v): + with self._lock: + self._m[k] = v + + def __contains__(self, k): + with self._lock: + return k in self._m + + def __len__(self): + with self._lock: + return len(self._m) + + def keys(self): + with self._lock: + return list(self._m.keys()) + + return _SyncMap(data) + + @staticmethod + def enumaration(c) -> Enumeration: + return Enumeration(c) + + @staticmethod + def list(enum: Enumeration) -> ArrayList: + items = [] + while enum.hasMoreElements(): + items.append(enum.nextElement()) + return ArrayList(items) + + @staticmethod + def newSetFromMap(m) -> HashSet: + class _SetFromMap(HashSet): + def __init__(self, backing_map): + super().__init__() + self._backing = backing_map + + def add(self, obj): + return self._backing.put(obj, True) is None + + def iterator(self): + return Iterator(self._backing.keySet()) + + def size(self): + return self._backing.size() + + def remove(self, obj): + return self._backing.remove(obj) is not None + + def contains(self, obj): + return self._backing.containsKey(obj) + + def clear(self): + self._backing.clear() + + return _SetFromMap(m) + + @staticmethod + def asLifoQueue(deque) -> 'Collections._LifoQueue': + class _LifoQueue: + def __init__(self, dq): + self._dq = dq + + def add(self, e): + self._dq.addFirst(e) + return True + + def offer(self, e): + return self.add(e) + + def remove(self): + if len(self._dq) == 0: + raise NoSuchElementException() + return self._dq.removeFirst() + + def poll(self): + return self._dq.pollFirst() + + def element(self): + if len(self._dq) == 0: + raise NoSuchElementException() + return self._dq.getFirst() + + def peek(self): + return self._dq.peekFirst() + + def size(self): + return self._dq.size() + + def isEmpty(self): + return self._dq.isEmpty() + + return _LifoQueue(deque) + + +# ============================================================ +# Arrays +# ============================================================ + +class Arrays: + """java.util.Arrays — static utility methods""" + @staticmethod + def asList(*args) -> ArrayList: + if len(args) == 1 and isinstance(args[0], (list, tuple)): + return ArrayList(args[0]) + return ArrayList(list(args)) + + @staticmethod + def sort(a, from_index: int = 0, to_index: int = None) -> None: + if to_index is None: + a.sort() + else: + a[from_index:to_index] = sorted(a[from_index:to_index]) + + @staticmethod + def parallelSort(a, from_index: int = 0, to_index: int = None) -> None: + Arrays.sort(a, from_index, to_index) + + @staticmethod + def binarySearch(a, key, from_index: int = 0, to_index: int = None) -> int: + if to_index is None: + lst = a + else: + lst = a[from_index:to_index] + idx = _bisect.bisect_left(lst, key) + if idx < len(lst) and lst[idx] == key: + return idx + from_index + return -(idx + from_index + 1) + + @staticmethod + def fill(a, val, from_index: int = 0, to_index: int = None) -> None: + if to_index is None: + for i in range(len(a)): + a[i] = val + else: + for i in range(from_index, to_index): + a[i] = val + + @staticmethod + def copyOf(original, new_length: int) -> list: + return list(original[:new_length]) + [None] * max(0, new_length - len(original)) + + @staticmethod + def copyOfRange(original, from_index: int, to_index: int) -> list: + return list(original[from_index:to_index]) + + @staticmethod + def equals(a, b) -> bool: + if len(a) != len(b): + return False + return all(x == y for x, y in zip(a, b)) + + @staticmethod + def deepEquals(a, b) -> bool: + if len(a) != len(b): + return False + return all(_deep_eq(x, y) for x, y in zip(a, b)) + + @staticmethod + def hashCode(a) -> int: + if a is None: + return 0 + h = 1 + for v in a: + h = (31 * h + hash(v)) & 0x7FFFFFFF + return h + + @staticmethod + def deepHashCode(a) -> int: + if a is None: + return 0 + h = 1 + for v in a: + if isinstance(v, (list, tuple)): + h = (31 * h + Arrays.deepHashCode(v)) & 0x7FFFFFFF + else: + h = (31 * h + hash(v)) & 0x7FFFFFFF + return h + + @staticmethod + def toString(a) -> str: + if a is None: + return "null" + return "[" + ", ".join(str(x) for x in a) + "]" + + @staticmethod + def deepToString(a) -> str: + if a is None: + return "null" + return "[" + ", ".join(_deep_str(x) for x in a) + "]" + + @staticmethod + def stream(a) -> 'Arrays._ArrayStream': + class _ArrayStream: + def __init__(self, arr): + self._arr = arr + self._idx = 0 + + def filter(self, pred): + return _ArrayStream([x for x in self._arr if pred(x)]) + + def map(self, func): + return _ArrayStream([func(x) for x in self._arr]) + + def forEach(self, action): + for x in self._arr: + action(x) + + def toArray(self): + return list(self._arr) + + def count(self): + return len(self._arr) + + return _ArrayStream(a) + + @staticmethod + def setAll(a, generator) -> None: + for i in range(len(a)): + a[i] = generator(i) + + @staticmethod + def parallelSetAll(a, generator) -> None: + Arrays.setAll(a, generator) + + @staticmethod + def spliterator(a) -> Iterator: + return Iterator(a) + + +def _deep_eq(x, y) -> bool: + if isinstance(x, (list, tuple)) and isinstance(y, (list, tuple)): + return Arrays.deepEquals(x, y) + return x == y + + +def _deep_str(x) -> str: + if isinstance(x, (list, tuple)): + return Arrays.deepToString(x) + return str(x) + + +# ============================================================ +# Date +# ============================================================ + +class Date(Stub): + """java.util.Date — wraps Python datetime""" + def __init__(self, *args): + super().__init__() + if len(args) == 0: + self._dt = _datetime.datetime.now() + elif len(args) == 1: + if isinstance(args[0], (int, float)): + self._dt = _datetime.datetime.fromtimestamp(args[0] / 1000.0) + elif isinstance(args[0], str): + self._dt = _datetime.datetime.fromisoformat(args[0]) + elif isinstance(args[0], _datetime.datetime): + self._dt = args[0] + elif isinstance(args[0], Date): + self._dt = args[0]._dt + else: + self._dt = _datetime.datetime.now() + elif len(args) >= 6: + year = args[0] - 1900 + month = args[1] + day = args[2] + hour = args[3] if len(args) > 3 else 0 + minute = args[4] if len(args) > 4 else 0 + second = args[5] if len(args) > 5 else 0 + self._dt = _datetime.datetime(year + 1900, month + 1, day, hour, minute, second) + else: + self._dt = _datetime.datetime.now() + + def getTime(self) -> int: + return int(self._dt.timestamp() * 1000) + + def setTime(self, time_millis: int) -> None: + self._dt = _datetime.datetime.fromtimestamp(time_millis / 1000.0) + + def before(self, when: 'Date') -> bool: + return self._dt < when._dt + + def after(self, when: 'Date') -> bool: + return self._dt > when._dt + + def compareTo(self, another: 'Date') -> int: + return (self._dt > another._dt) - (self._dt < another._dt) + + def equals(self, other) -> bool: + if isinstance(other, Date): + return self._dt == other._dt + return False + + def hashCode(self) -> int: + return hash(self._dt) & 0x7FFFFFFF + + def toString(self) -> str: + return self._dt.strftime("%a %b %d %H:%M:%S %z %Y") + + def clone(self) -> 'Date': + return Date(self._dt) + + def getYear(self) -> int: + return self._dt.year - 1900 + + def getMonth(self) -> int: + return self._dt.month - 1 + + def getDay(self) -> int: + return self._dt.weekday() + + def getHours(self) -> int: + return self._dt.hour + + def getMinutes(self) -> int: + return self._dt.minute + + def getSeconds(self) -> int: + return self._dt.second + + def getDate(self) -> int: + return self._dt.day + + def setYear(self, year: int) -> None: + self._dt = self._dt.replace(year=year + 1900) + + def setMonth(self, month: int) -> None: + self._dt = self._dt.replace(month=month + 1) + + def setDate(self, date: int) -> None: + self._dt = self._dt.replace(day=date) + + def setHours(self, hours: int) -> None: + self._dt = self._dt.replace(hour=hours) + + def setMinutes(self, minutes: int) -> None: + self._dt = self._dt.replace(minute=minutes) + + def setSeconds(self, seconds: int) -> None: + self._dt = self._dt.replace(second=seconds) + + def toGMTString(self) -> str: + return self._dt.strftime("%d %b %Y %H:%M:%S GMT") + + def toLocaleString(self) -> str: + return self._dt.strftime("%Y-%m-%d %H:%M:%S") + + @staticmethod + def parse(s: str) -> 'Date': + try: + return Date(_datetime.datetime.fromisoformat(s)) + except ValueError: + return Date() + + @staticmethod + def fromInstant(instant) -> 'Date': + return Date(instant.toEpochMilli()) + + def toInstant(self): + return self._dt + + def __str__(self): + return self.toString() + + def __repr__(self): + return f"Date({self.getTime()})" + + +# ============================================================ +# Calendar / GregorianCalendar +# ============================================================ + +class Calendar(Stub): + """java.util.Calendar — wraps Python datetime with Java field system""" + # Field constants + YEAR = 1 + MONTH = 2 + DAY_OF_MONTH = 5 + DAY_OF_YEAR = 6 + DAY_OF_WEEK = 7 + DAY_OF_WEEK_IN_MONTH = 8 + AM_PM = 9 + HOUR = 10 + HOUR_OF_DAY = 11 + MINUTE = 12 + SECOND = 13 + MILLISECOND = 14 + WEEK_OF_MONTH = 15 + WEEK_OF_YEAR = 16 + ERA = 17 + ZONE_OFFSET = 18 + DST_OFFSET = 19 + FIELD_COUNT = 20 + + # Day of week constants + SUNDAY = 1 + MONDAY = 2 + TUESDAY = 3 + WEDNESDAY = 4 + THURSDAY = 5 + FRIDAY = 6 + SATURDAY = 7 + + # Month constants + JANUARY = 0 + FEBRUARY = 1 + MARCH = 2 + APRIL = 3 + MAY = 4 + JUNE = 5 + JULY = 6 + AUGUST = 7 + SEPTEMBER = 8 + OCTOBER = 9 + NOVEMBER = 10 + DECEMBER = 11 + UNDECIMBER = 12 + + # AM/PM + AM = 0 + PM = 1 + + def __init__(self): + super().__init__() + self._dt = _datetime.datetime.now() + self._fields: Dict[int, int] = {} + self._is_set: Dict[int, bool] = {} + self._lenient = True + self._first_day_of_week = Calendar.SUNDAY + self._minimal_days_in_first_week = 1 + self._timezone = _datetime.timezone.utc + self._set_fields_from_dt() + + def _set_fields_from_dt(self) -> None: + self._fields[Calendar.YEAR] = self._dt.year + self._fields[Calendar.MONTH] = self._dt.month - 1 + self._fields[Calendar.DAY_OF_MONTH] = self._dt.day + self._fields[Calendar.DAY_OF_YEAR] = self._dt.timetuple().tm_yday + self._fields[Calendar.DAY_OF_WEEK] = self._dt.isoweekday() % 7 + 1 + self._fields[Calendar.HOUR] = self._dt.hour % 12 + self._fields[Calendar.HOUR_OF_DAY] = self._dt.hour + self._fields[Calendar.MINUTE] = self._dt.minute + self._fields[Calendar.SECOND] = self._dt.second + self._fields[Calendar.MILLISECOND] = self._dt.microsecond // 1000 + self._fields[Calendar.AM_PM] = Calendar.AM if self._dt.hour < 12 else Calendar.PM + self._fields[Calendar.WEEK_OF_YEAR] = self._dt.isocalendar()[1] + self._fields[Calendar.ERA] = 1 # AD = 1 + + def get(self, field: int) -> int: + return self._fields.get(field, 0) + + def set(self, field: int, value: int) -> None: + self._fields[field] = value + self._is_set[field] = True + + def setTime(self, date: 'Date') -> None: + self._dt = date._dt + self._set_fields_from_dt() + + def getTime(self) -> Date: + return Date(self._dt) + + def getTimeInMillis(self) -> int: + return int(self._dt.timestamp() * 1000) + + def setTimeInMillis(self, millis: int) -> None: + self._dt = _datetime.datetime.fromtimestamp(millis / 1000.0) + self._set_fields_from_dt() + + def add(self, field: int, amount: int) -> None: + if field == Calendar.YEAR: + self._dt = self._dt.replace(year=self._dt.year + amount) + elif field == Calendar.MONTH: + new_month = self._dt.month - 1 + amount + new_year = self._dt.year + new_month // 12 + new_month = new_month % 12 + try: + self._dt = self._dt.replace(year=new_year, month=new_month + 1) + except ValueError: + self._dt = self._dt.replace(year=new_year, month=new_month + 1, day=28) + elif field == Calendar.DAY_OF_MONTH: + self._dt += _datetime.timedelta(days=amount) + elif field == Calendar.HOUR_OF_DAY or field == Calendar.HOUR: + self._dt += _datetime.timedelta(hours=amount) + elif field == Calendar.MINUTE: + self._dt += _datetime.timedelta(minutes=amount) + elif field == Calendar.SECOND: + self._dt += _datetime.timedelta(seconds=amount) + elif field == Calendar.MILLISECOND: + self._dt += _datetime.timedelta(milliseconds=amount) + elif field == Calendar.WEEK_OF_YEAR or field == Calendar.WEEK_OF_MONTH: + self._dt += _datetime.timedelta(weeks=amount) + self._set_fields_from_dt() + + def roll(self, field: int, amount: int) -> None: + if field == Calendar.MONTH: + old_month = self._dt.month + new_month = ((old_month - 1 + amount) % 12) + 1 + try: + self._dt = self._dt.replace(month=new_month) + except ValueError: + self._dt = self._dt.replace(month=new_month, day=28) + elif field == Calendar.DAY_OF_MONTH: + import calendar as _cal + max_day = _cal.monthrange(self._dt.year, self._dt.month)[1] + new_day = ((self._dt.day - 1 + amount) % max_day) + 1 + self._dt = self._dt.replace(day=new_day) + else: + self.add(field, amount) + self._set_fields_from_dt() + + def before(self, when: 'Calendar') -> bool: + return self._dt < when._dt + + def after(self, when: 'Calendar') -> bool: + return self._dt > when._dt + + def compareTo(self, another: 'Calendar') -> int: + return (self._dt > another._dt) - (self._dt < another._dt) + + def equals(self, other) -> bool: + if isinstance(other, Calendar): + return self._dt == other._dt + return False + + def hashCode(self) -> int: + return hash(self._dt) & 0x7FFFFFFF + + def toString(self) -> str: + return str(self._dt) + + def isLenient(self) -> bool: + return self._lenient + + def setLenient(self, lenient: bool) -> None: + self._lenient = lenient + + def getFirstDayOfWeek(self) -> int: + return self._first_day_of_week + + def setFirstDayOfWeek(self, value: int) -> None: + self._first_day_of_week = value + + def getMinimalDaysInFirstWeek(self) -> int: + return self._minimal_days_in_first_week + + def setMinimalDaysInFirstWeek(self, value: int) -> None: + self._minimal_days_in_first_week = value + + def getActualMaximum(self, field: int) -> int: + import calendar as _cal + if field == Calendar.DAY_OF_MONTH: + return _cal.monthrange(self._dt.year, self._dt.month)[1] + if field == Calendar.DAY_OF_YEAR: + return 366 if self._dt.year % 4 == 0 and (self._dt.year % 100 != 0 or self._dt.year % 400 == 0) else 365 + if field == Calendar.MONTH: + return 11 + if field == Calendar.HOUR: + return 11 + if field == Calendar.HOUR_OF_DAY: + return 23 + if field == Calendar.MINUTE: + return 59 + if field == Calendar.SECOND: + return 59 + if field == Calendar.MILLISECOND: + return 999 + return 0 + + def getActualMinimum(self, field: int) -> int: + return 0 + + def getDisplayName(self, field: int, style: int, locale: 'Locale') -> str: + return str(self.get(field)) + + @staticmethod + def getInstance() -> 'GregorianCalendar': + return GregorianCalendar() + + +class GregorianCalendar(Calendar): + """java.util.GregorianCalendar""" + BC = 0 + AD = 1 + + def __init__(self, *args): + super().__init__() + if len(args) == 0: + self._dt = _datetime.datetime.now() + elif len(args) == 3: + year, month, day = args + self._dt = _datetime.datetime(year, month + 1, day) + elif len(args) >= 5: + year, month, day, hour, minute = args[:5] + second = args[5] if len(args) > 5 else 0 + self._dt = _datetime.datetime(year, month + 1, day, hour, minute, second) + else: + self._dt = _datetime.datetime.now() + self._set_fields_from_dt() + + def isLeapYear(self, year: int) -> bool: + return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0 + + def getGreatestMinimum(self, field: int) -> int: + return self.getActualMinimum(field) + + def getLeastMaximum(self, field: int) -> int: + return self.getActualMaximum(field) + + def getMaximum(self, field: int) -> int: + if field == Calendar.DAY_OF_MONTH: + return 31 + if field == Calendar.DAY_OF_YEAR: + return 366 + if field == Calendar.MONTH: + return 11 + return self.getActualMaximum(field) + + def getMinimum(self, field: int) -> int: + return 0 + + +# ============================================================ +# UUID +# ============================================================ + +class UUID(Stub): + """java.util.UUID — wraps Python uuid module""" + def __init__(self, most_sig_bits: int, least_sig_bits: int): + super().__init__() + self._most_sig_bits = most_sig_bits & 0xFFFFFFFFFFFFFFFF + self._least_sig_bits = least_sig_bits & 0xFFFFFFFFFFFFFFFF + # Rebuild a Python UUID from the bits for string representation + hex_str = f"{self._most_sig_bits:016x}{self._least_sig_bits:016x}" + self._uuid = _uuid.UUID(hex_str) + + @staticmethod + def randomUUID() -> 'UUID': + u = _uuid.uuid4() + most = u.int >> 64 + least = u.int & 0xFFFFFFFFFFFFFFFF + return UUID(most, least) + + @staticmethod + def nameUUIDFromBytes(name: bytes) -> 'UUID': + u = _uuid.uuid3(_uuid.NAMESPACE_DNS, name.decode('utf-8', errors='replace')) + most = u.int >> 64 + least = u.int & 0xFFFFFFFFFFFFFFFF + return UUID(most, least) + + @staticmethod + def fromString(name: str) -> 'UUID': + u = _uuid.UUID(name) + most = u.int >> 64 + least = u.int & 0xFFFFFFFFFFFFFFFF + return UUID(most, least) + + def getMostSignificantBits(self) -> int: + return self._most_sig_bits + + def getLeastSignificantBits(self) -> int: + return self._least_sig_bits + + def version(self) -> int: + return self._uuid.version + + def variant(self) -> int: + return self._uuid.variant + + def timestamp(self) -> int: + if self.version() != 1: + raise UnsupportedOperationException("Not a time-based UUID") + return self._uuid.time + + def clockSequence(self) -> int: + if self.version() != 1: + raise UnsupportedOperationException("Not a time-based UUID") + return self._uuid.clock_seq + + def node(self) -> int: + if self.version() != 1: + raise UnsupportedOperationException("Not a time-based UUID") + return self._uuid.node + + def compareTo(self, other: 'UUID') -> int: + return (self._uuid.int > other._uuid.int) - (self._uuid.int < other._uuid.int) + + def equals(self, other) -> bool: + if isinstance(other, UUID): + return self._uuid == other._uuid + return False + + def hashCode(self) -> int: + return hash(self._uuid) & 0x7FFFFFFF + + def toString(self) -> str: + return str(self._uuid) + + def __str__(self): + return str(self._uuid) + + def __repr__(self): + return f"UUID({str(self._uuid)})" + + +# ============================================================ +# StringTokenizer +# ============================================================ + +class StringTokenizer(Stub): + """java.util.StringTokenizer — simple tokenizer""" + def __init__(self, s: str, delim: str = " \t\n\r\f", return_delims: bool = False): + super().__init__() + self._str = str(s) + self._delim = delim + self._return_delims = return_delims + self._tokens = self._tokenize() + self._index = 0 + + def _tokenize(self): + if self._return_delims: + tokens = [] + buf = "" + for ch in self._str: + if ch in self._delim: + if buf: + tokens.append(buf) + buf = "" + tokens.append(ch) + else: + buf += ch + if buf: + tokens.append(buf) + return tokens + import re as _re + pattern = f"[{_re.escape(self._delim)}]+" + return [t for t in _re.split(pattern, self._str) if t] + + def countTokens(self) -> int: + return len(self._tokens) + + def hasMoreTokens(self) -> bool: + return self._index < len(self._tokens) + + def nextToken(self, delim: str = None) -> str: + if delim is not None: + self._delim = delim + remaining = self._str[self._index:] + st = StringTokenizer(remaining, delim, self._return_delims) + self._tokens = st._tokens + self._index = 0 + if not self.hasMoreTokens(): + raise NoSuchElementException() + tok = self._tokens[self._index] + self._index += 1 + return tok + + def hasMoreElements(self) -> bool: + return self.hasMoreTokens() + + def nextElement(self): + return self.nextToken() + + def __iter__(self): + return self + + def __next__(self): + if not self.hasMoreTokens(): + raise StopIteration + return self.nextToken() + + +# ============================================================ +# Optional +# ============================================================ + +class Optional(Stub): + """java.util.Optional — wraps value or None""" + def __init__(self, value=None): + super().__init__() + self._value = value + + @staticmethod + def empty() -> 'Optional': + return Optional(None) + + @staticmethod + def of(value) -> 'Optional': + if value is None: + raise TypeError("value is null") + return Optional(value) + + @staticmethod + def ofNullable(value) -> 'Optional': + return Optional(value) + + def get(self): + if self._value is None: + raise NoSuchElementException("No value present") + return self._value + + def isPresent(self) -> bool: + return self._value is not None + + def isEmpty(self) -> bool: + return self._value is None + + def ifPresent(self, consumer) -> None: + if self._value is not None: + consumer(self._value) + + def ifPresentOrElse(self, action, empty_action) -> None: + if self._value is not None: + action(self._value) + else: + empty_action() + + def filter(self, predicate) -> 'Optional': + if self._value is None: + return self + return Optional.ofNullable(self._value) if predicate(self._value) else Optional.empty() + + def map(self, mapper) -> 'Optional': + if self._value is None: + return Optional.empty() + return Optional.ofNullable(mapper(self._value)) + + def flatMap(self, mapper) -> 'Optional': + if self._value is None: + return Optional.empty() + return mapper(self._value) + + def orElse(self, other): + return self._value if self._value is not None else other + + def orElseGet(self, supplier): + if self._value is not None: + return self._value + return supplier() + + def orElseThrow(self, exception_supplier=None): + if self._value is not None: + return self._value + if exception_supplier: + raise exception_supplier() + raise NoSuchElementException("No value present") + + def stream(self): + if self._value is not None: + return [self._value] + return [] + + def equals(self, other) -> bool: + if isinstance(other, Optional): + return self._value == other._value + return False + + def hashCode(self) -> int: + return hash(self._value) & 0x7FFFFFFF if self._value is not None else 0 + + def toString(self) -> str: + if self._value is None: + return "Optional.empty" + return f"Optional[{self._value}]" + + def __str__(self): + return self.toString() + + def __repr__(self): + return self.toString() + + def __bool__(self): + return self._value is not None + + +# ============================================================ +# Random +# ============================================================ + +class Random(Stub): + """java.util.Random — wraps Python random module""" + def __init__(self, seed: _typing.Optional[int] = None): + super().__init__() + self._rng = _py_random.Random(seed) + + def setSeed(self, seed: int) -> None: + self._rng.seed(seed) + + def nextInt(self, bound: _typing.Optional[int] = None) -> int: + if bound is None: + return self._rng.randint(-2147483648, 2147483647) + if bound <= 0: + raise IllegalArgumentException("bound must be positive") + return self._rng.randrange(0, bound) + + def nextLong(self) -> int: + return (self._rng.randint(-2147483648, 2147483647) << 32) | self._rng.randint(0, 0xFFFFFFFF) + + def nextBoolean(self) -> bool: + return self._rng.random() < 0.5 + + def nextFloat(self) -> float: + return self._rng.random() + + def nextDouble(self) -> float: + return self._rng.random() + + def nextGaussian(self) -> float: + return self._rng.gauss(0.0, 1.0) + + def nextBytes(self, bytes_arr: bytearray) -> None: + for i in range(len(bytes_arr)): + bytes_arr[i] = self._rng.randint(0, 255) + + def next(self, bits: int) -> int: + return self._rng.getrandbits(bits) & ((1 << bits) - 1) + + @staticmethod + def create() -> 'Random': + return Random() + + +# ============================================================ +# Properties +# ============================================================ + +class Properties(HashMap): + """java.util.Properties — extends Hashtable-like dict with load/store""" + def __init__(self, defaults: 'Properties' = None): + super().__init__() + self._defaults = defaults + + def setProperty(self, key: str, value: str) -> None: + self.put(str(key), str(value)) + + def getProperty(self, key: str, default_value: str = None) -> _typing.Optional[str]: + val = self._data.get(str(key)) + if val is not None: + return str(val) + if self._defaults is not None: + return self._defaults.getProperty(key, default_value) + return default_value + + def load(self, in_stream) -> None: + if isinstance(in_stream, (str, bytes)): + lines = in_stream.splitlines() if isinstance(in_stream, str) else in_stream.decode('utf-8').splitlines() + else: + lines = in_stream.read().splitlines() if hasattr(in_stream, 'read') else in_stream.splitlines() + for line in lines: + line = line.strip() + if not line or line.startswith('#') or line.startswith('!'): + continue + if '=' in line: + key, val = line.split('=', 1) + elif ':' in line: + key, val = line.split(':', 1) + else: + key = line + val = "" + key = key.strip() + val = val.strip() + self._data[key] = val + + def store(self, out_stream, comments: str = None) -> None: + lines = [] + if comments: + lines.append(f"#{comments}") + lines.append(f"# {_datetime.datetime.now().isoformat()}") + for k, v in self._data.items(): + escaped_val = str(v).replace("\\", "\\\\").replace(" ", "\\ ") + lines.append(f"{k}={escaped_val}") + content = "\n".join(lines) + "\n" + if isinstance(out_stream, _io.IOBase): + out_stream.write(content) + else: + out_stream.write(content) # type: ignore + + def list(self, out_stream) -> None: + for k, v in self._data.items(): + out_stream.write(f"{k}={v}\n") + + def propertyNames(self): + return Iterator(self._data.keys()) + + def stringPropertyNames(self) -> set: + names = set() + for k in self._data.keys(): + if isinstance(k, str): + names.add(k) + if self._defaults: + names.update(self._defaults.stringPropertyNames()) + return names + + def loadFromXML(self, in_stream) -> None: + import xml.etree.ElementTree as _ET + tree = _ET.parse(in_stream) + root = tree.getroot() + for entry in root.findall('entry'): + key = entry.get('key', '') + self._data[key] = entry.text or '' + + def storeToXML(self, out_stream, comment: str = None) -> None: + import xml.etree.ElementTree as _ET + root = _ET.Element('properties') + if comment: + c = _ET.SubElement(root, 'comment') + c.text = comment + for k, v in self._data.items(): + entry = _ET.SubElement(root, 'entry') + entry.set('key', str(k)) + entry.text = str(v) + tree = _ET.ElementTree(root) + tree.write(out_stream, xml_declaration=True, encoding='UTF-8') + + def get(self, key): + val = self._data.get(key) + if val is not None: + return val + return self._defaults.get(key) if self._defaults else None + + def put(self, key, value): + return super().put(str(key), str(value)) + + def toString(self) -> str: + items = ", ".join(f"{k}={v}" for k, v in self._data.items()) + return f"{{{items}}}" + + +# ============================================================ +# EnumSet +# ============================================================ + +class EnumSet(HashSet): + """java.util.EnumSet — simple implementation using set""" + def __init__(self, c=None): + super().__init__() + self._element_type = None + if c is not None: + if isinstance(c, EnumSet): + self._data = set(c._data) + self._element_type = c._element_type + elif isinstance(c, type): + self._element_type = c + elif isinstance(c, _IterableABC): + for item in c: + self._data.add(item) + if self._element_type is None: + self._element_type = item.__class__ + + @staticmethod + def allOf(element_type) -> 'EnumSet': + es = EnumSet() + es._element_type = element_type + if hasattr(element_type, '_values'): + for v in element_type._values(): + es._data.add(v) + elif hasattr(element_type, '__members__'): + for name, member in element_type.__members__.items(): + es._data.add(member) + return es + + @staticmethod + def noneOf(element_type) -> 'EnumSet': + es = EnumSet() + es._element_type = element_type + return es + + @staticmethod + def range(element_type, from_val, to_val) -> 'EnumSet': + es = EnumSet() + es._element_type = element_type + started = False + if hasattr(element_type, '_values'): + for v in element_type._values(): + if v == from_val: + started = True + if started: + es._data.add(v) + if v == to_val: + break + elif hasattr(element_type, '__members__'): + for name, member in element_type.__members__.items(): + if member == from_val: + started = True + if started: + es._data.add(member) + if member == to_val: + break + return es + + @staticmethod + def copyOf(es) -> 'EnumSet': + return EnumSet(es) + + def complementOf(self, other: 'EnumSet') -> 'EnumSet': + result = EnumSet.noneOf(self._element_type) + if self._element_type and hasattr(self._element_type, '_values'): + for v in self._element_type._values(): + if v not in other._data: + result._data.add(v) + return result + + +# ============================================================ +# EnumMap +# ============================================================ + +class EnumMap(HashMap): + """java.util.EnumMap — simple implementation using dict""" + def __init__(self, key_type=None, m=None): + super().__init__() + self._key_type = key_type + if m is not None: + if isinstance(m, EnumMap): + self._data.update(m._data) + self._key_type = m._key_type + elif isinstance(m, dict): + self._data.update(m) + else: + for k, v in m.items(): + self._data[k] = v + + def put(self, key, value): + if self._key_type is None: + self._key_type = key.__class__ + return super().put(key, value) + + def keySet(self): + return set(self._data.keys()) + + +# ============================================================ +# Objects +# ============================================================ + +class Objects: + """java.util.Objects — static helper methods""" + @staticmethod + def equals(a, b) -> bool: + return a == b + + @staticmethod + def deepEquals(a, b) -> bool: + return _deep_eq(a, b) + + @staticmethod + def hashCode(o) -> int: + return 0 if o is None else (hash(o) & 0x7FFFFFFF) + + @staticmethod + def hash(*values) -> int: + return Arrays.hashCode(values) + + @staticmethod + def toString(o, null_default: str = "null") -> str: + if o is None: + return null_default + return str(o) + + @staticmethod + def compare(a, b, cmp) -> int: + if cmp: + return cmp(a, b) + return (a > b) - (a < b) + + @staticmethod + def requireNonNull(obj, message: Any = None): + if obj is None: + if message is None: + raise TypeError("null") + raise TypeError(str(message) if not callable(message) else message.get()) + return obj + + @staticmethod + def nonNull(obj) -> bool: + return obj is not None + + @staticmethod + def isNull(obj) -> bool: + return obj is None + + @staticmethod + def requireNonNullElse(obj, default): + return obj if obj is not None else default + + @staticmethod + def requireNonNullElseGet(obj, supplier): + return obj if obj is not None else supplier() + + @staticmethod + def checkIndex(index: int, length: int) -> int: + if index < 0 or index >= length: + raise IndexError(f"Index {index} out of bounds [0, {length})") + return index + + @staticmethod + def checkFromToIndex(from_index: int, to_index: int, length: int) -> int: + if from_index < 0 or from_index > to_index or to_index > length: + raise IndexError(f"Range [{from_index}, {to_index}) out of bounds [0, {length})") + return from_index + + @staticmethod + def checkFromIndexSize(from_index: int, size: int, length: int) -> int: + if from_index < 0 or size < 0 or from_index + size > length: + raise IndexError(f"Range [{from_index}, {from_index + size}) out of bounds [0, {length})") + return from_index + + +# ============================================================ +# Locale +# ============================================================ + +class Locale(Stub): + """java.util.Locale — basic locale representation""" + ENGLISH = None + FRENCH = None + GERMAN = None + ITALIAN = None + JAPANESE = None + KOREAN = None + CHINESE = None + SIMPLIFIED_CHINESE = None + TRADITIONAL_CHINESE = None + FRANCE = None + GERMANY = None + ITALY = None + JAPAN = None + KOREA = None + CHINA = None + UK = None + US = None + CANADA = None + CANADA_FRENCH = None + ROOT = None + + def __init__(self, language: str = "", country: str = "", variant: str = ""): + super().__init__() + self._language = language + self._country = country + self._variant = variant + + @staticmethod + def getDefault() -> 'Locale': + return Locale("en", "US") + + @staticmethod + def setDefault(locale: 'Locale') -> None: + pass + + def getLanguage(self) -> str: + return self._language + + def getCountry(self) -> str: + return self._country + + def getVariant(self) -> str: + return self._variant + + def getDisplayLanguage(self, locale: 'Locale' = None) -> str: + return self._language + + def getDisplayCountry(self, locale: 'Locale' = None) -> str: + return self._country + + def getDisplayName(self, locale: 'Locale' = None) -> str: + parts = [self._language] + if self._country: + parts.append(self._country) + if self._variant: + parts.append(self._variant) + return "_".join(parts) + + def toString(self) -> str: + return self.getDisplayName() + + def toLanguageTag(self) -> str: + tag = self._language + if self._country: + tag += "-" + self._country + if self._variant: + tag += "-" + self._variant + return tag + + @staticmethod + def forLanguageTag(tag: str) -> 'Locale': + parts = tag.replace("-", "_").split("_") + lang = parts[0] if len(parts) > 0 else "" + country = parts[1] if len(parts) > 1 else "" + variant = parts[2] if len(parts) > 2 else "" + return Locale(lang, country, variant) + + def equals(self, other) -> bool: + if isinstance(other, Locale): + return (self._language == other._language and + self._country == other._country and + self._variant == other._variant) + return False + + def hashCode(self) -> int: + return hash((self._language, self._country, self._variant)) & 0x7FFFFFFF + + +# Initialize Locale constants +Locale.ENGLISH = Locale("en") +Locale.FRENCH = Locale("fr") +Locale.GERMAN = Locale("de") +Locale.ITALIAN = Locale("it") +Locale.JAPANESE = Locale("ja") +Locale.KOREAN = Locale("ko") +Locale.CHINESE = Locale("zh") +Locale.SIMPLIFIED_CHINESE = Locale("zh", "CN") +Locale.TRADITIONAL_CHINESE = Locale("zh", "TW") +Locale.FRANCE = Locale("fr", "FR") +Locale.GERMANY = Locale("de", "DE") +Locale.ITALY = Locale("it", "IT") +Locale.JAPAN = Locale("ja", "JP") +Locale.KOREA = Locale("ko", "KR") +Locale.CHINA = Locale("zh", "CN") +Locale.UK = Locale("en", "GB") +Locale.US = Locale("en", "US") +Locale.CANADA = Locale("en", "CA") +Locale.CANADA_FRENCH = Locale("fr", "CA") +Locale.ROOT = Locale("", "") + + +# ============================================================ +# BitSet +# ============================================================ + +class BitSet(Stub): + """java.util.BitSet — bit manipulation""" + def __init__(self, nbits: int = 0): + super().__init__() + if isinstance(nbits, int): + self._bits = 0 + elif isinstance(nbits, BitSet): + self._bits = nbits._bits + else: + self._bits = 0 + + def _check(self, bit_index: int) -> None: + if bit_index < 0: + raise IndexError(f"bitIndex < 0: {bit_index}") + + def get(self, bit_index: int) -> bool: + self._check(bit_index) + return bool((self._bits >> bit_index) & 1) + + def getRange(self, from_index: int, to_index: int) -> 'BitSet': + self._check(from_index) + mask = ((1 << (to_index - from_index)) - 1) << from_index + result = BitSet() + result._bits = (self._bits & mask) >> from_index + return result + + def set(self, bit_index: int, value: bool = True) -> None: + self._check(bit_index) + if value: + self._bits |= (1 << bit_index) + else: + self._bits &= ~(1 << bit_index) + + def setRange(self, from_index: int, to_index: int, value: bool = True) -> None: + self._check(from_index) + mask = ((1 << (to_index - from_index)) - 1) << from_index + if value: + self._bits |= mask + else: + self._bits &= ~mask + + def clear(self, bit_index: int = None) -> None: + if bit_index is None: + self._bits = 0 + else: + self._check(bit_index) + self._bits &= ~(1 << bit_index) + + def clearRange(self, from_index: int, to_index: int) -> None: + self.setRange(from_index, to_index, False) + + def flip(self, bit_index: int) -> None: + self._check(bit_index) + self._bits ^= (1 << bit_index) + + def flipRange(self, from_index: int, to_index: int) -> None: + self._check(from_index) + mask = ((1 << (to_index - from_index)) - 1) << from_index + self._bits ^= mask + + def cardinality(self) -> int: + return self._bits.bit_count() + + def intersects(self, bs: 'BitSet') -> bool: + return (self._bits & bs._bits) != 0 + + def and_(self, bs: 'BitSet') -> None: + self._bits &= bs._bits + + def or_(self, bs: 'BitSet') -> None: + self._bits |= bs._bits + + def xor(self, bs: 'BitSet') -> None: + self._bits ^= bs._bits + + def andNot(self, bs: 'BitSet') -> None: + self._bits &= ~bs._bits + + def size(self) -> int: + return self._bits.bit_length() + (8 - (self._bits.bit_length() % 8)) % 8 + + def length(self) -> int: + return self._bits.bit_length() + + def isEmpty(self) -> bool: + return self._bits == 0 + + def nextSetBit(self, from_index: int) -> int: + self._check(from_index) + bits = self._bits >> from_index + if bits == 0: + return -1 + return (bits & -bits).bit_length() - 1 + from_index + + def nextClearBit(self, from_index: int) -> int: + self._check(from_index) + bits = ~(self._bits >> from_index) + if bits == 0: + return -1 + return (bits & -bits).bit_length() - 1 + from_index + + def previousSetBit(self, from_index: int) -> int: + if from_index < 0: + return -1 + mask = (1 << (from_index + 1)) - 1 + bits = self._bits & mask + if bits == 0: + return -1 + return bits.bit_length() - 1 + + def previousClearBit(self, from_index: int) -> int: + if from_index < 0: + return -1 + mask = (1 << (from_index + 1)) - 1 + bits = (~self._bits) & mask + if bits == 0: + return -1 + return bits.bit_length() - 1 + + def toByteArray(self) -> bytearray: + if self._bits == 0: + return bytearray() + n_bytes = (self._bits.bit_length() + 7) // 8 + return bytearray(self._bits.to_bytes(n_bytes, 'little')) + + def toLongArray(self) -> list: + if self._bits == 0: + return [] + n_longs = (self._bits.bit_length() + 63) // 64 + result = [] + for i in range(n_longs): + result.append((self._bits >> (i * 64)) & 0xFFFFFFFFFFFFFFFF) + return result + + @staticmethod + def valueOf(bytes_or_longs) -> 'BitSet': + bs = BitSet() + if isinstance(bytes_or_longs, (bytes, bytearray)): + bs._bits = int.from_bytes(bytes_or_longs, 'little') + elif isinstance(bytes_or_longs, list): + for i, v in enumerate(bytes_or_longs): + bs._bits |= (v & 0xFFFFFFFFFFFFFFFF) << (i * 64) + return bs + + def stream(self): + bits = [] + i = 0 + while True: + idx = self.nextSetBit(i) + if idx < 0: + break + bits.append(idx) + i = idx + 1 + return bits + + def equals(self, other) -> bool: + if isinstance(other, BitSet): + return self._bits == other._bits + return False + + def hashCode(self) -> int: + return hash(self._bits) & 0x7FFFFFFF + + def toString(self) -> str: + bits = [] + i = 0 + while True: + idx = self.nextSetBit(i) + if idx < 0: + break + bits.append(str(idx)) + i = idx + 1 + return "{" + ", ".join(bits) + "}" + + def clone(self) -> 'BitSet': + return BitSet(self) + + def __repr__(self): + return self.toString() + + def __eq__(self, other): + return self.equals(other) + + +# ============================================================ +# PriorityQueue +# ============================================================ + +class PriorityQueue(Stub): + """java.util.PriorityQueue — using heapq""" + def __init__(self, initial_capacity: Any = None, comparator=None): + super().__init__() + self._heap: list = [] + self._comparator = comparator + if initial_capacity is not None and not isinstance(initial_capacity, (int, float)): + if isinstance(initial_capacity, _IterableABC): + for item in initial_capacity: + self._heap.append(item) + if comparator: + self._heap.sort(key=comparator) + else: + self._heap.sort() + + def _wrap_key(self, obj): + if self._comparator: + return (self._comparator(obj), obj) + return obj + + def add(self, obj) -> bool: + _heapq.heappush(self._heap, self._wrap_key(obj)) + return True + + def offer(self, obj) -> bool: + return self.add(obj) + + def peek(self): + return self._heap[0][1] if self._heap and self._comparator else (self._heap[0] if self._heap else None) + + def element(self): + val = self.peek() + if val is None: + raise NoSuchElementException() + return val + + def poll(self): + if not self._heap: + return None + val = _heapq.heappop(self._heap) + return val[1] if self._comparator else val + + def remove(self, obj) -> bool: + if self._comparator: + wrapped = self._wrap_key(obj) + for i, v in enumerate(self._heap): + if v == wrapped: + self._heap[i] = self._heap[-1] + self._heap.pop() + _heapq.heapify(self._heap) + return True + else: + for i, v in enumerate(self._heap): + if v == obj: + self._heap[i] = self._heap[-1] + self._heap.pop() + _heapq.heapify(self._heap) + return True + return False + + def size(self) -> int: + return len(self._heap) + + def isEmpty(self) -> bool: + return len(self._heap) == 0 + + def clear(self) -> None: + self._heap.clear() + + def comparator(self): + return self._comparator + + def iterator(self) -> Iterator: + return Iterator(list(self._heap)) + + def toArray(self, a=None): + if self._comparator: + items = sorted([v[1] for v in self._heap], key=self._comparator) + else: + items = sorted(self._heap) + if a is None: + return items + return items[:len(a)] + + def forEach(self, action) -> None: + for item in list(self._heap): + action(item) + + def toString(self) -> str: + items = ", ".join(str(x) for x in self._heap) + return f"[{items}]" + + def __len__(self): + return len(self._heap) + + def __repr__(self): + return self.toString() + + +# ============================================================ +# Vector +# ============================================================ + +class Vector(Stub): + """java.util.Vector — synchronized list""" + def __init__(self, initial_capacity: Any = None, capacity_increment: int = 0): + super().__init__() + if initial_capacity is None: + self._data = [] + elif isinstance(initial_capacity, int): + self._data = [] + elif isinstance(initial_capacity, _IterableABC): + self._data = list(initial_capacity) + else: + self._data = [] + self._capacity_increment = capacity_increment + self._lock = _threading.Lock() + + def _synchronized(self, func, *args, **kwargs): + with self._lock: + return func(*args, **kwargs) + + def add(self, obj) -> bool: + with self._lock: + self._data.append(obj) + return True + + def addAt(self, index: int, obj) -> None: + with self._lock: + self._data.insert(index, obj) + + def addAll(self, c) -> bool: + with self._lock: + if isinstance(c, Vector): + self._data.extend(c._data) + else: + self._data.extend(c) + return True + + def addAllAt(self, index: int, c) -> bool: + with self._lock: + if isinstance(c, Vector): + self._data[index:index] = c._data + else: + self._data[index:index] = list(c) + return True + + def addElement(self, obj) -> None: + self.add(obj) + + def capacity(self) -> int: + with self._lock: + return len(self._data) + + def clear(self) -> None: + with self._lock: + self._data.clear() + + def clone(self) -> 'Vector': + with self._lock: + return Vector(self._data) + + def contains(self, obj) -> bool: + with self._lock: + return obj in self._data + + def containsAll(self, c) -> bool: + with self._lock: + if isinstance(c, Vector): + return all(x in self._data for x in c._data) + return all(x in self._data for x in c) + + def copyInto(self, an_array: list) -> None: + with self._lock: + for i, v in enumerate(self._data): + an_array[i] = v + + def elementAt(self, index: int): + with self._lock: + return self._data[index] + + def elements(self) -> Enumeration: + with self._lock: + return Enumeration(list(self._data)) + + def ensureCapacity(self, min_capacity: int) -> None: + pass + + def firstElement(self): + with self._lock: + if not self._data: + raise NoSuchElementException() + return self._data[0] + + def forEach(self, action) -> None: + with self._lock: + for item in list(self._data): + action(item) + + def get(self, index: int): + with self._lock: + return self._data[index] + + def indexOf(self, obj, index: int = 0) -> int: + with self._lock: + try: + return self._data.index(obj, index) + except ValueError: + return -1 + + def insertElementAt(self, obj, index: int) -> None: + with self._lock: + self._data.insert(index, obj) + + def isEmpty(self) -> bool: + with self._lock: + return len(self._data) == 0 + + def iterator(self) -> Iterator: + with self._lock: + return Iterator(list(self._data)) + + def lastElement(self): + with self._lock: + if not self._data: + raise NoSuchElementException() + return self._data[-1] + + def lastIndexOf(self, obj, index: int = None) -> int: + with self._lock: + if index is None: + index = len(self._data) - 1 + for i in range(index, -1, -1): + if self._data[i] == obj: + return i + return -1 + + def listIterator(self, index: int = 0) -> ListIterator: + with self._lock: + li = ListIterator(list(self._data)) + li._index = index + return li + + def remove(self, obj) -> bool: + with self._lock: + if obj in self._data: + self._data.remove(obj) + return True + return False + + def removeAt(self, index: int): + with self._lock: + return self._data.pop(index) + + def removeAll(self, c) -> bool: + with self._lock: + if isinstance(c, Vector): + items = c._data + else: + items = list(c) + old_len = len(self._data) + self._data = [x for x in self._data if x not in items] + return len(self._data) != old_len + + def removeAllElements(self) -> None: + with self._lock: + self._data.clear() + + def removeElement(self, obj) -> bool: + return self.remove(obj) + + def removeElementAt(self, index: int) -> None: + with self._lock: + self._data.pop(index) + + def removeIf(self, predicate) -> bool: + with self._lock: + old_len = len(self._data) + self._data = [x for x in self._data if not predicate(x)] + return len(self._data) != old_len + + def replaceAll(self, operator) -> None: + with self._lock: + self._data = [operator(x) for x in self._data] + + def retainAll(self, c) -> bool: + with self._lock: + if isinstance(c, Vector): + items = set(c._data) + else: + items = set(c) + old_len = len(self._data) + self._data = [x for x in self._data if x in items] + return len(self._data) != old_len + + def set(self, index: int, obj): + with self._lock: + old = self._data[index] + self._data[index] = obj + return old + + def setElementAt(self, obj, index: int) -> None: + with self._lock: + self._data[index] = obj + + def setSize(self, new_size: int) -> None: + with self._lock: + while len(self._data) < new_size: + self._data.append(None) + while len(self._data) > new_size: + self._data.pop() + + def size(self) -> int: + with self._lock: + return len(self._data) + + def sort(self, key=None, reverse: bool = False) -> None: + with self._lock: + self._data.sort(key=key, reverse=reverse) + + def subList(self, from_index: int, to_index: int) -> 'Vector': + with self._lock: + return Vector(self._data[from_index:to_index]) + + def toArray(self, a=None): + with self._lock: + if a is None: + return list(self._data) + if len(a) < len(self._data): + return list(self._data) + for i, v in enumerate(self._data): + a[i] = v + if len(self._data) < len(a): + a[len(self._data)] = None + return a + + def toString(self) -> str: + with self._lock: + items = ", ".join(str(x) for x in self._data) + return f"[{items}]" + + def trimToSize(self) -> None: + pass + + def __len__(self): + with self._lock: + return len(self._data) + + def __getitem__(self, idx): + with self._lock: + return self._data[idx] + + def __setitem__(self, idx, val): + with self._lock: + self._data[idx] = val + + def __delitem__(self, idx): + with self._lock: + del self._data[idx] + + def __iter__(self): + with self._lock: + return iter(list(self._data)) + + def __repr__(self): + return self.toString() + + def __contains__(self, obj): + with self._lock: + return obj in self._data + + def __eq__(self, other): + if isinstance(other, Vector): + with self._lock: + return self._data == other._data + return False + + +# ============================================================ +# Stack +# ============================================================ + +class Stack(Vector): + """java.util.Stack — extends Vector""" + def __init__(self): + super().__init__() + + def push(self, item): + with self._lock: + self._data.append(item) + return item + + def pop(self): + with self._lock: + if not self._data: + raise EmptyStackException() + return self._data.pop() + + def peek(self): + with self._lock: + if not self._data: + raise EmptyStackException() + return self._data[-1] + + def empty(self) -> bool: + with self._lock: + return len(self._data) == 0 + + def search(self, obj) -> int: + with self._lock: + try: + idx = len(self._data) - 1 - self._data[::-1].index(obj) + return len(self._data) - idx + except ValueError: + return -1 + + def add(self, obj) -> bool: + self.push(obj) + return True + + def remove(self, obj) -> bool: + with self._lock: + if obj in self._data: + self._data.remove(obj) + return True + return False \ No newline at end of file diff --git a/android_pyapi/java/util_concurrent.py b/android_pyapi/java/util_concurrent.py new file mode 100644 index 0000000..fd2c16a --- /dev/null +++ b/android_pyapi/java/util_concurrent.py @@ -0,0 +1,2037 @@ +""" +Java util.concurrent standard library APIs implemented in Python. +java.util.concurrent package - Executors, ThreadPoolExecutor, CountDownLatch, etc. +""" + +from .._core import Stub +from .lang import RuntimeException +import threading as _threading +import concurrent.futures as _futures +import time as _time +import queue as _queue +import math as _py_math + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) +CancellationException = type("CancellationException", (RuntimeException,), {}) +ExecutionException = type("ExecutionException", (RuntimeException,), {}) +RejectedExecutionException = type("RejectedExecutionException", (RuntimeException,), {}) +TimeoutException = type("TimeoutException", (RuntimeException,), {}) +BrokenBarrierException = type("BrokenBarrierException", (RuntimeException,), {}) +CompletionException = type("CompletionException", (RuntimeException,), {}) + +__all__ = [ + "Executors", "ExecutorService", "Executor", "ExecutorCompletionService", + "ThreadPoolExecutor", "AbstractExecutorService", + "ScheduledExecutorService", "ScheduledThreadPoolExecutor", + "FutureTask", "Future", "RunnableFuture", "RunnableScheduledFuture", + "Callable", "CompletionService", "CompletionStage", + "CountDownLatch", "CyclicBarrier", "Semaphore", "Phaser", "Exchanger", + "TimeUnit", + "TimeoutException", "CancellationException", "ExecutionException", + "RejectedExecutionException", "BrokenBarrierException", + "CompletionException", + "ConcurrentHashMap", "CopyOnWriteArrayList", "CopyOnWriteArraySet", + "ConcurrentLinkedQueue", "ConcurrentLinkedDeque", + "DelayQueue", "SynchronousQueue", "LinkedBlockingQueue", + "ArrayBlockingQueue", "PriorityBlockingQueue", + "BlockingQueue", "BlockingDeque", + "AtomicBoolean", "AtomicInteger", "AtomicLong", "AtomicReference", + "AtomicIntegerArray", "AtomicLongArray", "AtomicReferenceArray", + "AtomicIntegerFieldUpdater", "AtomicLongFieldUpdater", + "AtomicReferenceFieldUpdater", + "AtomicMarkableReference", "AtomicStampedReference", + "DoubleAccumulator", "DoubleAdder", + "LongAccumulator", "LongAdder", + "Lock", "ReentrantLock", "ReentrantReadWriteLock", + "ReadWriteLock", "Condition", "LockSupport", + "AbstractQueuedSynchronizer", "AbstractQueuedLongSynchronizer", + "Flow", "SubmissionPublisher", + "RecursiveAction", "RecursiveTask", "ForkJoinPool", + "ForkJoinTask", "ForkJoinWorkerThread", + "CompletableFuture", +] + + +# ============================================================ +# Interfaces +# ============================================================ + +class Callable(Stub): + def call(self): + raise NotImplementedError + + +class Future(Stub): + def get(self, timeout=None, unit=None): + raise NotImplementedError + + def cancel(self, may_interrupt): + raise NotImplementedError + + def isCancelled(self): + raise NotImplementedError + + def isDone(self): + raise NotImplementedError + + +class RunnableFuture(Future): + def run(self): + raise NotImplementedError + + +class RunnableScheduledFuture(RunnableFuture): + def isPeriodic(self): + raise NotImplementedError + + +class CompletionService(Stub): + def submit(self, task): + raise NotImplementedError + + def take(self): + raise NotImplementedError + + def poll(self, timeout=0, unit=None): + raise NotImplementedError + + +class CompletionStage(Stub): + pass + + +class Executor(Stub): + def execute(self, command): + raise NotImplementedError + + +class ExecutorService(Executor): + def submit(self, task, *args): + raise NotImplementedError + + def shutdown(self): + raise NotImplementedError + + def shutdownNow(self): + raise NotImplementedError + + def isShutdown(self): + raise NotImplementedError + + def isTerminated(self): + raise NotImplementedError + + def awaitTermination(self, timeout, unit): + raise NotImplementedError + + def invokeAll(self, tasks): + raise NotImplementedError + + def invokeAny(self, tasks): + raise NotImplementedError + + +class ScheduledExecutorService(ExecutorService): + def schedule(self, command, delay, unit): + raise NotImplementedError + + def scheduleAtFixedRate(self, command, initial_delay, period, unit): + raise NotImplementedError + + def scheduleWithFixedDelay(self, command, initial_delay, delay, unit): + raise NotImplementedError + + +# ============================================================ +# Runnable +# ============================================================ + +class Runnable(Stub): + def run(self): + raise NotImplementedError + + +# ============================================================ +# FutureTask +# ============================================================ + +class FutureTask(RunnableFuture): + def __init__(self, callable_or_runnable, result=None): + super().__init__() + self._callable = callable_or_runnable if isinstance(callable_or_runnable, Callable) else None + self._runnable = callable_or_runnable if isinstance(callable_or_runnable, Runnable) else None + self._result = result + self._done = False + self._cancelled = False + self._exception = None + self._lock = _threading.RLock() + self._done_event = _threading.Event() + + def run(self): + if self._cancelled: + return + try: + if self._callable: + self._result = self._callable.call() + elif self._runnable: + self._runnable.run() + with self._lock: + self._done = True + self._done_event.set() + except Exception as e: + with self._lock: + self._exception = e + self._done = True + self._done_event.set() + + def get(self, timeout=None, unit=None): + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + if not self._done_event.wait(seconds): + raise TimeoutException() + else: + self._done_event.wait() + with self._lock: + if self._exception: + raise ExecutionException(str(self._exception)) + return self._result + + def cancel(self, may_interrupt): + with self._lock: + if self._done: + return False + self._cancelled = True + self._done = True + self._done_event.set() + return True + + def isCancelled(self): + return self._cancelled + + def isDone(self): + return self._done + + def doneTask(self): + pass + + def set(self, v): + with self._lock: + self._result = v + self._done = True + self._done_event.set() + + def setException(self, ex): + with self._lock: + self._exception = ex + self._done = True + self._done_event.set() + + +# ============================================================ +# CompletableFuture +# ============================================================ + +class CompletableFuture(Future): + def __init__(self): + super().__init__() + self._result = None + self._exception = None + self._done = False + self._lock = _threading.RLock() + self._done_event = _threading.Event() + self._callbacks = [] + + def _run_callbacks(self): + for cb in self._callbacks: + try: + cb(self) + except Exception: + pass + + def complete(self, result): + with self._lock: + if self._done: + return self + self._result = result + self._done = True + self._done_event.set() + self._run_callbacks() + return self + + def completeExceptionally(self, ex): + with self._lock: + if self._done: + return self + self._exception = ex + self._done = True + self._done_event.set() + self._run_callbacks() + return self + + def cancel(self, may_interrupt): + with self._lock: + if self._done: + return False + self._cancelled = True + self._done = True + self._done_event.set() + self._run_callbacks() + return True + + def get(self, timeout=None, unit=None): + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + if not self._done_event.wait(seconds): + raise TimeoutException() + else: + self._done_event.wait() + with self._lock: + if self._exception: + raise ExecutionException(str(self._exception)) + return self._result + + def join(self): + return self.get() + + def isDone(self): + return self._done + + def isCancelled(self): + return getattr(self, "_cancelled", False) + + def thenApply(self, fn): + future = CompletableFuture() + def cb(f): + try: + future.complete(fn(f.get())) + except Exception as e: + future.completeExceptionally(e) + self._callbacks.append(cb) + return future + + def thenAccept(self, fn): + future = CompletableFuture() + def cb(f): + try: + fn(f.get()) + future.complete(None) + except Exception as e: + future.completeExceptionally(e) + self._callbacks.append(cb) + return future + + def thenRun(self, action): + future = CompletableFuture() + def cb(f): + try: + action.run() + future.complete(None) + except Exception as e: + future.completeExceptionally(e) + self._callbacks.append(cb) + return future + + def thenCombine(self, other, fn): + future = CompletableFuture() + def cb(f): + try: + other_result = other.get() + future.complete(fn(f.get(), other_result)) + except Exception as e: + future.completeExceptionally(e) + self._callbacks.append(cb) + return future + + def thenCompose(self, fn): + future = CompletableFuture() + def cb(f): + try: + next_future = fn(f.get()) + future.complete(next_future.get()) + except Exception as e: + future.completeExceptionally(e) + self._callbacks.append(cb) + return future + + def exceptionally(self, fn): + future = CompletableFuture() + def cb(f): + try: + f.get() + future.complete(f._result) + except Exception as e: + try: + fallback = fn(e) + future.complete(fallback) + except Exception as e2: + future.completeExceptionally(e2) + self._callbacks.append(cb) + return future + + @staticmethod + def completedFuture(result): + f = CompletableFuture() + f.complete(result) + return f + + @staticmethod + def failedFuture(exception): + f = CompletableFuture() + f.completeExceptionally(exception) + return f + + @staticmethod + def supplyAsync(supplier, executor=None): + f = CompletableFuture() + def run(): + try: + f.complete(supplier.get()) + except Exception as e: + f.completeExceptionally(e) + t = _threading.Thread(target=run, daemon=True) + t.start() + return f + + @staticmethod + def runAsync(runnable, executor=None): + f = CompletableFuture() + def run(): + try: + runnable.run() + f.complete(None) + except Exception as e: + f.completeExceptionally(e) + t = _threading.Thread(target=run, daemon=True) + t.start() + return f + + @staticmethod + def allOf(*futures): + result = CompletableFuture() + def check_all(): + for f in futures: + f.get() + result.complete(None) + t = _threading.Thread(target=check_all, daemon=True) + t.start() + return result + + @staticmethod + def anyOf(*futures): + result = CompletableFuture() + def check_any(): + while True: + for f in futures: + if f.isDone(): + result.complete(f.get()) + return + _time.sleep(0.01) + t = _threading.Thread(target=check_any, daemon=True) + t.start() + return result + + +# ============================================================ +# Executors +# ============================================================ + +class Executors(Stub): + @staticmethod + def newCachedThreadPool(): + pool = ThreadPoolExecutor(0, _futures.ThreadPoolExecutor(max_workers=10)) + pool._executor._max_workers = None + return pool + + @staticmethod + def newFixedThreadPool(n_threads): + return ThreadPoolExecutor(n_threads, _futures.ThreadPoolExecutor(max_workers=n_threads)) + + @staticmethod + def newSingleThreadExecutor(): + return ThreadPoolExecutor(1, _futures.ThreadPoolExecutor(max_workers=1)) + + @staticmethod + def newScheduledThreadPool(core_pool_size): + return ScheduledThreadPoolExecutor(core_pool_size) + + @staticmethod + def newWorkStealingPool(parallelism=None): + return ThreadPoolExecutor(parallelism or 4, _futures.ThreadPoolExecutor(max_workers=parallelism or 4)) + + @staticmethod + def defaultThreadFactory(): + return _threading.Thread + + @staticmethod + def privilegedThreadFactory(): + return _threading.Thread + + @staticmethod + def callable(runnable, result=None): + class _Callable(Callable): + def call(self): + if runnable: + runnable.run() + return result + return _Callable() + + +# ============================================================ +# ThreadPoolExecutor +# ============================================================ + +class ThreadPoolExecutor(ExecutorService): + def __init__(self, core_pool_size, executor=None): + super().__init__() + self._executor = executor or _futures.ThreadPoolExecutor(max_workers=core_pool_size) + self._core_pool_size = core_pool_size + self._max_pool_size = core_pool_size + self._keep_alive_time = 60 + self._allow_core_thread_timeout = False + + def execute(self, command): + def _run(): + try: + command.run() + except Exception: + pass + self._executor.submit(_run) + + def submit(self, task, *args): + if isinstance(task, Callable): + future = FutureTask(task) + self._executor.submit(future.run) + return future + elif isinstance(task, Runnable): + future = FutureTask(task) + self._executor.submit(future.run) + return future + else: + future = FutureTask(task) + self._executor.submit(future.run) + return future + + def shutdown(self): + self._executor.shutdown(wait=False) + + def shutdownNow(self): + self._executor.shutdown(wait=False) + + def isShutdown(self): + return self._executor._shutdown + + def isTerminated(self): + return self._executor._shutdown + + def awaitTermination(self, timeout, unit): + seconds = unit.toMillis(timeout) / 1000.0 + self._executor.shutdown(wait=True) + + def getPoolSize(self): + return self._core_pool_size + + def getActiveCount(self): + return 0 + + def getTaskCount(self): + return 0 + + def getCompletedTaskCount(self): + return 0 + + def getLargestPoolSize(self): + return self._core_pool_size + + def setCorePoolSize(self, size): + self._core_pool_size = size + + def setMaximumPoolSize(self, size): + self._max_pool_size = size + + def setKeepAliveTime(self, time, unit): + self._keep_alive_time = unit.toMillis(time) / 1000.0 + + def prestartAllCoreThreads(self): + pass + + def prestartCoreThread(self): + pass + + def allowsCoreThreadTimeOut(self): + return self._allow_core_thread_timeout + + def setRejectedExecutionHandler(self, handler): + pass + + def getRejectedExecutionHandler(self): + return None + + +class AbstractExecutorService(ExecutorService): + pass + + +# ============================================================ +# ScheduledThreadPoolExecutor +# ============================================================ + +class ScheduledThreadPoolExecutor(ThreadPoolExecutor, ScheduledExecutorService): + def __init__(self, core_pool_size): + super().__init__(core_pool_size) + + def schedule(self, command, delay, unit): + future = FutureTask(command) + seconds = unit.toMillis(delay) / 1000.0 + t = _threading.Timer(seconds, future.run) + t.daemon = True + t.start() + return future + + def scheduleAtFixedRate(self, command, initial_delay, period, unit): + seconds = unit.toMillis(period) / 1000.0 + init_sec = unit.toMillis(initial_delay) / 1000.0 + def _run(): + _time.sleep(init_sec) + while True: + command.run() + _time.sleep(seconds) + t = _threading.Thread(target=_run, daemon=True) + t.start() + return FutureTask(command) + + def scheduleWithFixedDelay(self, command, initial_delay, delay, unit): + return self.scheduleAtFixedRate(command, initial_delay, delay, unit) + + +class ExecutorCompletionService(CompletionService): + def __init__(self, executor): + super().__init__() + self._executor = executor + self._queue = _queue.Queue() + + def submit(self, task): + future = self._executor.submit(task) + self._queue.put(future) + return future + + def take(self): + return self._queue.get() + + def poll(self, timeout=0, unit=None): + if unit is not None: + sec = unit.toMillis(timeout) / 1000.0 + else: + sec = timeout + try: + return self._queue.get(timeout=sec) + except _queue.Empty: + return None + + +# ============================================================ +# TimeUnit +# ============================================================ + +class TimeUnit(Stub): + NANOSECONDS = None + MICROSECONDS = None + MILLISECONDS = None + SECONDS = None + MINUTES = None + HOURS = None + DAYS = None + + def __init__(self, name, scale): + self._name = name + self._scale = scale # nanos per unit + + def convert(self, source_duration, source_unit): + return int(source_duration * source_unit._scale / self._scale) + + def toNanos(self, duration): + return int(duration * self._scale) + + def toMicros(self, duration): + return int(duration * self._scale // 1000) + + def toMillis(self, duration): + return int(duration * self._scale // 1_000_000) + + def toSeconds(self, duration): + return int(duration * self._scale // 1_000_000_000) + + def toMinutes(self, duration): + return self.toSeconds(duration) // 60 + + def toHours(self, duration): + return self.toMinutes(duration) // 60 + + def toDays(self, duration): + return self.toHours(duration) // 24 + + def sleep(self, timeout): + _time.sleep(self.toMillis(timeout) / 1000.0) + + def timedJoin(self, thread, timeout): + thread.join(self.toMillis(timeout) / 1000.0) + + def timedWait(self, obj, timeout): + return True + + def toString(self): + return self._name + + +TimeUnit.NANOSECONDS = TimeUnit("NANOSECONDS", 1) +TimeUnit.MICROSECONDS = TimeUnit("MICROSECONDS", 1000) +TimeUnit.MILLISECONDS = TimeUnit("MILLISECONDS", 1_000_000) +TimeUnit.SECONDS = TimeUnit("SECONDS", 1_000_000_000) +TimeUnit.MINUTES = TimeUnit("MINUTES", 60_000_000_000) +TimeUnit.HOURS = TimeUnit("HOURS", 3_600_000_000_000) +TimeUnit.DAYS = TimeUnit("DAYS", 86_400_000_000_000) + + +# ============================================================ +# CountDownLatch +# ============================================================ + +class CountDownLatch(Stub): + def __init__(self, count): + super().__init__() + self._count = count + self._condition = _threading.Condition() + + def await_(self, timeout=None, unit=None): + with self._condition: + if self._count == 0: + return True + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + self._condition.wait(timeout=seconds) + return self._count == 0 + self._condition.wait() + return True + + def countDown(self): + with self._condition: + if self._count > 0: + self._count -= 1 + if self._count == 0: + self._condition.notify_all() + + def getCount(self): + return self._count + + def toString(self): + return f"CountDownLatch(count={self._count})" + + +# ============================================================ +# CyclicBarrier +# ============================================================ + +class CyclicBarrier(Stub): + def __init__(self, parties, barrier_action=None): + super().__init__() + self._parties = parties + self._barrier_action = barrier_action + self._count = parties + self._broken = False + self._lock = _threading.RLock() + self._condition = _threading.Condition() + + def await_(self, timeout=None, unit=None): + with self._condition: + if self._broken: + raise BrokenBarrierException() + self._count -= 1 + if self._count == 0: + if self._barrier_action: + try: + self._barrier_action.run() + except Exception: + self._broken = True + self._condition.notify_all() + raise BrokenBarrierException() + self._count = self._parties + self._condition.notify_all() + return 0 + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + self._condition.wait(timeout=seconds) + if self._count > 0: + self._broken = True + raise BrokenBarrierException() + else: + self._condition.wait() + return self._count + + def getNumberWaiting(self): + return self._parties - self._count + + def getParties(self): + return self._parties + + def isBroken(self): + return self._broken + + def reset(self): + with self._condition: + self._broken = False + self._count = self._parties + self._condition.notify_all() + + +# ============================================================ +# Semaphore +# ============================================================ + +class Semaphore(Stub): + def __init__(self, permits, fair=False): + super().__init__() + self._sem = _threading.Semaphore(permits) if not fair else _threading.Semaphore(permits) + self._fair = fair + self._permits = permits + self._lock = _threading.Lock() + + def acquire(self): + self._sem.acquire() + + def acquireUninterruptibly(self): + self._sem.acquire() + + def release(self): + self._sem.release() + + def tryAcquire(self, timeout=None, unit=None): + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + return self._sem.acquire(blocking=True, timeout=seconds) + return self._sem.acquire(blocking=False) + + def availablePermits(self): + return self._sem._value + + def drainPermits(self): + with self._lock: + val = self._sem._value + for _ in range(val): + self._sem.acquire(blocking=False) + return val + + def getQueueLength(self): + return 0 + + def hasQueuedThreads(self): + return False + + +# ============================================================ +# Phaser (stub) +# ============================================================ + +class Phaser(Stub): + def __init__(self, parties=0): + super().__init__() + self._parties = parties + self._phase = 0 + self._arrived = 0 + + def register(self): + self._parties += 1 + + def arrive(self): + self._arrived += 1 + return self._phase + + def arriveAndDeregister(self): + self._arrived += 1 + self._parties -= 1 + return self._phase + + def arriveAndAwaitAdvance(self): + self._arrived += 1 + if self._arrived >= self._parties: + self._phase += 1 + self._arrived = 0 + return self._phase + + def awaitAdvance(self, phase): + return self._phase + + def getPhase(self): + return self._phase + + def getArrivedParties(self): + return self._arrived + + def getRegisteredParties(self): + return self._parties + + def getUnarrivedParties(self): + return self._parties - self._arrived + + def isTerminated(self): + return False + + def forceTermination(self): + pass + + +# ============================================================ +# Exchanger +# ============================================================ + +class Exchanger(Stub): + def __init__(self): + super().__init__() + self._lock = _threading.Lock() + self._item = None + self._other_ready = _threading.Event() + + def exchange(self, x, timeout=None, unit=None): + with self._lock: + if self._item is None: + self._item = x + self._other_ready.set() + else: + result = self._item + self._item = x + self._other_ready.set() + return result + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + self._other_ready.wait(timeout=seconds) + else: + self._other_ready.wait() + with self._lock: + result = self._item + self._item = None + return result + + +# ============================================================ +# ConcurrentHashMap +# ============================================================ + +class ConcurrentHashMap(Stub): + def __init__(self, initial_capacity=16): + super().__init__() + self._map = {} + self._lock = _threading.RLock() + + def put(self, key, value): + with self._lock: + self._map[key] = value + return None + + def get(self, key): + with self._lock: + return self._map.get(key) + + def containsKey(self, key): + with self._lock: + return key in self._map + + def containsValue(self, value): + with self._lock: + return value in self._map.values() + + def size(self): + with self._lock: + return len(self._map) + + def isEmpty(self): + with self._lock: + return len(self._map) == 0 + + def remove(self, key): + with self._lock: + return self._map.pop(key, None) + + def clear(self): + with self._lock: + self._map.clear() + + def keySet(self): + with self._lock: + return set(self._map.keys()) + + def values(self): + with self._lock: + return list(self._map.values()) + + def entrySet(self): + with self._lock: + return set(self._map.items()) + + def putAll(self, m): + with self._lock: + if hasattr(m, "_map"): + self._map.update(m._map) + elif isinstance(m, dict): + self._map.update(m) + + def putIfAbsent(self, key, value): + with self._lock: + if key not in self._map: + self._map[key] = value + return None + return self._map[key] + + def getOrDefault(self, key, default): + with self._lock: + return self._map.get(key, default) + + def forEach(self, action): + with self._lock: + for k, v in self._map.items(): + action(k, v) + + +# ============================================================ +# CopyOnWriteArrayList / CopyOnWriteArraySet +# ============================================================ + +class CopyOnWriteArrayList(Stub): + def __init__(self, *args): + super().__init__() + self._list = [] + self._lock = _threading.RLock() + if args and isinstance(args[0], (list, tuple)): + self._list = list(args[0]) + + def add(self, e): + with self._lock: + self._list = list(self._list) + self._list.append(e) + return True + + def get(self, index): + return self._list[index] + + def set(self, index, element): + with self._lock: + old = self._list[index] + self._list = list(self._list) + self._list[index] = element + return old + + def remove(self, e): + with self._lock: + new_list = list(self._list) + try: + new_list.remove(e) + self._list = new_list + return True + except ValueError: + return False + + def size(self): + return len(self._list) + + def isEmpty(self): + return len(self._list) == 0 + + def contains(self, e): + return e in self._list + + def iterator(self): + return iter(list(self._list)) + + def toArray(self): + return list(self._list) + + def clear(self): + with self._lock: + self._list = [] + + +class CopyOnWriteArraySet(Stub): + def __init__(self): + super().__init__() + self._backend = CopyOnWriteArrayList() + + def add(self, e): + if self._backend.contains(e): + return False + return self._backend.add(e) + + def remove(self, e): + return self._backend.remove(e) + + def contains(self, e): + return self._backend.contains(e) + + def size(self): + return self._backend.size() + + def isEmpty(self): + return self._backend.isEmpty() + + def iterator(self): + return self._backend.iterator() + + +# ============================================================ +# BlockingQueue / LinkedBlockingQueue / ArrayBlockingQueue +# ============================================================ + +class BlockingQueue(Stub): + pass + + +class LinkedBlockingQueue(BlockingQueue): + def __init__(self, capacity=None): + super().__init__() + self._queue = _queue.Queue(maxsize=capacity if capacity else 0) + + def put(self, item): + self._queue.put(item) + + def take(self): + return self._queue.get() + + def offer(self, item, timeout=None, unit=None): + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + try: + self._queue.put(item, timeout=seconds) + return True + except _queue.Full: + return False + try: + self._queue.put_nowait(item) + return True + except _queue.Full: + return False + + def poll(self, timeout=None, unit=None): + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + try: + return self._queue.get(timeout=seconds) + except _queue.Empty: + return None + try: + return self._queue.get_nowait() + except _queue.Empty: + return None + + def size(self): + return self._queue.qsize() + + def isEmpty(self): + return self._queue.empty() + + def remainingCapacity(self): + return self._queue.maxsize - self._queue.qsize() + + def clear(self): + while not self._queue.empty(): + try: + self._queue.get_nowait() + except _queue.Empty: + break + + def contains(self, item): + return item in list(self._queue.queue) + + +class ArrayBlockingQueue(LinkedBlockingQueue): + def __init__(self, capacity, fair=False): + super().__init__(capacity) + + +class PriorityBlockingQueue(BlockingQueue): + def __init__(self, capacity=None): + super().__init__() + self._heap = [] + self._lock = _threading.RLock() + self._not_empty = _threading.Condition(self._lock) + + def put(self, item): + with self._lock: + _queue.heappush(self._heap, item) + self._not_empty.notify() + + def take(self): + with self._lock: + while not self._heap: + self._not_empty.wait() + return _queue.heappop(self._heap) + + def offer(self, item, timeout=None, unit=None): + self.put(item) + return True + + def poll(self, timeout=None, unit=None): + with self._lock: + if not self._heap: + return None + return _queue.heappop(self._heap) + + def size(self): + with self._lock: + return len(self._heap) + + def isEmpty(self): + with self._lock: + return len(self._heap) == 0 + + def clear(self): + with self._lock: + self._heap = [] + + +class SynchronousQueue(BlockingQueue): + def __init__(self, fair=False): + super().__init__() + self._exchange = _queue.Queue(maxsize=1) + + def put(self, item): + self._exchange.put(item) + + def take(self): + return self._exchange.get() + + def offer(self, item, timeout=None, unit=None): + try: + self._exchange.put_nowait(item) + return True + except _queue.Full: + return False + + def poll(self, timeout=None, unit=None): + try: + return self._exchange.get_nowait() + except _queue.Empty: + return None + + +class DelayQueue(BlockingQueue): + def __init__(self): + super().__init__() + self._queue = [] + + def put(self, item): + _queue.heappush(self._queue, item) + + def take(self): + while True: + if self._queue: + item = self._queue[0] + delay = item.getDelay(TimeUnit.MILLISECONDS) + if delay <= 0: + return _queue.heappop(self._queue) + _time.sleep(delay / 1000.0) + else: + _time.sleep(0.1) + + +class ConcurrentLinkedQueue(Stub): + def __init__(self): + super().__init__() + self._queue = _queue.Queue() + + def add(self, item): + self._queue.put(item) + return True + + def offer(self, item): + self._queue.put_nowait(item) + return True + + def poll(self): + try: + return self._queue.get_nowait() + except _queue.Empty: + return None + + def peek(self): + try: + item = self._queue.get_nowait() + self._queue.put_nowait(item) + return item + except _queue.Empty: + return None + + def size(self): + return self._queue.qsize() + + def isEmpty(self): + return self._queue.empty() + + def contains(self, item): + return item in list(self._queue.queue) + + +class ConcurrentLinkedDeque(ConcurrentLinkedQueue): + def addFirst(self, item): + self._queue.put(item) + + def addLast(self, item): + self._queue.put(item) + + def pollFirst(self): + return self.poll() + + def pollLast(self): + return self.poll() + + +class BlockingDeque(BlockingQueue): + pass + + +# ============================================================ +# Atomic classes +# ============================================================ + +class AtomicBoolean(Stub): + def __init__(self, initial=False): + super().__init__() + self._value = initial + self._lock = _threading.Lock() + + def get(self): + with self._lock: + return self._value + + def set(self, new_value): + with self._lock: + self._value = new_value + + def compareAndSet(self, expect, update): + with self._lock: + if self._value == expect: + self._value = update + return True + return False + + def getAndSet(self, new_value): + with self._lock: + old = self._value + self._value = new_value + return old + + def toString(self): + return str(self._value) + + +class AtomicInteger(Stub): + def __init__(self, initial=0): + super().__init__() + self._value = int(initial) + self._lock = _threading.Lock() + + def get(self): + with self._lock: + return self._value + + def set(self, new_value): + with self._lock: + self._value = int(new_value) + + def getAndSet(self, new_value): + with self._lock: + old = self._value + self._value = int(new_value) + return old + + def compareAndSet(self, expect, update): + with self._lock: + if self._value == expect: + self._value = int(update) + return True + return False + + def weakCompareAndSet(self, expect, update): + return self.compareAndSet(expect, update) + + def getAndIncrement(self): + with self._lock: + old = self._value + self._value += 1 + return old + + def getAndDecrement(self): + with self._lock: + old = self._value + self._value -= 1 + return old + + def getAndAdd(self, delta): + with self._lock: + old = self._value + self._value += delta + return old + + def incrementAndGet(self): + with self._lock: + self._value += 1 + return self._value + + def decrementAndGet(self): + with self._lock: + self._value -= 1 + return self._value + + def addAndGet(self, delta): + with self._lock: + self._value += delta + return self._value + + def lazySet(self, new_value): + with self._lock: + self._value = int(new_value) + + def updateAndGet(self, func): + with self._lock: + self._value = func(self._value) + return self._value + + def accumulateAndGet(self, x, func): + with self._lock: + self._value = func(self._value, x) + return self._value + + def intValue(self): + return self.get() + + def longValue(self): + return self.get() + + def floatValue(self): + return float(self.get()) + + def doubleValue(self): + return float(self.get()) + + def toString(self): + return str(self._value) + + +class AtomicLong(AtomicInteger): + pass + + +class AtomicReference(Stub): + def __init__(self, initial=None): + super().__init__() + self._value = initial + self._lock = _threading.Lock() + + def get(self): + with self._lock: + return self._value + + def set(self, new_value): + with self._lock: + self._value = new_value + + def compareAndSet(self, expect, update): + with self._lock: + if self._value == expect: + self._value = update + return True + return False + + def getAndSet(self, new_value): + with self._lock: + old = self._value + self._value = new_value + return old + + def getAndUpdate(self, func): + with self._lock: + old = self._value + self._value = func(old) + return old + + def updateAndGet(self, func): + with self._lock: + self._value = func(self._value) + return self._value + + def accumulateAndGet(self, x, func): + with self._lock: + self._value = func(self._value, x) + return self._value + + +class AtomicIntegerArray(Stub): + def __init__(self, length_or_array): + super().__init__() + if isinstance(length_or_array, int): + self._array = [0] * length_or_array + else: + self._array = list(length_or_array) + + def get(self, i): + return self._array[i] + + def set(self, i, new_value): + self._array[i] = new_value + + def compareAndSet(self, i, expect, update): + if self._array[i] == expect: + self._array[i] = update + return True + return False + + def getAndAdd(self, i, delta): + old = self._array[i] + self._array[i] += delta + return old + + def addAndGet(self, i, delta): + self._array[i] += delta + return self._array[i] + + def length(self): + return len(self._array) + + +class AtomicLongArray(AtomicIntegerArray): + pass + + +class AtomicReferenceArray(Stub): + def __init__(self, length_or_array): + super().__init__() + if isinstance(length_or_array, int): + self._array = [None] * length_or_array + else: + self._array = list(length_or_array) + + def get(self, i): + return self._array[i] + + def set(self, i, new_value): + self._array[i] = new_value + + def compareAndSet(self, i, expect, update): + if self._array[i] == expect: + self._array[i] = update + return True + return False + + def length(self): + return len(self._array) + + +class AtomicIntegerFieldUpdater(Stub): + @staticmethod + def newUpdater(tclass, field_name): + return AtomicIntegerFieldUpdater() + + def get(self, obj): + return 0 + + def set(self, obj, new_value): + pass + + def compareAndSet(self, obj, expect, update): + return True + + +class AtomicLongFieldUpdater(AtomicIntegerFieldUpdater): + pass + + +class AtomicReferenceFieldUpdater(Stub): + @staticmethod + def newUpdater(tclass, vclass, field_name): + return AtomicReferenceFieldUpdater() + + def get(self, obj): + return None + + def set(self, obj, new_value): + pass + + def compareAndSet(self, obj, expect, update): + return True + + +class AtomicMarkableReference(Stub): + def __init__(self, initial_ref, initial_mark): + super().__init__() + self._ref = initial_ref + self._mark = initial_mark + + def getReference(self): + return self._ref + + def isMarked(self): + return self._mark + + def compareAndSet(self, expected_ref, new_ref, expected_mark, new_mark): + if self._ref == expected_ref and self._mark == expected_mark: + self._ref = new_ref + self._mark = new_mark + return True + return False + + def attemptMark(self, expected_ref, new_mark): + if self._ref == expected_ref: + self._mark = new_mark + return True + return False + + +class AtomicStampedReference(Stub): + def __init__(self, initial_ref, initial_stamp): + super().__init__() + self._ref = initial_ref + self._stamp = initial_stamp + + def getReference(self): + return self._ref + + def getStamp(self): + return self._stamp + + def compareAndSet(self, expected_ref, new_ref, expected_stamp, new_stamp): + if self._ref == expected_ref and self._stamp == expected_stamp: + self._ref = new_ref + self._stamp = new_stamp + return True + return False + + def attemptStamp(self, expected_ref, new_stamp): + if self._ref == expected_ref: + self._stamp = new_stamp + return True + return False + + def set(self, new_ref, new_stamp): + self._ref = new_ref + self._stamp = new_stamp + + +class DoubleAccumulator(Stub): + def __init__(self, accumulator_function, identity): + super().__init__() + self._value = identity + self._func = accumulator_function + + def accumulate(self, x): + self._value = self._func(self._value, x) + + def get(self): + return self._value + + def reset(self): + self._value = 0.0 + + +class DoubleAdder(Stub): + def __init__(self): + super().__init__() + self._value = 0.0 + + def add(self, x): + self._value += x + + def sum(self): + return self._value + + def reset(self): + self._value = 0.0 + + def doubleValue(self): + return self._value + + def toString(self): + return str(self._value) + + +class LongAccumulator(Stub): + def __init__(self, accumulator_function, identity): + super().__init__() + self._value = identity + self._func = accumulator_function + + def accumulate(self, x): + self._value = self._func(self._value, x) + + def get(self): + return self._value + + def reset(self): + self._value = 0 + + +class LongAdder(Stub): + def __init__(self): + super().__init__() + self._value = 0 + + def add(self, x): + self._value += x + + def sum(self): + return self._value + + def reset(self): + self._value = 0 + + def intValue(self): + return self._value + + def longValue(self): + return self._value + + def toString(self): + return str(self._value) + + +# ============================================================ +# ReentrantLock +# ============================================================ + +class Lock(Stub): + def lock(self): + raise NotImplementedError + + def lockInterruptibly(self): + raise NotImplementedError + + def tryLock(self, timeout=None, unit=None): + raise NotImplementedError + + def unlock(self): + raise NotImplementedError + + def newCondition(self): + raise NotImplementedError + + +class ReentrantLock(Lock): + def __init__(self, fair=False): + super().__init__() + self._lock = _threading.RLock() if not fair else _threading.RLock() + + def lock(self): + self._lock.acquire() + + def lockInterruptibly(self): + self._lock.acquire() + + def tryLock(self, timeout=None, unit=None): + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + return self._lock.acquire(timeout=seconds) + return self._lock.acquire(blocking=False) + + def unlock(self): + self._lock.release() + + def newCondition(self): + return Condition() + + def getHoldCount(self): + return 1 if self._lock._is_owned() else 0 + + def isHeldByCurrentThread(self): + return self._lock._is_owned() + + def isLocked(self): + return True + + def getQueueLength(self): + return 0 + + def hasQueuedThreads(self): + return False + + def getOwner(self): + return _threading.current_thread() if self.isHeldByCurrentThread() else None + + +# ============================================================ +# ReadWriteLock +# ============================================================ + +class ReadWriteLock(Stub): + def readLock(self): + raise NotImplementedError + + def writeLock(self): + raise NotImplementedError + + +class ReentrantReadWriteLock(ReadWriteLock): + class ReadLock(Lock): + def __init__(self, rw_lock): + super().__init__() + self._rw_lock = rw_lock + + def lock(self): + self._rw_lock._read_lock.acquire() + + def unlock(self): + self._rw_lock._read_lock.release() + + def tryLock(self, timeout=None, unit=None): + return self._rw_lock._read_lock.acquire(blocking=False) + + class WriteLock(Lock): + def __init__(self, rw_lock): + super().__init__() + self._rw_lock = rw_lock + + def lock(self): + self._rw_lock._write_lock.acquire() + + def unlock(self): + self._rw_lock._write_lock.release() + + def tryLock(self, timeout=None, unit=None): + return self._rw_lock._write_lock.acquire(blocking=False) + + def __init__(self, fair=False): + super().__init__() + self._read_lock_obj = _threading.RLock() + self._write_lock_obj = _threading.Lock() + self._read_lock = ReentrantReadWriteLock.ReadLock(self) + self._write_lock = ReentrantReadWriteLock.WriteLock(self) + + def readLock(self): + return self._read_lock + + def writeLock(self): + return self._write_lock + + +class Condition(Stub): + def __init__(self): + super().__init__() + self._cond = _threading.Condition() + + def await_(self, timeout=None, unit=None): + if timeout is not None and unit is not None: + seconds = unit.toMillis(timeout) / 1000.0 + with self._cond: + return self._cond.wait(timeout=seconds) + with self._cond: + self._cond.wait() + + def awaitUninterruptibly(self): + with self._cond: + self._cond.wait() + + def awaitNanos(self, nanos_timeout): + seconds = nanos_timeout / 1_000_000_000.0 + with self._cond: + self._cond.wait(timeout=seconds) + return 0 + + def awaitUntil(self, deadline): + remaining = (deadline - _time.time()) * 1000 + if remaining > 0: + self.await_(remaining, TimeUnit.MILLISECONDS) + + def signal(self): + with self._cond: + self._cond.notify() + + def signalAll(self): + with self._cond: + self._cond.notify_all() + + +class LockSupport(Stub): + @staticmethod + def park(): + _time.sleep(1) + + @staticmethod + def parkNanos(nanos): + _time.sleep(nanos / 1_000_000_000.0) + + @staticmethod + def parkUntil(deadline): + remaining = deadline - _time.time() + if remaining > 0: + _time.sleep(remaining) + + @staticmethod + def unpark(thread): + pass + + +# ============================================================ +# AbstractQueuedSynchronizer (stubs) +# ============================================================ + +class AbstractQueuedSynchronizer(Stub): + def __init__(self): + super().__init__() + self._state = 0 + + def getState(self): + return self._state + + def setState(self, state): + self._state = state + + def compareAndSetState(self, expect, update): + if self._state == expect: + self._state = update + return True + return False + + def acquire(self, arg): + pass + + def acquireInterruptibly(self, arg): + pass + + def tryAcquire(self, arg): + return True + + def tryRelease(self, arg): + return True + + def release(self, arg): + return True + + def acquireShared(self, arg): + pass + + def releaseShared(self, arg): + return True + + def tryAcquireShared(self, arg): + return 0 + + def tryReleaseShared(self, arg): + return True + + def hasQueuedThreads(self): + return False + + def getQueueLength(self): + return 0 + + +class AbstractQueuedLongSynchronizer(AbstractQueuedSynchronizer): + pass + + +# ============================================================ +# Flow / SubmissionPublisher (stubs) +# ============================================================ + +class Flow(Stub): + class Publisher(Stub): + def subscribe(self, subscriber): + pass + + class Subscriber(Stub): + def onSubscribe(self, subscription): + pass + + def onNext(self, item): + pass + + def onError(self, throwable): + pass + + def onComplete(self): + pass + + class Subscription(Stub): + def request(self, n): + pass + + def cancel(self): + pass + + class Processor(Stub): + pass + + +class SubmissionPublisher(Flow.Publisher): + def __init__(self): + super().__init__() + self._subscribers = [] + + def subscribe(self, subscriber): + self._subscribers.append(subscriber) + + def submit(self, item): + for sub in self._subscribers: + sub.onNext(item) + return 1 + + def close(self): + for sub in self._subscribers: + sub.onComplete() + + +# ============================================================ +# ForkJoinPool / ForkJoinTask (stubs) +# ============================================================ + +class ForkJoinPool(Stub): + def __init__(self, parallelism=None): + super().__init__() + self._parallelism = parallelism or _py_math.ceil(_threading._count / 2) + self._pool = _futures.ThreadPoolExecutor(max_workers=self._parallelism) + + def execute(self, task): + self._pool.submit(task.run) + + def submit(self, task): + future = FutureTask(task) + self._pool.submit(future.run) + return future + + def invoke(self, task): + return task.run() + + def shutdown(self): + self._pool.shutdown(wait=False) + + +class ForkJoinTask(Future): + def fork(self): + return self + + def join(self): + return None + + def invoke(self): + return None + + def adapt(self, callable_or_runnable): + return ForkJoinTask() + + +class RecursiveAction(ForkJoinTask): + def compute(self): + raise NotImplementedError + + def run(self): + self.compute() + + +class RecursiveTask(ForkJoinTask): + def compute(self): + raise NotImplementedError + + def run(self): + self.compute() + + +class ForkJoinWorkerThread(Stub): + def __init__(self, pool): + super().__init__() + self._pool = pool + + def getPool(self): + return self._pool \ No newline at end of file diff --git a/android_pyapi/java/util_regex.py b/android_pyapi/java/util_regex.py new file mode 100644 index 0000000..53e9b7b --- /dev/null +++ b/android_pyapi/java/util_regex.py @@ -0,0 +1,287 @@ +""" +Java regex standard library APIs implemented in Python. +java.util.regex package - Pattern, Matcher, MatchResult, PatternSyntaxException. +""" + +from .._core import Stub +from .io import IOException +from .lang import RuntimeException +import re as _re + +IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) +IllegalStateException = type("IllegalStateException", (RuntimeException,), {}) + +__all__ = [ + "Pattern", "Matcher", "MatchResult", "PatternSyntaxException", +] + + +# ============================================================ +# PatternSyntaxException +# ============================================================ + +class PatternSyntaxException(IllegalArgumentException): + def __init__(self, desc, regex, index): + super().__init__(f"{desc}: {regex} at index {index}") + self._desc = desc + self._regex = regex + self._index = index + + def getDescription(self): + return self._desc + + def getPattern(self): + return self._regex + + def getIndex(self): + return self._index + + def getMessage(self): + return f"{self._desc} near index {self._index}: {self._regex}" + + +# ============================================================ +# MatchResult +# ============================================================ + +class MatchResult(Stub): + def start(self, group=None): + raise NotImplementedError + + def end(self, group=None): + raise NotImplementedError + + def group(self, group=None): + raise NotImplementedError + + def groupCount(self): + raise NotImplementedError + + +# ============================================================ +# Matcher +# ============================================================ + +class Matcher(MatchResult): + def __init__(self, pattern, input_str): + super().__init__() + self._pattern = pattern + self._input = input_str + self._re_pattern = pattern._pattern + self._match = None + self._region_start = 0 + self._region_end = len(input_str) + self._transparent_bounds = False + self._anchoring_bounds = True + self._last_match = None + + def matches(self): + self._match = self._re_pattern.fullmatch(self._input, self._region_start, self._region_end) + self._last_match = self._match + return self._match is not None + + def find(self, start=None): + if start is not None: + self._match = self._re_pattern.search(self._input, start, self._region_end) + else: + if self._match is None: + self._match = self._re_pattern.search(self._input, self._region_start, self._region_end) + else: + pos = self._match.end() + self._match = self._re_pattern.search(self._input, pos, self._region_end) + if self._match: + self._last_match = self._match + return self._match is not None + + def lookingAt(self): + self._match = self._re_pattern.match(self._input, self._region_start, self._region_end) + self._last_match = self._match + return self._match is not None + + def start(self, group=0): + if self._match is None: + raise IllegalStateException("No match available") + if group == 0: + return self._match.start() + return self._match.start(group) + + def end(self, group=0): + if self._match is None: + raise IllegalStateException("No match available") + if group == 0: + return self._match.end() + return self._match.end(group) + + def group(self, group=0): + if self._match is None: + raise IllegalStateException("No match available") + if isinstance(group, str): + return self._match.group(group) + return self._match.group(group) + + def groupCount(self): + return self._re_pattern.groups + + def replaceAll(self, replacement): + return self._re_pattern.sub(replacement, self._input) + + def replaceFirst(self, replacement): + return self._re_pattern.sub(replacement, self._input, count=1) + + def appendReplacement(self, sb, replacement): + if self._match is None: + raise IllegalStateException("No match available") + self._append_pos = getattr(self, '_append_pos', 0) + sb.append(self._input[self._append_pos:self._match.start()]) + sb.append(self._re_pattern.sub(replacement, self._match.group(), count=1)) + self._append_pos = self._match.end() + return self + + def appendTail(self, sb): + self._append_pos = getattr(self, '_append_pos', 0) + sb.append(self._input[self._append_pos:]) + return sb + + def reset(self, input_str=None): + if input_str is not None: + self._input = input_str + self._region_end = len(input_str) + self._match = None + self._last_match = None + return self + + def region(self, start, end): + self._region_start = start + self._region_end = end + return self + + def regionStart(self): + return self._region_start + + def regionEnd(self): + return self._region_end + + def hasTransparentBounds(self): + return self._transparent_bounds + + def hasAnchoringBounds(self): + return self._anchoring_bounds + + def useAnchoringBounds(self, value): + self._anchoring_bounds = value + return self + + def useTransparentBounds(self, value): + self._transparent_bounds = value + return self + + def hitEnd(self): + return self._match is not None and self._match.end() >= self._region_end + + def requireEnd(self): + return self._match is not None and not self.hitEnd() + + def toMatchResult(self): + result = Matcher(self._pattern, self._input) + result._match = self._match + result._last_match = self._last_match + return result + + def pattern(self): + return self._pattern + + def toString(self): + return f"Matcher[pattern={self._pattern.pattern()}]" + + def namedGroups(self): + return self._re_pattern.groupindex + + +class IllegalStateException(RuntimeException): + pass + + +# ============================================================ +# Pattern +# ============================================================ + +class Pattern(Stub): + CANON_EQ = 128 + CASE_INSENSITIVE = 2 + COMMENTS = 4 + DOTALL = 32 + LITERAL = 16 + MULTILINE = 8 + UNICODE_CASE = 64 + UNICODE_CHARACTER_CLASS = 256 + UNIX_LINES = 1 + + _re_flags_map = { + 0: 0, # default + 2: _re.IGNORECASE, + 8: _re.MULTILINE, + 32: _re.DOTALL, + 64: _re.UNICODE, + 4: _re.VERBOSE, + } + + def __init__(self, regex, flags=0): + super().__init__() + self._regex = str(regex) + self._flags = flags + re_flags = 0 + for java_flag, py_flag in self._re_flags_map.items(): + if flags & java_flag: + re_flags |= py_flag + try: + self._pattern = _re.compile(self._regex, re_flags) + except _re.error as e: + raise PatternSyntaxException(str(e), self._regex, e.pos if hasattr(e, 'pos') else 0) + + @staticmethod + def compile(regex, flags=0): + return Pattern(regex, flags) + + def matcher(self, input_str): + return Matcher(self, input_str) + + def split(self, input_str, limit=0): + if limit <= 0: + parts = self._pattern.split(str(input_str)) + if limit == 0: + while parts and parts[-1] == "": + parts.pop() + else: + parts = self._pattern.split(str(input_str), maxsplit=limit - 1) + while len(parts) < limit: + parts.append("") + return parts + + def splitAsStream(self, input_str): + return iter(self.split(input_str)) + + @staticmethod + def matches(regex, input_str): + return bool(_re.fullmatch(regex, str(input_str))) + + @staticmethod + def quote(s): + return _re.escape(str(s)) + + def flags(self): + return self._flags + + def pattern(self): + return self._regex + + def toString(self): + return self._regex + + def predicate(self): + return lambda s: bool(self._pattern.fullmatch(str(s))) + + def asPredicate(self): + return self.predicate() + + def asMatchPredicate(self): + return lambda s: bool(self._pattern.fullmatch(str(s))) \ No newline at end of file diff --git a/android_pyapi/java/util_zip.py b/android_pyapi/java/util_zip.py new file mode 100644 index 0000000..034616c --- /dev/null +++ b/android_pyapi/java/util_zip.py @@ -0,0 +1,697 @@ +""" +Java zip standard library APIs implemented in Python. +java.util.zip package - ZipFile, ZipEntry, GZIP, CRC, Deflater, Inflater. +""" + +from .._core import Stub +from .io import IOException, Closeable, Flushable +import zipfile as _zipfile +import gzip as _gzip +import zlib as _zlib +import io as _pyio +import os as _os +from datetime import datetime as _datetime + +__all__ = [ + "ZipFile", "ZipEntry", "ZipOutputStream", "ZipInputStream", + "GZIPInputStream", "GZIPOutputStream", + "CRC32", "CheckedInputStream", "CheckedOutputStream", + "Adler32", + "DataFormatException", "ZipException", + "Deflater", "Inflater", + "DeflaterOutputStream", "InflaterInputStream", +] + + +# ============================================================ +# Exception classes +# ============================================================ + +class ZipException(IOException): + pass + +class DataFormatException(Exception): + pass + + +# ============================================================ +# CRC32 +# ============================================================ + +class CRC32(Stub): + def __init__(self): + super().__init__() + self._value = 0 + + def getValue(self): + return self._value + + def reset(self): + self._value = 0 + + def update(self, b, off=None, length=None): + if isinstance(b, int): + self._value = _zlib.crc32(bytes([b & 0xFF]), self._value) & 0xFFFFFFFF + elif isinstance(b, (bytes, bytearray)): + if off is not None and length is not None: + self._value = _zlib.crc32(bytes(b[off:off+length]), self._value) & 0xFFFFFFFF + else: + self._value = _zlib.crc32(bytes(b), self._value) & 0xFFFFFFFF + + +# ============================================================ +# Adler32 +# ============================================================ + +class Adler32(Stub): + def __init__(self): + super().__init__() + self._value = 1 + + def getValue(self): + return self._value + + def reset(self): + self._value = 1 + + def update(self, b, off=None, length=None): + if isinstance(b, int): + self._value = _zlib.adler32(bytes([b & 0xFF]), self._value) & 0xFFFFFFFF + elif isinstance(b, (bytes, bytearray)): + if off is not None and length is not None: + self._value = _zlib.adler32(bytes(b[off:off+length]), self._value) & 0xFFFFFFFF + else: + self._value = _zlib.adler32(bytes(b), self._value) & 0xFFFFFFFF + + +# ============================================================ +# CheckedInputStream / CheckedOutputStream +# ============================================================ + +class CheckedInputStream(Closeable): + def __init__(self, inp, cksum): + super().__init__() + self._in = inp + self._cksum = cksum + + def read(self, b=None, off=None, length=None): + if b is None: + result = self._in.read() + if result >= 0: + self._cksum.update(result) + return result + if off is None: + off = 0 + if length is None: + length = len(b) + result = self._in.read(b, off, length) + if result > 0: + self._cksum.update(b[off:off+result]) + return result + + def getChecksum(self): + return self._cksum + + def close(self): + self._in.close() + + def skip(self, n): + return self._in.skip(n) + + +class CheckedOutputStream(Closeable, Flushable): + def __init__(self, out, cksum): + super().__init__() + self._out = out + self._cksum = cksum + + def write(self, b, off=None, length=None): + if isinstance(b, int): + self._out.write(b) + self._cksum.update(b) + else: + if off is not None and length is not None: + data = bytes(b[off:off+length]) + else: + data = bytes(b) + self._out.write(data) + self._cksum.update(data) + + def getChecksum(self): + return self._cksum + + def flush(self): + self._out.flush() + + def close(self): + self._out.close() + + +# ============================================================ +# ZipEntry +# ============================================================ + +class ZipEntry(Stub): + STORED = 0 + DEFLATED = 8 + + def __init__(self, name): + super().__init__() + self._name = name + self._method = ZipEntry.DEFLATED + self._size = 0 + self._compressed_size = 0 + self._crc = 0 + self._comment = "" + self._time = 0 + self._extra = b"" + self._is_directory = name.endswith("/") + + def getName(self): + return self._name + + def getMethod(self): + return self._method + + def setMethod(self, method): + self._method = method + + def getSize(self): + return self._size + + def setSize(self, size): + self._size = size + + def getCompressedSize(self): + return self._compressed_size + + def setCompressedSize(self, size): + self._compressed_size = size + + def getCrc(self): + return self._crc + + def setCrc(self, crc): + self._crc = crc + + def getComment(self): + return self._comment + + def setComment(self, comment): + self._comment = comment + + def getTime(self): + return self._time + + def setTime(self, time): + self._time = time + + def getExtra(self): + return self._extra + + def setExtra(self, extra): + self._extra = extra + + def isDirectory(self): + return self._is_directory + + def toString(self): + return self._name + + def __str__(self): + return self._name + + +# ============================================================ +# ZipFile +# ============================================================ + +class ZipFile(Closeable): + OPEN_READ = 1 + OPEN_DELETE = 2 + + def __init__(self, file, mode="r"): + super().__init__() + if isinstance(file, str): + path = file + else: + path = file.getPath() + self._path = path + self._mode = mode + try: + self._zfile = _zipfile.ZipFile(path, mode) + except Exception as e: + raise ZipException(str(e)) + + def entries(self): + return iter(self._zfile.infolist()) + + def getEntry(self, name): + try: + info = self._zfile.getinfo(name) + entry = ZipEntry(info.filename) + entry.setMethod(info.compress_type) + entry.setSize(info.file_size) + entry.setCompressedSize(info.compress_size) + entry.setCrc(info.CRC) + entry.setTime(int(_datetime(*info.date_time).timestamp() * 1000) if info.date_time else 0) + return entry + except KeyError: + return None + + def getInputStream(self, entry): + try: + data = self._zfile.read(entry.getName() if isinstance(entry, ZipEntry) else entry) + from ..io import ByteArrayInputStream + return ByteArrayInputStream(data) + except Exception as e: + raise ZipException(str(e)) + + def close(self): + self._zfile.close() + + def size(self): + return len(self._zfile.namelist()) + + def getName(self): + return self._path + + def getComment(self): + return "" + + +# ============================================================ +# ZipOutputStream +# ============================================================ + +class ZipOutputStream(Closeable, Flushable): + STORED = 0 + DEFLATED = 8 + + def __init__(self, out): + super().__init__() + self._out = out + self._zfile = _zipfile.ZipFile(out, "w", _zipfile.ZIP_DEFLATED) + self._method = ZipOutputStream.DEFLATED + self._level = -1 + self._comment = "" + + def putNextEntry(self, entry): + zi = _zipfile.ZipInfo(entry.getName() if isinstance(entry, ZipEntry) else str(entry)) + if hasattr(entry, 'getTime') and entry.getTime(): + dt = _datetime.fromtimestamp(entry.getTime() / 1000.0) + zi.date_time = dt.timetuple()[:6] + self._zfile.writestr(zi, b"") + + def closeEntry(self): + pass + + def write(self, b, off=None, length=None): + if off is not None and length is not None: + self._zfile._zout.write(bytes(b[off:off+length])) + else: + self._zfile._zout.write(bytes(b) if not isinstance(b, (bytes, bytearray)) else b) + + def finish(self): + self._zfile.close() + + def close(self): + self._zfile.close() + + def setMethod(self, method): + self._method = method + + def setLevel(self, level): + self._level = level + + def setComment(self, comment): + self._comment = comment + + def flush(self): + self._zfile._zout.flush() if hasattr(self._zfile._zout, 'flush') else None + + +# ============================================================ +# ZipInputStream +# ============================================================ + +class ZipInputStream(Closeable): + def __init__(self, inp): + super().__init__() + self._inp = inp + self._zfile = _zipfile.ZipFile(inp) + self._entries = iter(self._zfile.infolist()) + self._current_entry = None + self._current_data = b"" + self._current_pos = 0 + + def getNextEntry(self): + try: + info = next(self._entries) + entry = ZipEntry(info.filename) + entry.setMethod(info.compress_type) + entry.setSize(info.file_size) + entry.setCompressedSize(info.compress_size) + entry.setCrc(info.CRC) + self._current_entry = info + self._current_data = self._zfile.read(info) + self._current_pos = 0 + return entry + except StopIteration: + self._current_entry = None + self._current_data = b"" + return None + + def read(self, b=None, off=None, length=None): + if self._current_entry is None: + return -1 + avail = len(self._current_data) - self._current_pos + if avail <= 0: + return -1 + if b is None: + val = self._current_data[self._current_pos] + self._current_pos += 1 + return val + if off is None: + off = 0 + if length is None: + length = len(b) + n = min(length, avail) + if isinstance(b, bytearray): + b[off:off+n] = self._current_data[self._current_pos:self._current_pos+n] + self._current_pos += n + return n + + def closeEntry(self): + self._current_entry = None + self._current_data = b"" + + def available(self): + if self._current_entry is None: + return 0 + return len(self._current_data) - self._current_pos + + def close(self): + self._zfile.close() + + +# ============================================================ +# GZIPInputStream +# ============================================================ + +class GZIPInputStream(Closeable): + def __init__(self, inp, size=512): + super().__init__() + self._inp = inp + self._gzip_file = _gzip.GzipFile(fileobj=_pyio.BufferedReader(inp) if hasattr(inp, 'read') else inp) + + def read(self, b=None, off=None, length=None): + if b is None: + data = self._gzip_file.read(1) + return data[0] if data else -1 + if off is None: + off = 0 + if length is None: + length = len(b) + data = self._gzip_file.read(length) + if not data: + return -1 + n = len(data) + if isinstance(b, bytearray): + b[off:off+n] = data + else: + ba = bytearray(b) + ba[off:off+n] = data + b[:] = bytes(ba) + return n + + def close(self): + self._gzip_file.close() + + +# ============================================================ +# GZIPOutputStream +# ============================================================ + +class GZIPOutputStream(Closeable, Flushable): + def __init__(self, out, size=512): + super().__init__() + self._out = out + self._gzip_file = _gzip.GzipFile(fileobj=out, mode="wb") + + def write(self, b, off=None, length=None): + if isinstance(b, int): + self._gzip_file.write(bytes([b & 0xFF])) + elif isinstance(b, (bytes, bytearray)): + if off is not None and length is not None: + self._gzip_file.write(bytes(b[off:off+length])) + else: + self._gzip_file.write(bytes(b)) + + def finish(self): + self._gzip_file.close() + + def close(self): + self._gzip_file.close() + + def flush(self): + self._gzip_file.flush() + + +# ============================================================ +# Deflater +# ============================================================ + +class Deflater(Stub): + BEST_COMPRESSION = 9 + BEST_SPEED = 1 + DEFAULT_COMPRESSION = -1 + DEFAULT_STRATEGY = 0 + FILTERED = 1 + HUFFMAN_ONLY = 2 + NO_COMPRESSION = 0 + NO_FLUSH = 0 + SYNC_FLUSH = 2 + FULL_FLUSH = 3 + + def __init__(self, level=-1, nowrap=False): + super().__init__() + self._level = level + self._nowrap = nowrap + self._compress = _zlib.compressobj(level=level if level >= 0 else 6, + method=_zlib.DEFLATED, + wbits=-_zlib.MAX_WBITS if nowrap else _zlib.MAX_WBITS) + self._input_buffer = b"" + self._finished = False + + def setInput(self, data, off=0, length=-1): + if length < 0: + length = len(data) - off + self._input_buffer = bytes(data[off:off+length]) + + def needsInput(self): + return len(self._input_buffer) == 0 + + def setDictionary(self, dictionary): + pass + + def getTotalIn(self): + return len(self._input_buffer) + + def getTotalOut(self): + return 0 + + def deflate(self, output, off=0, length=-1): + if length < 0: + length = len(output) - off + data = self._compress.compress(self._input_buffer) + self._input_buffer = b"" + n = min(len(data), length) + if isinstance(output, bytearray): + output[off:off+n] = data[:n] + else: + ba = bytearray(output) + ba[off:off+n] = data[:n] + output[:] = bytes(ba) + return n + + def finish(self): + self._finished = True + + def finished(self): + return self._finished + + def end(self): + pass + + def getAdler(self): + return 1 + + def reset(self): + self._compress = _zlib.compressobj(level=self._level if self._level >= 0 else 6, + method=_zlib.DEFLATED, + wbits=-_zlib.MAX_WBITS if self._nowrap else _zlib.MAX_WBITS) + self._input_buffer = b"" + self._finished = False + + +# ============================================================ +# Inflater +# ============================================================ + +class Inflater(Stub): + def __init__(self, nowrap=False): + super().__init__() + self._nowrap = nowrap + wbits = -_zlib.MAX_WBITS if nowrap else _zlib.MAX_WBITS + self._decompress = _zlib.decompressobj(wbits) + self._input_buffer = b"" + self._finished = False + + def setInput(self, data, off=0, length=-1): + if length < 0: + length = len(data) - off + self._input_buffer = bytes(data[off:off+length]) + + def needsInput(self): + return len(self._input_buffer) == 0 + + def setDictionary(self, dictionary): + pass + + def getTotalIn(self): + return len(self._input_buffer) + + def getTotalOut(self): + return 0 + + def inflate(self, output, off=0, length=-1): + if length < 0: + length = len(output) - off + try: + data = self._decompress.decompress(self._input_buffer) + self._input_buffer = b"" + n = min(len(data), length) + if isinstance(output, bytearray): + output[off:off+n] = data[:n] + else: + ba = bytearray(output) + ba[off:off+n] = data[:n] + output[:] = bytes(ba) + return n + except _zlib.error as e: + raise DataFormatException(str(e)) + + def finished(self): + return self._finished + + def end(self): + pass + + def getAdler(self): + return 1 + + def needsDictionary(self): + return False + + def reset(self): + wbits = -_zlib.MAX_WBITS if self._nowrap else _zlib.MAX_WBITS + self._decompress = _zlib.decompressobj(wbits) + self._input_buffer = b"" + self._finished = False + + +# ============================================================ +# DeflaterOutputStream +# ============================================================ + +class DeflaterOutputStream(Closeable, Flushable): + def __init__(self, out, deflater=None, size=512): + super().__init__() + self._out = out + self._deflater = deflater or Deflater() + self._buffer = bytearray(size) + + def write(self, b, off=None, length=None): + if isinstance(b, int): + self._out.write(bytes([b & 0xFF])) + else: + if off is not None and length is not None: + self._out.write(bytes(b[off:off+length])) + else: + self._deflater.setInput(bytes(b) if isinstance(b, (bytes, bytearray)) else bytes(b)) + while not self._deflater.needsInput(): + n = self._deflater.deflate(self._buffer) + if n > 0: + self._out.write(bytes(self._buffer[:n])) + + def finish(self): + self._deflater.finish() + while not self._deflater.finished(): + n = self._deflater.deflate(self._buffer) + if n > 0: + self._out.write(bytes(self._buffer[:n])) + + def close(self): + self.finish() + self._deflater.end() + self._out.close() + + def flush(self): + self._out.flush() + + +# ============================================================ +# InflaterInputStream +# ============================================================ + +class InflaterInputStream(Closeable): + def __init__(self, inp, inflater=None, size=512): + super().__init__() + self._in = inp + self._inflater = inflater or Inflater() + self._buffer = bytearray(size) + + def read(self, b=None, off=None, length=None): + if b is None: + buf = bytearray(1) + n = self._read_from_inflater(buf, 0, 1) + return buf[0] if n > 0 else -1 + if off is None: + off = 0 + if length is None: + length = len(b) + return self._read_from_inflater(b, off, length) + + def _read_from_inflater(self, b, off, length): + while not self._inflater.finished(): + if self._inflater.needsInput(): + data = self._in.read(self._buffer) + if data is None or data == -1 or data == 0: + break + if isinstance(data, int): + break + self._inflater.setInput(data) + try: + n = self._inflater.inflate(b, off, length) + if n > 0: + return n + except DataFormatException: + break + return -1 + + def available(self): + return 0 + + def close(self): + self._inflater.end() + self._in.close() + + def mark(self, readlimit): + pass + + def reset(self): + raise IOException("mark/reset not supported") + + def markSupported(self): + return False \ No newline at end of file diff --git a/android_pyapi/org/__init__.py b/android_pyapi/org/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/android_pyapi/org/__pycache__/__init__.cpython-310.pyc b/android_pyapi/org/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..0e9bda8 Binary files /dev/null and b/android_pyapi/org/__pycache__/__init__.cpython-310.pyc differ diff --git a/android_pyapi/org/__pycache__/json.cpython-310.pyc b/android_pyapi/org/__pycache__/json.cpython-310.pyc new file mode 100644 index 0000000..608f88b Binary files /dev/null and b/android_pyapi/org/__pycache__/json.cpython-310.pyc differ diff --git a/android_pyapi/org/json.py b/android_pyapi/org/json.py new file mode 100644 index 0000000..140a80c --- /dev/null +++ b/android_pyapi/org/json.py @@ -0,0 +1,858 @@ +""" +Java JSON standard library APIs implemented in Python. +org.json package - JSONObject, JSONArray, JSONTokener, JSONStringer, JSONWriter, etc. +""" + +from .._core import Stub +import json as _json + +__all__ = [ + "JSONObject", "JSONArray", "JSONTokener", + "JSONStringer", "JSONWriter", "JSONException", "JSONPointer", + "JSONPointerBuilder", +] + + +# ============================================================ +# JSONException +# ============================================================ + +class JSONException(Exception): + def __init__(self, *args): + if len(args) == 1 and isinstance(args[0], Exception): + super().__init__(str(args[0])) + self._cause = args[0] + else: + super().__init__(*args) + self._cause = None + + def getCause(self): + return self._cause + + +# ============================================================ +# JSONObject.NULL +# ============================================================ + +class _JSONNull(Stub): + def __init__(self): + super().__init__() + + def equals(self, other): + return other is None or isinstance(other, _JSONNull) + + def hashCode(self): + return 0 + + def toString(self): + return "null" + + +# ============================================================ +# JSONObject +# ============================================================ + +class JSONObject(Stub): + NULL = _JSONNull() + + def __init__(self, *args): + super().__init__() + self._map = {} + if len(args) == 1: + arg = args[0] + if isinstance(arg, str): + tokener = JSONTokener(arg) + obj = tokener.nextValue() + if isinstance(obj, JSONObject): + self._map = obj._map + else: + raise JSONException("String is not a JSONObject") + elif isinstance(arg, dict): + for k, v in arg.items(): + self._map[str(k)] = JSONObject._wrap(v) + elif isinstance(arg, JSONObject): + names = None + self._map = dict(arg._map) + + def _wrap(val): + if val is None: + return JSONObject.NULL + if isinstance(val, (bool, int, float)): + return val + if isinstance(val, str): + return val + if isinstance(val, (list, tuple)): + arr = JSONArray() + for item in val: + arr.put(JSONObject._wrap(item)) + return arr + if isinstance(val, dict): + obj = JSONObject() + for k, v in val.items(): + obj.put(str(k), JSONObject._wrap(v)) + return obj + if isinstance(val, JSONObject) or isinstance(val, JSONArray): + return val + return val + + def put(self, key, value): + self._map[str(key)] = JSONObject._wrap(value) + return self + + def accumulate(self, key, value): + k = str(key) + current = self._map.get(k) + if current is None: + arr = JSONArray() + arr.put(value) + self._map[k] = arr + elif isinstance(current, JSONArray): + current.put(value) + else: + arr = JSONArray() + arr.put(current) + arr.put(value) + self._map[k] = arr + return self + + def get(self, key): + k = str(key) + if k not in self._map: + raise JSONException(f"JSONObject[\"{k}\"] not found") + val = self._map[k] + if val is JSONObject.NULL: + raise JSONException(f"JSONObject[\"{k}\"] is null") + return val + + def opt(self, key, default=None): + k = str(key) + val = self._map.get(k) + if val is None or val is JSONObject.NULL: + return default + return val + + def getString(self, key): + val = self.get(key) + if isinstance(val, str): + return val + raise JSONException(f"JSONObject[\"{key}\"] is not a string") + + def getInt(self, key): + val = self.get(key) + if isinstance(val, int): + return val + if isinstance(val, float): + return int(val) + raise JSONException(f"JSONObject[\"{key}\"] is not an int") + + def getLong(self, key): + val = self.get(key) + if isinstance(val, int): + return val + if isinstance(val, float): + return int(val) + raise JSONException(f"JSONObject[\"{key}\"] is not a long") + + def getDouble(self, key): + val = self.get(key) + if isinstance(val, (int, float)): + return float(val) + raise JSONException(f"JSONObject[\"{key}\"] is not a double") + + def getBoolean(self, key): + val = self.get(key) + if isinstance(val, bool): + return val + if val == "true": + return True + if val == "false": + return False + raise JSONException(f"JSONObject[\"{key}\"] is not a boolean") + + def getJSONArray(self, key): + val = self.get(key) + if isinstance(val, JSONArray): + return val + raise JSONException(f"JSONObject[\"{key}\"] is not a JSONArray") + + def getJSONObject(self, key): + val = self.get(key) + if isinstance(val, JSONObject): + return val + raise JSONException(f"JSONObject[\"{key}\"] is not a JSONObject") + + def optString(self, key, default=""): + val = self.opt(key) + if val is None or val is JSONObject.NULL: + return default + return str(val) + + def optInt(self, key, default=0): + val = self.opt(key) + if val is None or val is JSONObject.NULL: + return default + if isinstance(val, int): + return val + if isinstance(val, float): + return int(val) + try: + return int(val) + except (ValueError, TypeError): + return default + + def optLong(self, key, default=0): + return self.optInt(key, default) + + def optDouble(self, key, default=0.0): + val = self.opt(key) + if val is None or val is JSONObject.NULL: + return default + if isinstance(val, (int, float)): + return float(val) + try: + return float(val) + except (ValueError, TypeError): + return default + + def optBoolean(self, key, default=False): + val = self.opt(key) + if val is None or val is JSONObject.NULL: + return default + if isinstance(val, bool): + return val + if isinstance(val, str): + return val.lower() == "true" + return default + + def optJSONArray(self, key): + val = self.opt(key) + if isinstance(val, JSONArray): + return val + return None + + def optJSONObject(self, key): + val = self.opt(key) + if isinstance(val, JSONObject): + return val + return None + + def has(self, key): + return str(key) in self._map + + def remove(self, key): + k = str(key) + if k in self._map: + del self._map[k] + + def keys(self): + return iter(self._map.keys()) + + def names(self): + if not self._map: + return None + arr = JSONArray() + for k in self._map: + arr.put(k) + return arr + + def length(self): + return len(self._map) + + def toString(self, indent_factor=None): + if indent_factor is None: + return self._to_json_string() + return self._to_json_pretty(indent_factor) + + def _to_json_string(self): + parts = ["{"] + for i, (k, v) in enumerate(self._map.items()): + parts.append('"' + k + '": ' + JSONObject._value_to_string(v)) + if i < len(self._map) - 1: + parts.append(", ") + parts.append("}") + return "".join(parts) + + def _to_json_pretty(self, indent): + parts = ["{"] + for i, (k, v) in enumerate(self._map.items()): + parts.append("\n" + " " * indent + '"' + k + '": ' + JSONObject._value_to_string(v)) + if i < len(self._map) - 1: + parts.append(",") + parts.append("\n}") + return "".join(parts) + + def _value_to_string(val): + if val is None or val is JSONObject.NULL: + return "null" + if isinstance(val, bool): + return "true" if val else "false" + if isinstance(val, (int, float)): + return str(val) + if isinstance(val, str): + return _json.dumps(val) + if isinstance(val, JSONObject): + return val._to_json_string() + if isinstance(val, JSONArray): + return val._to_json_string() + return str(val) + + def toMap(self): + result = {} + for k, v in self._map.items(): + if isinstance(v, JSONObject): + result[k] = v.toMap() + elif isinstance(v, JSONArray): + result[k] = v.toList() + elif v is JSONObject.NULL: + result[k] = None + else: + result[k] = v + return result + + def write(self, writer): + writer._write(self._to_json_string()) + + def toJSONArray(self, names): + if names is None or names.length() == 0: + return None + arr = JSONArray() + for i in range(names.length()): + arr.put(self.opt(names.getString(i))) + return arr + + def hashCode(self): + return hash(frozenset(self._map.items())) + + def equals(self, other): + if isinstance(other, JSONObject): + return self._map == other._map + return False + + +# ============================================================ +# JSONArray +# ============================================================ + +class JSONArray(Stub): + def __init__(self, *args): + super().__init__() + self._list = [] + if len(args) == 1: + arg = args[0] + if isinstance(arg, str): + tokener = JSONTokener(arg) + val = tokener.nextValue() + if isinstance(val, JSONArray): + self._list = val._list + else: + raise JSONException("String is not a JSONArray") + elif isinstance(arg, (list, tuple)): + for item in arg: + self.put(JSONObject._wrap(item)) + + def put(self, *args): + if len(args) == 1: + self._list.append(JSONObject._wrap(args[0])) + elif len(args) == 2: + index, value = args + while len(self._list) <= index: + self._list.append(None) + self._list[index] = JSONObject._wrap(value) + return self + + def get(self, index): + if index < 0 or index >= len(self._list): + raise JSONException(f"JSONArray[{index}] not found") + val = self._list[index] + if val is JSONObject.NULL: + raise JSONException(f"JSONArray[{index}] is null") + return val + + def getString(self, index): + val = self.get(index) + if isinstance(val, str): + return val + raise JSONException(f"JSONArray[{index}] is not a string") + + def getInt(self, index): + val = self.get(index) + if isinstance(val, int): + return val + if isinstance(val, float): + return int(val) + raise JSONException(f"JSONArray[{index}] is not an int") + + def getLong(self, index): + val = self.get(index) + if isinstance(val, int): + return val + if isinstance(val, float): + return int(val) + raise JSONException(f"JSONArray[{index}] is not a long") + + def getDouble(self, index): + val = self.get(index) + if isinstance(val, (int, float)): + return float(val) + raise JSONException(f"JSONArray[{index}] is not a double") + + def getBoolean(self, index): + val = self.get(index) + if isinstance(val, bool): + return val + if val == "true": + return True + if val == "false": + return False + raise JSONException(f"JSONArray[{index}] is not a boolean") + + def getJSONArray(self, index): + val = self.get(index) + if isinstance(val, JSONArray): + return val + raise JSONException(f"JSONArray[{index}] is not a JSONArray") + + def getJSONObject(self, index): + val = self.get(index) + if isinstance(val, JSONObject): + return val + raise JSONException(f"JSONArray[{index}] is not a JSONObject") + + def opt(self, index, default=None): + if index < 0 or index >= len(self._list): + return default + val = self._list[index] + if val is JSONObject.NULL: + return default + return val + + def optString(self, index, default=""): + val = self.opt(index) + if val is None: + return default + return str(val) + + def optInt(self, index, default=0): + val = self.opt(index) + if val is None: + return default + if isinstance(val, int): + return val + if isinstance(val, float): + return int(val) + try: + return int(val) + except (ValueError, TypeError): + return default + + def optLong(self, index, default=0): + return self.optInt(index, default) + + def optDouble(self, index, default=0.0): + val = self.opt(index) + if val is None: + return default + if isinstance(val, (int, float)): + return float(val) + try: + return float(val) + except (ValueError, TypeError): + return default + + def optBoolean(self, index, default=False): + val = self.opt(index) + if val is None: + return default + if isinstance(val, bool): + return val + if isinstance(val, str): + return val.lower() == "true" + return default + + def optJSONArray(self, index): + val = self.opt(index) + if isinstance(val, JSONArray): + return val + return None + + def optJSONObject(self, index): + val = self.opt(index) + if isinstance(val, JSONObject): + return val + return None + + def remove(self, index): + if 0 <= index < len(self._list): + return self._list.pop(index) + return None + + def length(self): + return len(self._list) + + def isNull(self, index): + if 0 <= index < len(self._list): + return self._list[index] is JSONObject.NULL or self._list[index] is None + return True + + def toString(self, indent_factor=None): + if indent_factor is None: + return self._to_json_string() + return self._to_json_pretty(indent_factor) + + def _to_json_string(self): + parts = ["["] + for i, v in enumerate(self._list): + parts.append(JSONObject._value_to_string(v)) + if i < len(self._list) - 1: + parts.append(", ") + parts.append("]") + return "".join(parts) + + def _to_json_pretty(self, indent): + parts = ["["] + for i, v in enumerate(self._list): + parts.append("\n" + " " * indent + JSONObject._value_to_string(v)) + if i < len(self._list) - 1: + parts.append(",") + parts.append("\n]") + return "".join(parts) + + def toList(self): + result = [] + for v in self._list: + if isinstance(v, JSONObject): + result.append(v.toMap()) + elif isinstance(v, JSONArray): + result.append(v.toList()) + elif v is JSONObject.NULL: + result.append(None) + else: + result.append(v) + return result + + def join(self, separator): + parts = [] + for v in self._list: + parts.append(JSONObject._value_to_string(v)) + return separator.join(parts) + + +# ============================================================ +# JSONTokener +# ============================================================ + +class JSONTokener(Stub): + def __init__(self, source): + super().__init__() + if hasattr(source, "read"): + self._source = source.read() + else: + self._source = str(source) + self._index = 0 + + def nextValue(self): + self._skipWhitespace() + if self._index >= len(self._source): + raise JSONException("Unexpected end of input") + ch = self._source[self._index] + if ch == "{": + return self._readObject() + if ch == "[": + return self._readArray() + if ch == '"': + return self._readString() + if ch == "'": + return self._readString("'") + if ch == "t" or ch == "f": + return self._readLiteral() + if ch == "n": + return self._readNull() + return self._readNumber() + + def _skipWhitespace(self): + while self._index < len(self._source) and self._source[self._index] in " \t\n\r": + self._index += 1 + + def _readObject(self): + self._index += 1 # skip { + obj = JSONObject() + self._skipWhitespace() + if self._index < len(self._source) and self._source[self._index] == "}": + self._index += 1 + return obj + while True: + self._skipWhitespace() + key = self.nextValue() + self._skipWhitespace() + if self._index < len(self._source) and self._source[self._index] == ":": + self._index += 1 + self._skipWhitespace() + val = self.nextValue() + obj.put(str(key), val) + self._skipWhitespace() + if self._index < len(self._source) and self._source[self._index] == ",": + self._index += 1 + elif self._index < len(self._source) and self._source[self._index] == "}": + self._index += 1 + return obj + else: + raise JSONException("Expected ',' or '}' in object") + + def _readArray(self): + self._index += 1 # skip [ + arr = JSONArray() + self._skipWhitespace() + if self._index < len(self._source) and self._source[self._index] == "]": + self._index += 1 + return arr + while True: + self._skipWhitespace() + val = self.nextValue() + arr.put(val) + self._skipWhitespace() + if self._index < len(self._source) and self._source[self._index] == ",": + self._index += 1 + elif self._index < len(self._source) and self._source[self._index] == "]": + self._index += 1 + return arr + else: + raise JSONException("Expected ',' or ']' in array") + + def _readString(self, quote='"'): + self._index += 1 # skip quote + result = [] + while self._index < len(self._source): + ch = self._source[self._index] + if ch == "\\": + self._index += 1 + if self._index >= len(self._source): + raise JSONException("Unterminated string escape") + esc = self._source[self._index] + if esc == '"': + result.append('"') + elif esc == "\\": + result.append("\\") + elif esc == "/": + result.append("/") + elif esc == "b": + result.append("\b") + elif esc == "f": + result.append("\f") + elif esc == "n": + result.append("\n") + elif esc == "r": + result.append("\r") + elif esc == "t": + result.append("\t") + elif esc == "u": + hex_str = self._source[self._index+1:self._index+5] + result.append(chr(int(hex_str, 16))) + self._index += 4 + else: + result.append(esc) + self._index += 1 + elif ch == quote: + self._index += 1 + return "".join(result) + else: + result.append(ch) + self._index += 1 + raise JSONException("Unterminated string") + + def _readLiteral(self): + for lit in ("true", "false"): + if self._source[self._index:self._index+len(lit)] == lit: + self._index += len(lit) + return lit == "true" + raise JSONException(f"Unexpected literal at {self._index}") + + def _readNull(self): + if self._source[self._index:self._index+4] == "null": + self._index += 4 + return JSONObject.NULL + raise JSONException(f"Unexpected token at {self._index}") + + def _readNumber(self): + start = self._index + while self._index < len(self._source) and self._source[self._index] in "-+0123456789.eE": + self._index += 1 + num_str = self._source[start:self._index] + if "." in num_str or "e" in num_str or "E" in num_str: + return float(num_str) + return int(num_str) + + def nextString(self, quote='"'): + return self._readString(quote) + + def nextChar(self, c): + self._skipWhitespace() + if self._index < len(self._source) and self._source[self._index] == c: + self._index += 1 + return c + raise JSONException(f"Expected '{c}'") + + def nextTo(self, delimiter): + start = self._index + while self._index < len(self._source) and self._source[self._index] not in delimiter: + self._index += 1 + return self._source[start:self._index] + + def skipPast(self, text): + idx = self._source.find(text, self._index) + if idx >= 0: + self._index = idx + len(text) + + def skipTo(self, c): + idx = self._source.find(c, self._index) + if idx >= 0: + self._index = idx + return idx + return -1 + + def back(self): + self._index -= 1 + + def more(self): + self._skipWhitespace() + return self._index < len(self._source) + + def readString(self): + return self._readString() + + +# ============================================================ +# JSONWriter +# ============================================================ + +class JSONWriter(Stub): + def __init__(self, writer): + super().__init__() + self._writer = writer + self._stack = [] + self._in_object = False + self._need_comma = False + + def _write(self, s): + self._writer.write(s) + + def array(self): + self._beforeValue() + self._stack.append(("array", False)) + self._write("[") + return self + + def endArray(self): + self._write("]") + self._stack.pop() + return self + + def object(self): + self._beforeValue() + self._stack.append(("object", False)) + self._write("{") + return self + + def endObject(self): + self._write("}") + self._stack.pop() + return self + + def key(self, s): + if not self._stack or self._stack[-1][0] != "object": + raise JSONException("Can't set key outside object") + self._beforeKey() + self._write('"' + s + '":') + return self + + def value(self, v): + self._beforeValue() + self._write(JSONObject._value_to_string(v)) + return self + + def _beforeValue(self): + if self._stack and self._stack[-1][0] == "object": + raise JSONException("Expected key in object") + + def _beforeKey(self): + if self._stack and self._stack[-1][1]: + self._write(",") + self._stack[-1] = (self._stack[-1][0], True) + + +# ============================================================ +# JSONStringer +# ============================================================ + +class JSONStringer(JSONWriter): + def __init__(self): + from io import StringIO + super().__init__(StringIO()) + + def toString(self): + return self._writer.getvalue() + + +# ============================================================ +# JSONPointer +# ============================================================ + +class JSONPointer(Stub): + def __init__(self, pointer): + super().__init__() + if not pointer or pointer == "": + self._tokens = [] + elif pointer.startswith("#"): + pointer = pointer[1:] + pointer = pointer.replace("~1", "/").replace("~0", "~") + import urllib.parse + pointer = urllib.parse.unquote(pointer) + self._tokens = pointer.split("/")[1:] if len(pointer) > 1 else [] + else: + self._tokens = pointer.split("/")[1:] if len(pointer) > 0 else [] + + def query(self, root): + current = root + for token in self._tokens: + token = token.replace("~1", "/").replace("~0", "~") + if isinstance(current, JSONObject): + current = current.get(token) + elif isinstance(current, JSONArray): + try: + idx = int(token) + current = current.get(idx) + except (ValueError, IndexError): + raise JSONException(f"Invalid index: {token}") + else: + raise JSONException(f"Cannot index into {type(current)}") + return current + + def queryFrom(self, root): + return self.query(root) + + def toString(self): + return "#/" + "/".join(t.replace("~", "~0").replace("/", "~1") for t in self._tokens) + + def hashCode(self): + return hash(tuple(self._tokens)) + + def equals(self, other): + if isinstance(other, JSONPointer): + return self._tokens == other._tokens + return False + + +# ============================================================ +# JSONPointerBuilder +# ============================================================ + +class JSONPointerBuilder(Stub): + def __init__(self): + super().__init__() + self._tokens = [] + + def append(self, token): + self._tokens.append(str(token)) + return self + + def build(self): + return JSONPointer("#/" + "/".join(t.replace("~", "~0").replace("/", "~1") for t in self._tokens)) \ No newline at end of file