diff --git a/android_compat.py b/android_compat.py new file mode 100644 index 0000000..acd3d6f --- /dev/null +++ b/android_compat.py @@ -0,0 +1,3552 @@ +""" +Android API → Python 跨平台兼容层 +==================================== +将 Android 特有的 Java/Kotlin API 全面翻译为 Python 实现, +使 Operit 脚本可无需修改地在 Windows/macOS/Linux 上运行。 + +覆盖的 Android API 领域: + - android.content (Intent, Context, ContentResolver, ContentValues) + - android.net (Uri) + - android.os (Environment, Build, PowerManager, VibrationEffect) + - android.app (ActivityManager, NotificationManager, AlarmManager) + - android.graphics (Bitmap, Canvas, Color, Paint, Rect) + - android.graphics.pdf (PdfRenderer) + - android.provider (Settings, MediaStore, ContactsContract, CalendarContract) + - android.view (View, ViewGroup, accessibility/UIAutomation) + - android.bluetooth (BluetoothAdapter, BLE) + - android.location (LocationManager, Geocoder) + - android.telephony (TelephonyManager) + - android.hardware (Camera, SensorManager) + - android.media (MediaPlayer, AudioRecord, TtsEngine) + - android.widget (Toast) + - android.print (PrintManager) + - android.accounts (AccountManager) + - android.hardware.display (DisplayManager) + - java.io (File, FileInputStream/OutputStream, BufferedReader) + - android.hardware.biometrics (BiometricPrompt) +""" + +import os +import sys +import re +import io +import json +import time +import math +import random +import base64 +import hashlib +import struct +import shutil +import tempfile +import subprocess +import platform +import webbrowser +import urllib.parse +import urllib.request +import socket +import xml.etree.ElementTree as ET +from pathlib import Path +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional, Tuple, Union, Callable +from collections import OrderedDict + +try: + from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageChops + HAS_PIL = True +except ImportError: + HAS_PIL = False + +try: + import psutil + HAS_PSUTIL = True +except ImportError: + HAS_PSUTIL = False + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +try: + import pyautogui + HAS_PYAUTOGUI = True +except ImportError: + HAS_PYAUTOGUI = False + +try: + import screeninfo + HAS_SCREENINFO = True +except ImportError: + HAS_SCREENINFO = False + +# ============================================================ +# GLOBAL STATE - 模拟 Android Application 上下文 +# ============================================================ + +_sdcard_root = os.path.join(os.path.expanduser("~"), "Android-compat", "sdcard") +_app_data_dir = os.path.join(os.path.expanduser("~"), "Android-compat", "app-data") +_download_dir = os.path.join(os.path.expanduser("~"), "Downloads") +_package_name = "com.ai.assistance.operit" +_app_version = "1.0.0" + +def _ensure_sdcard(): + """确保模拟的 sdcard 目录结构存在""" + for d in [_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, "Android", "data"), + os.path.join(_sdcard_root, "Android", "data", _package_name, "files"), + os.path.join(_sdcard_root, "Android", "data", _package_name, "files", "packages"), + os.path.join(_sdcard_root, "Android", "data", _package_name, "js_temp"), + _app_data_dir]: + os.makedirs(d, exist_ok=True) + +_ensure_sdcard() + + +# ============================================================ +# android.net.Uri +# ============================================================ + +class Uri: + """模拟 android.net.Uri""" + + @staticmethod + def parse(uri_string: str) -> "Uri": + return Uri(uri_string) + + @staticmethod + def fromFile(path: str) -> "Uri": + return Uri(f"file://{path}") + + @staticmethod + def withAppendedPath(base: "Uri", segment: str) -> "Uri": + return Uri(f"{base._uri}/{segment}") + + def __init__(self, uri_string: str): + self._uri = uri_string + self._parsed = urllib.parse.urlparse(uri_string) + + def getScheme(self) -> str: + """content, file, http, https""" + return self._parsed.scheme or "" + + def getPath(self) -> str: + p = self._parsed.path + if p.startswith("/sdcard"): + return _sdcard_root + p + return p + + def getLastPathSegment(self) -> str: + return os.path.basename(self._parsed.path) or "" + + def toString(self) -> str: + return self._uri + + def __str__(self): + return self._uri + + +# ============================================================ +# android.content.ContentValues +# ============================================================ + +class ContentValues: + """模拟 android.content.ContentValues""" + + def __init__(self): + self._values: Dict[str, Any] = {} + + def put(self, key: str, value: Any) -> None: + self._values[key] = value + + def get(self, key: str) -> 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 getAsLong(self, key: str) -> Optional[int]: + v = self._values.get(key) + return int(v) if v is not None else None + + def remove(self, key: str) -> None: + self._values.pop(key, None) + + def containsKey(self, key: str) -> bool: + return key in self._values + + def valueSet(self) -> Dict[str, Any]: + return dict(self._values) + + def size(self) -> int: + return len(self._values) + + def clear(self) -> None: + self._values.clear() + + +# ============================================================ +# android.content.ContentResolver +# ============================================================ + +class ContentResolver: + """模拟 android.content.ContentResolver""" + + def __init__(self, context: "Context" = None): + self._context = context + + def query(self, uri: Uri, projection: List[str] = None, + selection: str = None, selectionArgs: List[str] = None, + sortOrder: str = None) -> "Cursor": + """模拟 ContentResolver.query - 有限实现""" + cursor = Cursor() + scheme = uri.getScheme() + + # file:// URI → 读文件 + if scheme == "file": + path = uri.getPath() + if os.path.isfile(path): + try: + with open(path, "r", encoding="utf-8") as f: + cursor._data = [{"_data": path, "_size": os.path.getsize(path)}] + except Exception: + pass + + # content:// URI → 模拟 + path = uri._parsed.path + if "calendar" in path: + cursor._data = [{ + "_id": "1", "title": "Calendar Event", + "dtstart": str(int(time.time()) * 1000), + "dtend": str(int(time.time() + 3600) * 1000) + }] + elif "contacts" in path: + cursor._data = [{"_id": "1", "display_name": "Test Contact", "has_phone_number": "1"}] + elif "media" in path or "images" in path or "downloads" in path: + cursor._data = self._scan_media() + elif "settings" in path: + cursor._data = [{"name": "default", "value": ""}] + + return cursor + + def _scan_media(self) -> List[Dict]: + """扫描本地文件系统中的媒体文件""" + results = [] + media_dirs = [ + os.path.expanduser("~/Pictures"), + os.path.expanduser("~/Downloads"), + os.path.join(_sdcard_root, "DCIM"), + os.path.join(_sdcard_root, "Download"), + ] + for d in media_dirs: + if not os.path.isdir(d): + continue + for root, dirs, files in os.walk(d): + for f in files[:50]: # limit + ext = os.path.splitext(f)[1].lower() + if ext in (".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mp3"): + results.append({ + "_data": os.path.join(root, f), + "_size": os.path.getsize(os.path.join(root, f)), + "title": os.path.splitext(f)[0], + "mime_type": f"image/{ext[1:]}" if ext in (".jpg", ".jpeg", ".png") else "video/mp4" + }) + return results + + def insert(self, uri: Uri, values: ContentValues) -> Uri: + """模拟插入""" + return Uri(f"content://media/external/images/media/{int(time.time())}") + + def update(self, uri: Uri, values: ContentValues, where: str = None, + selectionArgs: List[str] = None) -> int: + """模拟更新""" + return 1 + + def delete(self, uri: Uri, where: str = None, selectionArgs: List[str] = None) -> int: + return 0 + + def openInputStream(self, uri: Uri) -> io.BytesIO: + path = uri.getPath() + if os.path.isfile(path): + with open(path, "rb") as f: + return io.BytesIO(f.read()) + return io.BytesIO() + + def openOutputStream(self, uri: Uri) -> io.BytesIO: + return io.BytesIO() + + def getType(self, uri: Uri) -> str: + path = uri.getPath() + ext = os.path.splitext(path)[1].lower() + if ext == ".jpg" or ext == ".jpeg": return "image/jpeg" + if ext == ".png": return "image/png" + if ext == ".mp4": return "video/mp4" + if ext == ".mp3": return "audio/mpeg" + return "application/octet-stream" + + +class Cursor: + """模拟 android.database.Cursor""" + + def __init__(self): + self._data: List[Dict] = [] + self._pos = -1 + + def moveToFirst(self) -> bool: + if self._data: + self._pos = 0 + return True + return False + + def moveToNext(self) -> bool: + if self._pos + 1 < len(self._data): + self._pos += 1 + return True + return False + + def moveToPosition(self, pos: int) -> bool: + if 0 <= pos < len(self._data): + self._pos = pos + return True + return False + + def getCount(self) -> int: + return len(self._data) + + def getColumnIndex(self, columnName: str) -> int: + if self._pos >= 0 and self._pos < len(self._data): + keys = list(self._data[self._pos].keys()) + return keys.index(columnName) if columnName in keys else -1 + return -1 + + def getString(self, columnIndex: int) -> Optional[str]: + if self._pos >= 0 and self._pos < len(self._data): + keys = list(self._data[self._pos].keys()) + if 0 <= columnIndex < len(keys): + return str(self._data[self._pos].get(keys[columnIndex], "")) + return None + + def getInt(self, columnIndex: int) -> int: + val = self.getString(columnIndex) + return int(val) if val else 0 + + def getLong(self, columnIndex: int) -> int: + return self.getInt(columnIndex) + + def close(self) -> None: + self._data = [] + + def isClosed(self) -> bool: + return len(self._data) == 0 + + def getPosition(self) -> int: + return self._pos + + def __iter__(self): + for item in self._data: + yield item + + +# ============================================================ +# android.content.Intent +# ============================================================ + +class IntentAction: + """常用 Intent Action 常量""" + ACTION_MAIN = "android.intent.action.MAIN" + ACTION_VIEW = "android.intent.action.VIEW" + ACTION_SEND = "android.intent.action.SEND" + ACTION_SENDTO = "android.intent.action.SENDTO" + ACTION_DIAL = "android.intent.action.DIAL" + ACTION_CALL = "android.intent.action.CALL" + ACTION_CALL_EMERGENCY = "android.intent.action.CALL_EMERGENCY" + ACTION_INSERT = "android.intent.action.INSERT" + ACTION_EDIT = "android.intent.action.EDIT" + ACTION_DELETE = "android.intent.action.DELETE" + ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH" + ACTION_SET_ALARM = "android.intent.action.SET_ALARM" + ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE" + ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE" + ACTION_BATTERY_LOW = "android.intent.action.BATTERY_LOW" + ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED" + ACTION_PICK = "android.intent.action.PICK" + ACTION_GET_CONTENT = "android.intent.action.GET_CONTENT" + ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT" + ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT" + ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED" + ACTION_POWER_CONNECTED = "android.intent.action.POWER_CONNECTED" + ACTION_POWER_DISCONNECTED = "android.intent.action.POWER_DISCONNECTED" + ACTION_SCREEN_ON = "android.intent.action.SCREEN_ON" + ACTION_SCREEN_OFF = "android.intent.action.SCREEN_OFF" + ACTION_TIME_TICK = "android.intent.action.TIME_TICK" + ACTION_USER_PRESENT = "android.intent.action.USER_PRESENT" + + +class Intent: + """模拟 android.content.Intent - 桌面端映射到 webbrowser+subprocess""" + + CATEGORY_DEFAULT = "android.intent.category.DEFAULT" + FLAG_ACTIVITY_NEW_TASK = 0x10000000 + FLAG_ACTIVITY_CLEAR_TOP = 0x04000000 + FLAG_GRANT_READ_URI_PERMISSION = 0x00000001 + + def __init__(self, action_or_params=None, uri_or_params=None): + self._action = "" + self._data: Optional[Uri] = None + self._type = "" + self._flags = 0 + self._categories: List[str] = [] + self._extras: Dict[str, Any] = {} + self._component: Optional[tuple] = None + self._package: Optional[str] = None + + if isinstance(action_or_params, str): + self._action = action_or_params + elif isinstance(action_or_params, dict): + self._from_dict(action_or_params) + + if isinstance(uri_or_params, str): + self._data = Uri.parse(uri_or_params) + elif isinstance(uri_or_params, Uri): + self._data = uri_or_params + + def _from_dict(self, params: dict): + self._action = params.get("action", params.get("intentAction", "")) + d = params.get("data", params.get("uri", params.get("uriString", ""))) + if d: + self._data = Uri.parse(d) if isinstance(d, str) else d + self._type = params.get("type", "") + self._extras = params.get("extras", {}) + + def setAction(self, action: str) -> "Intent": + self._action = action + return self + + def setData(self, data: str) -> "Intent": + self._data = Uri.parse(data) + return self + + def setType(self, type_: str) -> "Intent": + self._type = type_ + return self + + def setDataAndType(self, data: str, type_: str) -> "Intent": + self._data = Uri.parse(data) + 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]: + v = self._extras.get(name) + return str(v) if v is not None else None + + def getIntExtra(self, name: str, default: int = 0) -> int: + return int(self._extras.get(name, default)) + + def getBooleanExtra(self, name: str, default: bool = False) -> bool: + return bool(self._extras.get(name, default)) + + def getLongExtra(self, name: str, default: int = 0) -> int: + return int(self._extras.get(name, default)) + + def getStringArrayListExtra(self, name: str) -> List[str]: + val = self._extras.get(name, []) + return [str(v) for v in val] if isinstance(val, list) else [] + + def addCategory(self, category: str) -> "Intent": + if category not in self._categories: + self._categories.append(category) + return self + + def setFlags(self, flags: int) -> "Intent": + self._flags = flags + return self + + def addFlags(self, flags: int) -> "Intent": + self._flags |= flags + return self + + def setComponent(self, package: str, cls: str) -> "Intent": + self._component = (package, cls) + self._package = package + return self + + def setPackage(self, package: str) -> "Intent": + self._package = package + return self + + def getAction(self) -> str: + return self._action + + def getData(self) -> Optional[Uri]: + return self._data + + def getScheme(self) -> str: + return self._data.getScheme() if self._data else "" + + def resolveActivity(self, pm: "PackageManager") -> str: + """解析 Activity 名称""" + action_map = { + IntentAction.ACTION_VIEW: "com.android.browser/.BrowserActivity", + IntentAction.ACTION_DIAL: "com.android.dialer/.DialtactsActivity", + IntentAction.ACTION_SEND: "com.android.mms/.ui.ConversationList", + IntentAction.ACTION_IMAGE_CAPTURE: "com.android.camera/.CameraActivity", + } + return action_map.get(self._action, "unknown") + + def to_dict(self) -> dict: + return { + "action": self._action, + "data": str(self._data) if self._data else None, + "type": self._type, + "extras": self._extras, + "package": self._package, + "categories": self._categories, + "flags": self._flags, + } + + +# ============================================================ +# android.content.Context +# ============================================================ + +class Context: + """模拟 android.content.Context""" + + MODE_PRIVATE = 0 + MODE_APPEND = 32768 + + def __init__(self, package_name: str = None): + self._package_name = package_name or _package_name + self._package_manager = PackageManager() + self._content_resolver = ContentResolver(self) + self._files_dir = os.path.join(_app_data_dir, "files") + self._cache_dir = os.path.join(_app_data_dir, "cache") + self._preferences: Dict[str, Any] = {} + self._resources = Resources() + os.makedirs(self._files_dir, exist_ok=True) + os.makedirs(self._cache_dir, exist_ok=True) + + def getPackageName(self) -> str: + return self._package_name + + def getPackageManager(self) -> "PackageManager": + return self._package_manager + + def getContentResolver(self) -> ContentResolver: + return self._content_resolver + + def getFilesDir(self) -> str: + return self._files_dir + + def getCacheDir(self) -> str: + return self._cache_dir + + def getDir(self, name: str, mode: int = 0) -> str: + d = os.path.join(_app_data_dir, name) + os.makedirs(d, exist_ok=True) + return d + + def getExternalFilesDir(self, type_: str = None) -> str: + path = os.path.join(_sdcard_root, "Android", "data", self._package_name, "files") + if type_: + path = os.path.join(path, type_) + os.makedirs(path, exist_ok=True) + return path + + def getExternalCacheDir(self) -> str: + path = os.path.join(_sdcard_root, "Android", "data", self._package_name, "cache") + os.makedirs(path, exist_ok=True) + return path + + def getExternalStorageDirectory(self) -> str: + return _sdcard_root + + def getResources(self) -> "Resources": + return self._resources + + def getSystemService(self, name: str) -> Any: + """返回模拟的系统服务""" + services = { + "activity": ActivityManager(), + "power": PowerManager(), + "notification": NotificationManager(), + "alarm": AlarmManager(), + "location": LocationManager(), + "sensor": SensorManager(), + "audio": AudioManager(), + "connectivity": ConnectivityManager(), + "display": DisplayManager(), + "window": WindowManager(), + "input": InputManager(), + "layout_inflater": LayoutInflater(), + "storage": StorageManager(), + "clipboard": ClipboardManager(), + "download": DownloadManager(), + "telephony": TelephonyManager(), + } + return services.get(name) + + def startActivity(self, intent: Intent) -> bool: + """启动 Activity - 映射到桌面操作""" + action = intent.getAction() + + # ACTION_VIEW: 打开 URL + if action == IntentAction.ACTION_VIEW: + data = intent.getData() + if data: + url = data.toString() + if url.startswith("http"): + webbrowser.open(url) + return True + return True + + # ACTION_DIAL: 拨号 + elif action == IntentAction.ACTION_DIAL: + data = intent.getData() + if data: + console.log(f"[AndroidCompat] 模拟拨号: {data.toString()}") + return True + + # ACTION_SEND: 分享 + elif action == IntentAction.ACTION_SEND: + text = intent.getStringExtra("android.intent.extra.TEXT") + if text: + console.log(f"[AndroidCompat] 模拟分享文本: {text[:100]}") + return True + + # ACTION_SET_ALARM: 设置闹钟 + elif action == IntentAction.ACTION_SET_ALARM: + alarm_data = intent.to_dict() + console.log(f"[AndroidCompat] 模拟设置闹钟: {alarm_data['extras']}") + return True + + # ACTION_IMAGE_CAPTURE: 拍照 + elif action == IntentAction.ACTION_IMAGE_CAPTURE: + console.log("[AndroidCompat] 模拟拍照") + return True + + # ACTION_INSERT: 插入日历事件 + elif action == IntentAction.ACTION_INSERT: + console.log(f"[AndroidCompat] 模拟插入日历事件") + return True + + console.log(f"[AndroidCompat] 未映射的 Intent: {intent.getAction()}") + return False + + def sendBroadcast(self, intent: Intent) -> None: + """广播 - 桌面端模拟""" + console.log(f"[AndroidCompat] 模拟广播: {intent.getAction()}") + self._broadcast_receivers = getattr(self, "_broadcast_receivers", {}) + action = intent.getAction() + if action in self._broadcast_receivers: + for receiver in self._broadcast_receivers[action]: + receiver.onReceive(self, intent) + + def registerReceiver(self, receiver: "BroadcastReceiver", filter_: "IntentFilter") -> Intent: + self._broadcast_receivers = getattr(self, "_broadcast_receivers", {}) + action = filter_._action + if action not in self._broadcast_receivers: + self._broadcast_receivers[action] = [] + self._broadcast_receivers[action].append(receiver) + return None + + def unregisterReceiver(self, receiver: "BroadcastReceiver") -> None: + self._broadcast_receivers = getattr(self, "_broadcast_receivers", {}) + for action, receivers in list(self._broadcast_receivers.items()): + self._broadcast_receivers[action] = [r for r in receivers if r != receiver] + + def getSharedPreferences(self, name: str, mode: int = 0) -> "SharedPreferences": + return SharedPreferences(self, name) + + def checkSelfPermission(self, permission: str) -> int: + """模拟权限检查 - 默认有所有权限""" + return 0 # PERMISSION_GRANTED + + def getPackageCodePath(self) -> str: + return __file__ + + def getApplicationInfo(self) -> "ApplicationInfo": + return ApplicationInfo() + + +class ApplicationInfo: + """模拟 android.content.pm.ApplicationInfo""" + def __init__(self): + self.sourceDir = __file__ + self.dataDir = _app_data_dir + self.nativeLibraryDir = os.path.join(os.path.dirname(sys.executable), "lib") + + +class Resources: + """模拟 android.content.res.Resources""" + def getString(self, id_or_name) -> str: + return f"resource_string_{id_or_name}" + def getDrawable(self, id_or_name) -> Any: + return None + def getColor(self, id_or_name) -> int: + return 0xFF000000 + def getDimension(self, id_or_name) -> float: + return 0.0 + def getIdentifier(self, name: str, defType: str, defPackage: str) -> int: + return hash(name) % 0x7FFFFFFF + + +class SharedPreferences: + """模拟 android.content.SharedPreferences""" + def __init__(self, context: Context, name: str): + self._path = os.path.join(_app_data_dir, f"prefs_{name}.json") + self._data = {} + if os.path.isfile(self._path): + try: + with open(self._path, "r") as f: + self._data = json.load(f) + except Exception: + self._data = {} + + def getString(self, key: str, default: str = "") -> str: + return str(self._data.get(key, default)) + + def getInt(self, key: str, default: int = 0) -> int: + return int(self._data.get(key, default)) + + def getBoolean(self, key: str, default: bool = False) -> bool: + return bool(self._data.get(key, default)) + + def getFloat(self, key: str, default: float = 0.0) -> float: + return float(self._data.get(key, default)) + + def getLong(self, key: str, default: int = 0) -> int: + return int(self._data.get(key, default)) + + def contains(self, key: str) -> bool: + return key in self._data + + def edit(self) -> "SharedPreferencesEditor": + return SharedPreferencesEditor(self) + + def getAll(self) -> Dict: + return dict(self._data) + + +class SharedPreferencesEditor: + """SharedPreferences.Editor""" + def __init__(self, prefs: SharedPreferences): + self._prefs = prefs + self._changes = {} + + def putString(self, key: str, value: str) -> "SharedPreferencesEditor": + self._changes[key] = value + return self + + def putInt(self, key: str, value: int) -> "SharedPreferencesEditor": + self._changes[key] = value + return self + + def putBoolean(self, key: str, value: bool) -> "SharedPreferencesEditor": + self._changes[key] = value + return self + + def putLong(self, key: str, value: int) -> "SharedPreferencesEditor": + self._changes[key] = value + return self + + def remove(self, key: str) -> "SharedPreferencesEditor": + self._changes[key] = None + return self + + def clear(self) -> "SharedPreferencesEditor": + self._changes = {} + self._prefs._data = {} + return self + + def apply(self) -> None: + self.commit() + + def commit(self) -> bool: + for k, v in self._changes.items(): + if v is None: + self._prefs._data.pop(k, None) + else: + self._prefs._data[k] = v + try: + os.makedirs(os.path.dirname(self._prefs._path), exist_ok=True) + with open(self._prefs._path, "w", encoding="utf-8") as f: + json.dump(self._prefs._data, f, indent=2, ensure_ascii=False) + return True + except Exception: + return False + + +# ============================================================ +# android.content.BroadcastReceiver + IntentFilter +# ============================================================ + +class BroadcastReceiver: + """模拟 android.content.BroadcastReceiver""" + def onReceive(self, context: Context, intent: Intent) -> None: + pass + + +class IntentFilter: + """模拟 android.content.IntentFilter""" + def __init__(self, action: str = None): + self._action = action + self._actions: List[str] = [] + if action: + self._actions.append(action) + + def addAction(self, action: str) -> None: + if action not in self._actions: + self._actions.append(action) + + +# ============================================================ +# android.os (Build, Environment, PowerManager, Vibration) +# ============================================================ + +class Build: + """模拟 android.os.Build""" + + class VERSION: + SDK_INT = 33 # Android 13 + RELEASE = "13" + CODENAME = "Tiramisu" + INCREMENTAL = "2026.06" + SDK_NAME = "Tiramisu" + + BOARD = "generic" + BRAND = "OperitCompat" + DEVICE = "pc" + DISPLAY = "OP1.230607.001" + FINGERPRINT = "generic/OperitCompat/pc:13/TP1A.220624.014/12345678:user/release-keys" + HARDWARE = "x86_64" + HOST = "localhost" + ID = "TP1A.220624.014" + MANUFACTURER = "OperitCompat" + MODEL = platform.node() + PRODUCT = "operit_compat_pc" + SERIAL = "0123456789ABCDEF" + SUPPORTED_ABIS = [platform.machine()] + TAGS = "release-keys" + TIME = int(time.time()) + TYPE = "user" + USER = os.environ.get("USER", "user") + + @staticmethod + def getRadioVersion() -> str: + return "unknown" + + @staticmethod + def getSerial() -> str: + return Build.SERIAL + + +class Environment: + """模拟 android.os.Environment""" + + MEDIA_MOUNTED = "mounted" + MEDIA_UNMOUNTED = "unmounted" + + @staticmethod + def getExternalStorageDirectory() -> str: + return _sdcard_root + + @staticmethod + def getExternalStoragePublicDirectory(type_: str) -> str: + mapping = { + "DCIM": "DCIM", + "Download": "Download", + "Documents": "Documents", + "Music": "Music", + "Movies": "Movies", + "Pictures": "Pictures", + "Alarms": "Alarms", + "Notifications": "Notifications", + "Ringtones": "Ringtones", + } + subdir = mapping.get(type_, type_) + path = os.path.join(_sdcard_root, subdir) + os.makedirs(path, exist_ok=True) + return path + + @staticmethod + def getDataDirectory() -> str: + return _app_data_dir + + @staticmethod + def getDownloadCacheDirectory() -> str: + return os.path.join(_app_data_dir, "cache", "downloads") + + @staticmethod + def getExternalStorageState() -> str: + return Environment.MEDIA_MOUNTED + + +class PowerManager: + """模拟 android.os.PowerManager""" + PARTIAL_WAKE_LOCK = 1 + SCREEN_BRIGHT_WAKE_LOCK = 10 + FULL_WAKE_LOCK = 26 + ACQUIRE_CAUSES_WAKEUP = 0x10000000 + ON_AFTER_RELEASE = 0x20000000 + + def newWakeLock(self, levelAndFlags: int, tag: str) -> "WakeLock": + return WakeLock() + + def isScreenOn(self) -> bool: + return True + + def isInteractive(self) -> bool: + return True + + def isPowerSaveMode(self) -> bool: + return False + + def isDeviceIdleMode(self) -> bool: + return False + + def reboot(self, reason: str = None) -> None: + console.log(f"[AndroidCompat] 模拟重启: {reason}") + + def goToSleep(self, timeMs: int) -> None: + pass + + +class WakeLock: + """模拟 android.os.PowerManager.WakeLock""" + def acquire(self, timeoutMs: int = None) -> None: + pass + def release(self, flags: int = 0) -> None: + pass + def isHeld(self) -> bool: + return False + def setReferenceCounted(self, value: bool) -> None: + pass + + +class VibrationEffect: + """模拟 android.os.VibrationEffect""" + DEFAULT_AMPLITUDE = -1 + + @staticmethod + def createOneShot(milliseconds: int, amplitude: int = DEFAULT_AMPLITUDE) -> "VibrationEffect": + return VibrationEffect() + + @staticmethod + def createWaveform(timings: List[int], amplitudes: List[int], repeat: int) -> "VibrationEffect": + return VibrationEffect() + + +class Vibrator: + """模拟 android.os.Vibrator""" + def vibrate(self, effect_or_ms) -> None: + pass + def hasVibrator(self) -> bool: + return False + def cancel(self) -> None: + pass + + +# ============================================================ +# android.app services +# ============================================================ + +class ActivityManager: + """模拟 android.app.ActivityManager""" + def getRunningAppProcesses(self) -> List: + if HAS_PSUTIL: + return [{"processName": p.name(), "pid": p.pid, + "importance": 100} for p in psutil.process_iter()[:10]] + return [{"processName": "python", "pid": os.getpid(), "importance": 100}] + + def getMemoryInfo(self) -> "MemoryInfo": + return ActivityManager.MemoryInfo() + + def killBackgroundProcesses(self, packageName: str) -> None: + console.log(f"[AndroidCompat] 模拟结束进程: {packageName}") + + class MemoryInfo: + def __init__(self): + self.availMem = 8 * 1024 * 1024 * 1024 # 8GB + self.totalMem = 16 * 1024 * 1024 * 1024 # 16GB + self.threshold = 256 * 1024 * 1024 # 256MB + self.lowMemory = False + + def getAppTasks(self) -> List: + return [] + + +class PackageManager: + """模拟 android.content.pm.PackageManager""" + + def getInstalledPackages(self, flags: int = 0) -> List["PackageInfo"]: + """返回已安装的应用列表""" + packages = [] + system_packages = [ + "com.android.settings", "com.android.systemui", + "com.android.launcher3", "com.google.android.gms", + "com.google.android.gsf", "com.android.documentsui", + "com.android.providers.downloads", "com.android.vending", + ] + for pkg in system_packages: + packages.append(PackageInfo(pkg, "13", True)) + packages.append(PackageInfo(_package_name, "1.0.0", False)) + return packages + + def getPackageInfo(self, packageName: str, flags: int = 0) -> Optional["PackageInfo"]: + for pkg in self.getInstalledPackages(): + if pkg.packageName == packageName: + return pkg + return PackageInfo(packageName, "0.0.0", False) + + def getApplicationLabel(self, info: "ApplicationInfo") -> str: + return info.packageName if hasattr(info, 'packageName') else _package_name + + def queryIntentActivities(self, intent: Intent, flags: int = 0) -> List: + return [ResolveInfo()] + + def resolveActivity(self, intent: Intent, flags: int = 0) -> Optional["ResolveInfo"]: + return ResolveInfo() + + def checkPermission(self, permission: str, pid: int, uid: int) -> int: + return 0 # PERMISSION_GRANTED + + def getPackagesForUid(self, uid: int) -> List[str]: + return [_package_name] + + @staticmethod + def getPermission(permission: str) -> str: + return f"android.permission.{permission}" + + +class PackageInfo: + def __init__(self, pkg_name: str, version: str = "1.0", is_system: bool = False): + self.packageName = pkg_name + self.versionName = version + self.versionCode = int(version.replace(".", "")) + self.applicationInfo = ApplicationInfo() + self.applicationInfo.packageName = pkg_name + self.firstInstallTime = int(time.time()) + self.lastUpdateTime = int(time.time()) + self.sharedUserId = None + self.requestedPermissions = [] + self.gids = [] + + +class ResolveInfo: + def __init__(self): + self.activityInfo = ActivityInfo() + self.resolvePackageName = "com.android.browser" + self.priority = 0 + + def loadLabel(self, pm: PackageManager) -> str: + return self.resolvePackageName + + +class ActivityInfo: + def __init__(self): + self.packageName = _package_name + self.name = "MainActivity" + self.processName = _package_name + self.taskAffinity = _package_name + self.launchMode = 0 + self.flags = 0 + self.screenOrientation = 1 + self.configChanges = 0 + self.exported = True + self.theme = 0 + + +class NotificationManager: + """模拟 android.app.NotificationManager""" + IMPORTANCE_HIGH = 4 + IMPORTANCE_DEFAULT = 3 + IMPORTANCE_LOW = 2 + IMPORTANCE_MIN = 1 + IMPORTANCE_NONE = 0 + + def notify(self, id_or_tag, notification_or_id=None) -> None: + console.log(f"[AndroidCompat] 模拟通知: id={id_or_tag}") + + def cancel(self, id_or_tag, notification_id=None) -> None: + pass + + def cancelAll(self) -> None: + pass + + def createNotificationChannel(self, channel) -> None: + pass + + def getNotificationChannel(self, channelId: str) -> Optional["NotificationChannel"]: + return NotificationChannel(channelId, "Default", self.IMPORTANCE_DEFAULT) + + +class NotificationChannel: + def __init__(self, id_: str, name: str, importance: int): + self.id = id_ + self.name = name + self.importance = importance + self.description = "" + self.enableVibration = True + self.enableLights = True + + +class AlarmManager: + """模拟 android.app.AlarmManager""" + RTC = 0 + RTC_WAKEUP = 1 + ELAPSED_REALTIME = 2 + ELAPSED_REALTIME_WAKEUP = 3 + INTERVAL_FIFTEEN_MINUTES = 900000 + INTERVAL_HALF_HOUR = 1800000 + INTERVAL_HOUR = 3600000 + INTERVAL_DAY = 86400000 + + def set(self, type_: int, triggerAtMillis: int, operation: Any) -> None: + console.log(f"[AndroidCompat] 模拟闹钟: type={type_}, trigger={triggerAtMillis}") + + def setRepeating(self, type_: int, triggerAtMillis: int, intervalMillis: int, operation: Any) -> None: + console.log(f"[AndroidCompat] 模拟重复闹钟: trigger={triggerAtMillis}, interval={intervalMillis}") + + def setExact(self, type_: int, triggerAtMillis: int, operation: Any) -> None: + self.set(type_, triggerAtMillis, operation) + + def cancel(self, operation: Any) -> None: + pass + + def setAlarmClock(self, info: "AlarmClockInfo", operation: Any) -> None: + pass + + +# ============================================================ +# android.graphics +# ============================================================ + +class Color: + """模拟 android.graphics.Color""" + BLACK = 0xFF000000 + WHITE = 0xFFFFFFFF + RED = 0xFFFF0000 + GREEN = 0xFF00FF00 + BLUE = 0xFF0000FF + YELLOW = 0xFFFFFF00 + CYAN = 0xFF00FFFF + MAGENTA = 0xFFFF00FF + GRAY = 0xFF808080 + TRANSPARENT = 0x00000000 + + @staticmethod + def argb(alpha: int, red: int, green: int, blue: int) -> int: + return (alpha << 24) | (red << 16) | (green << 8) | blue + + @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 parseColor(colorString: str) -> int: + colorString = colorString.strip() + if colorString.startswith("#"): + hex_str = colorString[1:] + if len(hex_str) == 6: + return 0xFF000000 | int(hex_str, 16) + elif len(hex_str) == 8: + return int(hex_str, 16) + return Color.BLACK + + @staticmethod + def rgb(red: int, green: int, blue: int) -> int: + return 0xFF000000 | (red << 16) | (green << 8) | blue + + @staticmethod + def toArgb(color: int) -> int: + return color + + @staticmethod + def toColor(c): + return c + + +class Rect: + """模拟 android.graphics.Rect""" + def __init__(self, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0): + 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 contains(self, x: int, y: int) -> bool: + return self.left <= x <= self.right and self.top <= y <= self.bottom + + def isEmpty(self) -> bool: + return self.width() <= 0 or self.height() <= 0 + + def inset(self, dx: int, dy: int) -> None: + self.left += dx + self.right -= dx + self.top += dy + self.bottom -= dy + + def offset(self, dx: int, dy: int) -> None: + self.left += dx + self.right += dx + self.top += dy + self.bottom += dy + + def toShortString(self) -> str: + return f"[{self.left},{self.top}][{self.right},{self.bottom}]" + + def __repr__(self): + return self.toShortString() + + +class Paint: + """模拟 android.graphics.Paint""" + ANTI_ALIAS_FLAG = 0x01 + FILL = 0 + STROKE = 1 + FILL_AND_STROKE = 2 + + def __init__(self, flags: int = 0): + self._flags = flags + self._color = Color.BLACK + self._strokeWidth = 0.0 + self._textSize = 16.0 + self._style = self.FILL + self._alpha = 255 + self._typeface = None + + def setColor(self, color: int) -> None: + self._color = color + + def getColor(self) -> int: + return self._color + + def setStrokeWidth(self, width: float) -> None: + self._strokeWidth = width + + def setTextSize(self, size: float) -> None: + self._textSize = size + + def setStyle(self, style: int) -> None: + self._style = style + + def setAlpha(self, alpha: int) -> None: + self._alpha = alpha + + def setTypeface(self, typeface) -> None: + self._typeface = typeface + + def measureText(self, text: str) -> float: + return len(text) * self._textSize * 0.6 + + +class Bitmap: + """模拟 android.graphics.Bitmap - 基于 PIL""" + + class Config: + ARGB_8888 = "ARGB_8888" + RGB_565 = "RGB_565" + ALPHA_8 = "ALPHA_8" + + @staticmethod + def createBitmap(width: int, height: int, config: str = Config.ARGB_8888) -> "Bitmap": + return Bitmap(width, height, config) + + @staticmethod + def createScaledBitmap(src: "Bitmap", dstWidth: int, dstHeight: int, filter: bool) -> "Bitmap": + if HAS_PIL: + pil_img = src._pil.resize((dstWidth, dstHeight), Image.LANCZOS if filter else Image.NEAREST) + result = Bitmap(dstWidth, dstHeight, src._config) + result._pil = pil_img + return result + return Bitmap(dstWidth, dstHeight, src._config) + + @staticmethod + def decodeFile(path: str) -> Optional["Bitmap"]: + if HAS_PIL: + try: + pil_img = Image.open(path) + bmp = Bitmap(pil_img.width, pil_img.height) + bmp._pil = pil_img + return bmp + except Exception: + return None + return None + + @staticmethod + def decodeByteArray(data: bytes, offset: int, length: int) -> Optional["Bitmap"]: + if HAS_PIL: + try: + pil_img = Image.open(io.BytesIO(data[offset:offset + length])) + bmp = Bitmap(pil_img.width, pil_img.height) + bmp._pil = pil_img + return bmp + except Exception: + return None + return None + + def __init__(self, width: int, height: int, config: str = Config.ARGB_8888): + self._width = width + self._height = height + self._config = config + if HAS_PIL: + self._pil = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + else: + self._pil = None + self._recycled = False + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def getConfig(self) -> str: + return self._config + + def getPixel(self, x: int, y: int) -> int: + if HAS_PIL and self._pil: + try: + r, g, b, a = self._pil.getpixel((x, y)) + return Color.argb(a, r, g, b) + except Exception: + pass + return 0 + + def setPixel(self, x: int, y: int, color: int) -> None: + if HAS_PIL and self._pil: + a = Color.alpha(color) + r = Color.red(color) + g = Color.green(color) + b = Color.blue(color) + self._pil.putpixel((x, y), (r, g, b, a)) + + def eraseColor(self, color: int) -> None: + if HAS_PIL and self._pil: + r, g, b, a = Color.red(color), Color.green(color), Color.blue(color), Color.alpha(color) + self._pil = Image.new("RGBA", (self._width, self._height), (r, g, b, a)) + + def compress(self, format_: "Bitmap.CompressFormat", quality: int, stream: io.BytesIO) -> bool: + if HAS_PIL and self._pil: + pil_fmt = "JPEG" if format_ == Bitmap.CompressFormat.JPEG else "PNG" + if self._pil.mode == "RGBA": + self._pil = self._pil.convert("RGB") if pil_fmt == "JPEG" else self._pil + self._pil.save(stream, format=pil_fmt, quality=quality) + return True + return False + + def copy(self, config: str, isMutable: bool) -> "Bitmap": + result = Bitmap(self._width, self._height, config) + if HAS_PIL and self._pil: + result._pil = self._pil.copy() + return result + + def recycle(self) -> None: + self._recycled = True + self._pil = None + + def isRecycled(self) -> bool: + return self._recycled + + class CompressFormat: + JPEG = "JPEG" + PNG = "PNG" + WEBP = "WEBP" + + +class Canvas: + """模拟 android.graphics.Canvas - 基于 PIL""" + + def __init__(self, bitmap: Bitmap = None): + self._bitmap = bitmap + self._draw = ImageDraw.Draw(bitmap._pil) if HAS_PIL and bitmap and bitmap._pil else None + + def drawColor(self, color: int) -> None: + if self._bitmap: + self._bitmap.eraseColor(color) + + def drawPoint(self, x: float, y: float, paint: Paint) -> None: + if self._draw: + c = Color.toArgb(paint._color) + self._draw.point([(x, y)], fill=(Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))) + + def drawLine(self, startX: float, startY: float, stopX: float, stopY: float, paint: Paint) -> None: + if self._draw: + c = Color.toArgb(paint._color) + self._draw.line([(startX, startY), (stopX, stopY)], + fill=(Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)), + width=max(1, int(paint._strokeWidth))) + + def drawRect(self, rect: Rect, paint: Paint) -> None: + if self._draw: + c = Color.toArgb(paint._color) + self._draw.rectangle([rect.left, rect.top, rect.right, rect.bottom], + fill=(Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))) + + def drawCircle(self, cx: float, cy: float, radius: float, paint: Paint) -> None: + if self._draw: + c = Color.toArgb(paint._color) + self._draw.ellipse([cx - radius, cy - radius, cx + radius, cy + radius], + fill=(Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))) + + def drawText(self, text: str, x: float, y: float, paint: Paint) -> None: + if self._draw: + c = Color.toArgb(paint._color) + self._draw.text((x, y), text, fill=(Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))) + + def drawBitmap(self, bitmap: Bitmap, left: float, top: float, paint: Paint = None) -> None: + if HAS_PIL and self._bitmap and self._bitmap._pil and bitmap._pil: + self._bitmap._pil.paste(bitmap._pil, (int(left), int(top))) + + def getWidth(self) -> int: + return self._bitmap._width if self._bitmap else 0 + + def getHeight(self) -> int: + return self._bitmap._height if self._bitmap else 0 + + def save(self) -> int: + return 1 + + def restore(self) -> None: + pass + + +class Typeface: + """模拟 android.graphics.Typeface""" + DEFAULT = "DEFAULT" + BOLD = "BOLD" + ITALIC = "ITALIC" + BOLD_ITALIC = "BOLD_ITALIC" + MONOSPACE = "MONOSPACE" + SANS_SERIF = "SANS_SERIF" + SERIF = "SERIF" + + @staticmethod + def defaultFromStyle(style: int) -> "Typeface": + return Typeface() + + @staticmethod + def create(name: str, style: int) -> "Typeface": + return Typeface() + + +class BitmapFactory: + """模拟 android.graphics.BitmapFactory""" + + @staticmethod + def decodeFile(path: str, opts: dict = None) -> Optional[Bitmap]: + return Bitmap.decodeFile(path) + + @staticmethod + def decodeByteArray(data: bytes, offset: int, length: int, opts: dict = None) -> Optional[Bitmap]: + return Bitmap.decodeByteArray(data, offset, length) + + @staticmethod + def decodeResource(res: Resources, id_: int) -> Optional[Bitmap]: + return Bitmap.createBitmap(100, 100) + + class Options: + def __init__(self): + self.inSampleSize = 1 + self.inPreferredConfig = Bitmap.Config.ARGB_8888 + self.inJustDecodeBounds = False + self.outWidth = 0 + self.outHeight = 0 + self.inMutable = False + + +class Matrix: + """模拟 android.graphics.Matrix""" + def __init__(self): + self._values = [1, 0, 0, 0, 1, 0, 0, 0, 1] + + def setScale(self, sx: float, sy: float) -> None: + self._values[0] = sx + self._values[4] = sy + + def setTranslate(self, dx: float, dy: float) -> None: + self._values[2] = dx + self._values[5] = dy + + def setRotate(self, degrees: float) -> None: + rad = math.radians(degrees) + self._values[0] = math.cos(rad) + self._values[1] = -math.sin(rad) + self._values[3] = math.sin(rad) + self._values[4] = math.cos(rad) + + def postScale(self, sx: float, sy: float) -> None: + self._values[0] *= sx + self._values[4] *= sy + + def postTranslate(self, dx: float, dy: float) -> None: + self._values[2] += dx + self._values[5] += dy + + def mapRect(self, rect: Rect) -> Rect: + return rect + + +# ============================================================ +# android.graphics.pdf.PdfRenderer +# ============================================================ + +class PdfRenderer: + """模拟 android.graphics.pdf.PdfRenderer - 基于 PyMuPDF""" + + def __init__(self, input: Any, renderMode: int = 0): + self._pageCount = 0 + self._currentPage = None + self._closed = False + try: + import fitz + self._doc = fitz.open(input) + self._pageCount = len(self._doc) + except ImportError: + console.log("[PdfRenderer] PyMuPDF 未安装,使用 pdf2image 降级") + self._pageCount = 0 + try: + from pdf2image import convert_from_path + self._images = convert_from_path(input if isinstance(input, str) else "temp.pdf") + self._pageCount = len(self._images) + except ImportError: + console.log("[PdfRenderer] pdf2image 也未安装,使用空回退") + self._images = [] + + def getPageCount(self) -> int: + return self._pageCount + + def openPage(self, index: int) -> "PdfRenderer.Page": + self._currentPage = PdfRenderer.Page(index) + try: + if hasattr(self, '_doc'): + fitz_page = self._doc[index] + pix = fitz_page.get_pixmap() + pil_img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) + bmp = Bitmap(pix.width, pix.height) + bmp._pil = pil_img + self._currentPage._render = bmp + self._currentPage._width = pix.width + self._currentPage._height = pix.height + elif hasattr(self, '_images') and index < len(self._images): + pil_img = self._images[index] + bmp = Bitmap(pil_img.width, pil_img.height) + bmp._pil = pil_img + self._currentPage._render = bmp + self._currentPage._width = pil_img.width + self._currentPage._height = pil_img.height + except Exception as e: + console.log(f"[PdfRenderer] 渲染第 {index} 页出错: {e}") + return self._currentPage + + def close(self) -> None: + self._closed = True + if hasattr(self, '_doc'): + self._doc.close() + + class Page: + RENDER_MODE_FOR_DISPLAY = 1 + RENDER_MODE_FOR_PRINT = 2 + + def __init__(self, index: int): + self._index = index + self._render = None + self._width = 0 + self._height = 0 + + def getWidth(self) -> int: + return self._width + + def getHeight(self) -> int: + return self._height + + def render(self, bitmap: Bitmap, left: int, top: int, right: int, bottom: int, + paintFlags: int, renderMode: int = RENDER_MODE_FOR_DISPLAY) -> None: + if self._render and self._render._pil and bitmap._pil: + bitmap._pil.paste(self._render._pil.resize((right - left, bottom - top))) + + def close(self) -> None: + pass + + +# ============================================================ +# android.view / accessibility 体系 (UINode) +# ============================================================ + +class View: + """模拟 android.view.View""" + def __init__(self, context: Context = None): + self._context = context + self._x = 0 + self._y = 0 + self._width = 0 + self._height = 0 + self._visible = True + self._enabled = True + self._clickable = False + self._text = "" + self._contentDescription = "" + self._children: List[View] = [] + self._parent: Optional[View] = None + self._backgroundColor = Color.TRANSPARENT + self._alpha = 1.0 + self._rotation = 0.0 + self._scaleX = 1.0 + self._scaleY = 1.0 + self._tag = None + self._id = -1 + self._onClickListener = None + self._elevation = 0.0 + + def getX(self) -> float: return float(self._x) + def getY(self) -> float: return float(self._y) + def getWidth(self) -> int: return self._width + def getHeight(self) -> int: return self._height + def getVisibility(self) -> int: return 0 if self._visible else 8 + def isEnabled(self) -> bool: return self._enabled + def isClickable(self) -> bool: return self._clickable + def isShown(self) -> bool: return self._visible + + def setVisibility(self, visibility: int) -> None: + self._visible = (visibility != 8) + def setEnabled(self, enabled: bool) -> None: self._enabled = enabled + def setClickable(self, clickable: bool) -> None: self._clickable = clickable + def setBackgroundColor(self, color: int) -> None: self._backgroundColor = color + def setAlpha(self, alpha: float) -> None: self._alpha = alpha + def setRotation(self, rotation: float) -> None: self._rotation = rotation + def setScaleX(self, sx: float) -> None: self._scaleX = sx + def setScaleY(self, sy: float) -> None: self._scaleY = sy + def setTag(self, tag: Any) -> None: self._tag = tag + def setId(self, id_: int) -> None: self._id = id_ + + def getText(self) -> str: return self._text + def setText(self, text: str) -> None: self._text = text + def getContentDescription(self) -> str: return self._contentDescription + def setContentDescription(self, desc: str) -> None: self._contentDescription = desc + + def getParent(self) -> Optional["View"]: return self._parent + def getChildCount(self) -> int: return len(self._children) + def getChildAt(self, index: int) -> Optional["View"]: + return self._children[index] if 0 <= index < len(self._children) else None + def addView(self, child: "View") -> None: + self._children.append(child) + child._parent = self + def removeView(self, child: "View") -> None: + if child in self._children: + self._children.remove(child) + child._parent = None + def removeAllViews(self) -> None: + for child in self._children: + child._parent = None + self._children.clear() + + def findViewById(self, id_: int) -> Optional["View"]: + if self._id == id_: return self + for child in self._children: + result = child.findViewById(id_) + if result: return result + return None + + def getGlobalVisibleRect(self, rect: Rect) -> bool: + r = Rect(self._x, self._y, self._x + self._width, self._y + self._height) + rect.left, rect.top, rect.right, rect.bottom = r.left, r.top, r.right, r.bottom + return self._visible + + def performClick(self) -> bool: + if self._onClickListener: + self._onClickListener.onClick(self) + return True + if HAS_PYAUTOGUI: + pyautogui.click(self._x + self._width // 2, self._y + self._height // 2) + return True + return False + + def setOnClickListener(self, listener) -> None: + self._onClickListener = listener + + def post(self, action: Callable) -> bool: + action() + return True + + def postDelayed(self, action: Callable, delayMs: int) -> bool: + import threading + timer = threading.Timer(delayMs / 1000, action) + timer.start() + return True + + def invalidate(self) -> None: pass + + +class TextView(View): + """模拟 android.widget.TextView""" + def __init__(self, context: Context = None): + super().__init__(context) + self._text = "" + self._textSize = 16.0 + self._textColor = Color.BLACK + self._gravity = 0 + self._lines = 1 + self._typeface = None + self._inputType = 0 + self._hint = "" + self._maxLines = 1 + self._ellipsize = None + + def setTextSize(self, size: float) -> None: self._textSize = size + def setTextColor(self, color: int) -> None: self._textColor = color + def setGravity(self, gravity: int) -> None: self._gravity = gravity + def setLines(self, lines: int) -> None: self._lines = lines + def setHint(self, hint: str) -> None: self._hint = hint + def setInputType(self, type_: int) -> None: self._inputType = type_ + def setMaxLines(self, maxLines: int) -> None: self._maxLines = maxLines + def setEllipsize(self, where: str) -> None: self._ellipsize = where + def getText(self) -> str: return self._text + def setText(self, text: str) -> None: self._text = text + def getTextSize(self) -> float: return self._textSize + def getCurrentTextColor(self) -> int: return self._textColor + def getHint(self) -> str: return self._hint + + +class EditText(TextView): + """模拟 android.widget.EditText""" + def __init__(self, context: Context = None): + super().__init__(context) + self._selectionStart = 0 + self._selectionEnd = 0 + + def getSelectionStart(self) -> int: return self._selectionStart + def getSelectionEnd(self) -> int: return self._selectionEnd + def setSelection(self, start: int, stop: int = None) -> None: + self._selectionStart = start + self._selectionEnd = stop if stop is not None else start + + def selectAll(self) -> None: + self._selectionStart = 0 + self._selectionEnd = len(self._text) + + def append(self, text: str) -> None: + self._text += text + + +class Button(TextView): + """模拟 android.widget.Button""" + def __init__(self, context: Context = None): + super().__init__(context) + self._clickable = True + + +class ImageView(View): + """模拟 android.widget.ImageView""" + ScaleType = type("ScaleType", (), {"FIT_XY": "FIT_XY", "CENTER_CROP": "CENTER_CROP", + "CENTER_INSIDE": "CENTER_INSIDE", "MATRIX": "MATRIX"}) + + def __init__(self, context: Context = None): + super().__init__(context) + self._imageBitmap = None + self._scaleType = ImageView.ScaleType.FIT_CENTER + + def setImageBitmap(self, bmp: Bitmap) -> None: self._imageBitmap = bmp + def setImageResource(self, resId: int) -> None: pass + def setScaleType(self, scaleType: str) -> None: self._scaleType = scaleType + def getDrawable(self) -> Any: return self._imageBitmap + + +class ScrollView(View): + """模拟 android.widget.ScrollView""" + def __init__(self, context: Context = None): + super().__init__(context) + self._scrollX = 0 + self._scrollY = 0 + + def scrollTo(self, x: int, y: int) -> None: + self._scrollX = x + self._scrollY = y + + def scrollBy(self, dx: int, dy: int) -> None: + self._scrollX += dx + self._scrollY += dy + + def fullScroll(self, direction: int) -> bool: return True + + def getScrollX(self) -> int: return self._scrollX + def getScrollY(self) -> int: return self._scrollY + + +class RecyclerView(View): + """模拟 androidx.recyclerview.widget.RecyclerView""" + def smoothScrollToPosition(self, position: int) -> None: pass + + +class ViewGroup(View): + """模拟 android.view.ViewGroup""" + def __init__(self, context: Context = None): + super().__init__(context) + + def addView(self, child: View) -> None: + self._children.append(child) + child._parent = self + + def removeView(self, child: View) -> None: + self._children.remove(child) + child._parent = None + + def removeAllViews(self) -> None: + self._children.clear() + + def getChildCount(self) -> int: + return len(self._children) + + def getChildAt(self, index: int) -> Optional[View]: + return self._children[index] if 0 <= index < len(self._children) else None + + def findViewById(self, id_: int) -> Optional[View]: + return super().findViewById(id_) + + def indexOfChild(self, child: View) -> int: + return self._children.index(child) if child in self._children else -1 + + +class LinearLayout(ViewGroup): + """模拟 android.widget.LinearLayout""" + HORIZONTAL = 0 + VERTICAL = 1 + + def __init__(self, context: Context = None): + super().__init__(context) + self._orientation = self.VERTICAL + self._gravity = 0 + + def setOrientation(self, orientation: int) -> None: + self._orientation = orientation + + def getOrientation(self) -> int: + return self._orientation + + def setGravity(self, gravity: int) -> None: + self._gravity = gravity + + +class FrameLayout(ViewGroup): + """模拟 android.widget.FrameLayout""" + def __init__(self, context: Context = None): + super().__init__(context) + + +class RelativeLayout(ViewGroup): + """模拟 android.widget.RelativeLayout""" + def __init__(self, context: Context = None): + super().__init__(context) + + +# ============================================================ +# UINode - Android UI 自动化核心 +# ============================================================ + +class UINode: + """模拟 Android 无障碍 UI 节点树 - 基于 pyautogui / Playwright""" + + def __init__(self): + self._nodes: List[dict] = [] + self._focused_node = None + + @staticmethod + async def getCurrentPage() -> "AccessibilityPage": + """获取当前页面无障碍树""" + return AccessibilityPage() + + +class AccessibilityNodeInfo: + """模拟 android.view.accessibility.AccessibilityNodeInfo""" + def __init__(self): + self._node_id = hash(str(time.time() + random.random())) + self.packageName = _package_name + self.className = "android.view.View" + self.text = "" + self.contentDescription = "" + self.viewId = "" + self.resourceName = "" + self.boundsInScreen = Rect(0, 0, 1920, 1080) + self.visibleToUser = True + self.enabled = True + self.clickable = False + self.longClickable = False + self.focusable = False + self.focused = False + self.scrollable = False + self.checkable = False + self.checked = False + self.selected = False + self.password = False + self._children: List["AccessibilityNodeInfo"] = [] + self._parent = None + self.childCount = 0 + self.row = -1 + self.column = -1 + self.rowCount = -1 + self.columnCount = -1 + self.inputType = 0 + self.hintText = "" + self.errorText = "" + self.depth = 0 + + def getChild(self, index: int) -> Optional["AccessibilityNodeInfo"]: + return self._children[index] if 0 <= index < len(self._children) else None + + def getChildCount(self) -> int: + return len(self._children) + + def getParent(self) -> Optional["AccessibilityNodeInfo"]: + return self._parent + + def addChild(self, child: "AccessibilityNodeInfo") -> None: + self._children.append(child) + child._parent = self + self.childCount = len(self._children) + child.depth = self.depth + 1 + + def findByText(self, text: str) -> Optional["AccessibilityNodeInfo"]: + if text in (self.text or "") or text in (self.contentDescription or ""): + return self + for child in self._children: + result = child.findByText(text) + if result: + return result + return None + + def findById(self, id_: str) -> Optional["AccessibilityNodeInfo"]: + if self.viewId == id_ or self.resourceName == id_: + return self + for child in self._children: + result = child.findById(id_) + if result: + return result + return None + + def findAccessibilityFocus(self) -> Optional["AccessibilityNodeInfo"]: + if self.focused: + return self + for child in self._children: + result = child.findAccessibilityFocus() + if result: + return result + return None + + def performAction(self, action: int) -> bool: + if action == 16: # CLICK + if HAS_PYAUTOGUI: + x = self.boundsInScreen.centerX() + y = self.boundsInScreen.centerY() + pyautogui.click(x, y) + return True + elif action == 1: # FOCUS + self.focused = True + return True + elif action == 64: # ACCESSIBILITY_FOCUS + self.focused = True + return True + elif action == 4096: # SCROLL_FORWARD + if HAS_PYAUTOGUI: + pyautogui.scroll(-5) + return True + elif action == 8192: # SCROLL_BACKWARD + if HAS_PYAUTOGUI: + pyautogui.scroll(5) + return True + elif action == 128: # CLEAR_ACCESSIBILITY_FOCUS + self.focused = False + return True + return False + + def recycle(self) -> None: + pass + + +class AccessibilityPage: + """模拟当前页面的无障碍信息""" + + CLICK = 16 + LONG_CLICK = 32 + FOCUS = 1 + SCROLL_FORWARD = 4096 + SCROLL_BACKWARD = 8192 + ACCESSIBILITY_FOCUS = 64 + CLEAR_ACCESSIBILITY_FOCUS = 128 + + def __init__(self): + self._root = AccessibilityNodeInfo() + self._build_fake_tree() + + def _build_fake_tree(self): + """构建模拟的 UI 树""" + self._root.className = "android.widget.FrameLayout" + self._root.packageName = _package_name + self._root.resourceName = f"{_package_name}:id/content" + self._root.boundsInScreen = Rect(0, 0, 1920, 1080) + self._root.depth = 0 + + # 状态栏 + status_bar = AccessibilityNodeInfo() + status_bar.className = "android.widget.LinearLayout" + status_bar.resourceName = f"{_package_name}:id/statusBar" + status_bar.boundsInScreen = Rect(0, 0, 1920, 80) + self._root.addChild(status_bar) + + # 主内容区域 + content = AccessibilityNodeInfo() + content.className = "android.widget.ScrollView" + content.resourceName = f"{_package_name}:id/contentArea" + content.boundsInScreen = Rect(0, 80, 1920, 1000) + content.contentDescription = "主内容区域" + self._root.addChild(content) + + # 底部导航 + nav = AccessibilityNodeInfo() + nav.className = "android.widget.LinearLayout" + nav.resourceName = f"{_package_name}:id/navigation" + nav.boundsInScreen = Rect(0, 1000, 1920, 1080) + self._root.addChild(nav) + + btn_home = AccessibilityNodeInfo() + btn_home.className = "android.widget.Button" + btn_home.text = "首页" + btn_home.clickable = True + btn_home.boundsInScreen = Rect(0, 1000, 640, 1080) + nav.addChild(btn_home) + + btn_search = AccessibilityNodeInfo() + btn_search.className = "android.widget.Button" + btn_search.text = "搜索" + btn_search.clickable = True + btn_search.boundsInScreen = Rect(640, 1000, 1280, 1080) + nav.addChild(btn_search) + + btn_profile = AccessibilityNodeInfo() + btn_profile.className = "android.widget.Button" + btn_profile.text = "我的" + btn_profile.clickable = True + btn_profile.boundsInScreen = Rect(1280, 1000, 1920, 1080) + nav.addChild(btn_profile) + + def getRoot(self) -> AccessibilityNodeInfo: + return self._root + + def findByText(self, text: str) -> Optional[AccessibilityNodeInfo]: + return self._root.findByText(text) + + def findById(self, id_: str) -> Optional[AccessibilityNodeInfo]: + return self._root.findById(id_) + + def refresh(self) -> bool: + self._build_fake_tree() + return True + + def getSystemWindows(self) -> List: + return [self._root] + + def getWindows(self) -> List: + return [self._root] + + +# ============================================================ +# android.hardware services +# ============================================================ + +class SensorManager: + """模拟 android.hardware.SensorManager""" + TYPE_ACCELEROMETER = 1 + TYPE_GYROSCOPE = 4 + TYPE_LIGHT = 5 + TYPE_PRESSURE = 6 + TYPE_PROXIMITY = 8 + TYPE_MAGNETIC_FIELD = 2 + + def getDefaultSensor(self, type_: int): + return Sensor(type_) + + def getSensorList(self, type_: int) -> List: + if type_ == -1: + return [Sensor(t) for t in range(1, 13)] + return [Sensor(type_)] if type_ >= 1 else [] + + def registerListener(self, listener, sensor, rate: int = 3) -> bool: + return True + + def unregisterListener(self, listener, sensor=None) -> None: + pass + + +class Sensor: + def __init__(self, type_: int): + self.type = type_ + self.name = { + 1: "Accelerometer", 4: "Gyroscope", 5: "Light", + 6: "Pressure", 8: "Proximity", 2: "Magnetic Field" + }.get(type_, f"Sensor_{type_}") + self.vendor = "OperitCompat" + self.version = 1 + self.resolution = 0.01 + self.maximumRange = 100.0 + self.minDelay = 10000 + self.power = 0.1 + + +class SensorEvent: + def __init__(self, sensor: Sensor, values: List[float]): + self.sensor = sensor + self.values = values + self.timestamp = int(time.time() * 1000000) + self.accuracy = 3 + + +class CameraManager: + """模拟 android.hardware.camera2.CameraManager""" + def getCameraIdList(self) -> List[str]: + return ["0"] + + def getCameraCharacteristics(self, cameraId: str) -> "CameraCharacteristics": + return CameraCharacteristics() + + def openCamera(self, cameraId: str, stateCallback, handler) -> None: + pass + + +class CameraCharacteristics: + def get(self, key: Any) -> Any: + return None + + +# ============================================================ +# android.location +# ============================================================ + +class LocationManager: + """模拟 android.location.LocationManager""" + GPS_PROVIDER = "gps" + NETWORK_PROVIDER = "network" + PASSIVE_PROVIDER = "passive" + + def getLastKnownLocation(self, provider: str) -> Optional["Location"]: + return self._get_fake_location() + + def getCurrentLocation(self, provider: str, result: Any = None, executor: Any = None) -> Optional["Location"]: + return self._get_fake_location() + + def requestLocationUpdates(self, provider: str, minTimeMs: int, + minDistanceM: float, listener: Any) -> None: + pass + + def removeUpdates(self, listener: Any) -> None: + pass + + def isProviderEnabled(self, provider: str) -> bool: + return True + + def getAllProviders(self) -> List[str]: + return [self.GPS_PROVIDER, self.NETWORK_PROVIDER, self.PASSIVE_PROVIDER] + + def _get_fake_location(self) -> "Location": + loc = Location("gps") + loc._latitude = 39.9042 + random.random() * 0.01 + loc._longitude = 116.4074 + random.random() * 0.01 + loc._altitude = 43.0 + loc._accuracy = 10.0 + loc._time = int(time.time()) + loc._speed = 0.0 + loc._bearing = 0.0 + return loc + + +class Location: + def __init__(self, provider: str = "gps"): + self._provider = provider + self._latitude = 0.0 + self._longitude = 0.0 + self._altitude = 0.0 + self._accuracy = 0.0 + self._time = 0 + self._speed = 0.0 + self._bearing = 0.0 + + def getLatitude(self) -> float: return self._latitude + def getLongitude(self) -> float: return self._longitude + def getAltitude(self) -> float: return self._altitude + def getAccuracy(self) -> float: return self._accuracy + def getTime(self) -> int: return self._time + def getSpeed(self) -> float: return self._speed + def getBearing(self) -> float: return self._bearing + def getProvider(self) -> str: return self._provider + def distanceTo(self, dest: "Location") -> float: + """Haversine 公式计算距离""" + lat1, lon1 = math.radians(self._latitude), math.radians(self._longitude) + lat2, lon2 = math.radians(dest._latitude), math.radians(dest._longitude) + dlat, dlon = lat2 - lat1, lon2 - lon1 + a = math.sin(dlat/2)**2 + math.cos(lat1)*math.cos(lat2)*math.sin(dlon/2)**2 + return 2 * 6371000 * math.asin(math.sqrt(a)) + + +class Geocoder: + """模拟 android.location.Geocoder""" + @staticmethod + def getFromLocation(latitude: float, longitude: float, maxResults: int) -> List["Address"]: + return [Address(latitude, longitude)] + + @staticmethod + def getFromLocationName(locationName: str, maxResults: int) -> List["Address"]: + return [Address(39.9042, 116.4074)] + + +class Address: + def __init__(self, lat: float, lng: float): + self.featureName = "" + self.thoroughfare = "" + self.locality = "Beijing" + self.adminArea = "Beijing" + self.countryName = "China" + self.countryCode = "CN" + self.postalCode = "100000" + self.subAdminArea = "" + self.subLocality = "" + self.latitude = lat + self.longitude = lng + + def getMaxAddressLineIndex(self) -> int: + return 1 + + def getAddressLine(self, index: int) -> str: + return f"{self.locality}, {self.adminArea}, {self.countryName}" + + def getLocality(self) -> str: return self.locality + def getAdminArea(self) -> str: return self.adminArea + def getCountryName(self) -> str: return self.countryName + + +# ============================================================ +# android.media +# ============================================================ + +class MediaPlayer: + """模拟 android.media.MediaPlayer""" + def __init__(self): + self._source = None + self._volume = 1.0 + self._looping = False + + def setDataSource(self, path_or_uri) -> None: self._source = path_or_uri + def setVolume(self, left: float, right: float) -> None: self._volume = (left + right) / 2 + def setLooping(self, looping: bool) -> None: self._looping = looping + def prepare(self) -> None: pass + def prepareAsync(self) -> None: pass + def start(self) -> None: + console.log(f"[MediaPlayer] 模拟播放: {self._source}") + def stop(self) -> None: pass + def pause(self) -> None: pass + def release(self) -> None: pass + def isPlaying(self) -> bool: return False + def getDuration(self) -> int: return 0 + def getCurrentPosition(self) -> int: return 0 + def seekTo(self, msec: int) -> None: pass + def reset(self) -> None: pass + + @staticmethod + def create(context: Context, uri: Any) -> "MediaPlayer": + return MediaPlayer() + + +class AudioRecord: + """模拟 android.media.AudioRecord""" + def __init__(self, audioSource, sampleRate, channelConfig, audioFormat, bufferSize): + pass + def startRecording(self) -> None: pass + def stop(self) -> None: pass + def read(self, audioData, offsetInBytes, sizeInBytes) -> int: + return sizeInBytes + def release(self) -> None: pass + def getRecordingState(self) -> int: return 3 # RECORDSTATE_RECORDING + def getState(self) -> int: return 1 # STATE_INITIALIZED + + +class AudioManager: + """模拟 android.media.AudioManager""" + RINGER_MODE_NORMAL = 2 + RINGER_MODE_SILENT = 0 + RINGER_MODE_VIBRATE = 1 + STREAM_MUSIC = 3 + STREAM_RING = 2 + STREAM_ALARM = 4 + STREAM_NOTIFICATION = 5 + + def getStreamVolume(self, streamType: int) -> int: + return 7 + def setStreamVolume(self, streamType: int, volume: int, flags: int) -> None: pass + def getRingerMode(self) -> int: return self.RINGER_MODE_NORMAL + def setRingerMode(self, mode: int) -> None: pass + def adjustVolume(self, direction: int, flags: int) -> None: pass + def isMusicActive(self) -> bool: return False + + +# ============================================================ +# android.telephony +# ============================================================ + +class TelephonyManager: + """模拟 android.telephony.TelephonyManager""" + def getDeviceId(self) -> str: return "000000000000000" + def getImei(self) -> str: return "000000000000000" + def getSimSerialNumber(self) -> str: return "000000000000000" + def getNetworkOperatorName(self) -> str: return "Network" + def getNetworkCountryIso(self) -> str: return "CN" + def getPhoneType(self) -> int: return 0 # PHONE_TYPE_NONE + def getDataState(self) -> int: return 2 # DATA_CONNECTED + def getCallState(self) -> int: return 0 # CALL_STATE_IDLE + def getSimState(self) -> int: return 5 # SIM_STATE_READY + def hasIccCard(self) -> bool: return False + def isNetworkRoaming(self) -> bool: return False + def getNetworkType(self) -> int: return 13 # NETWORK_TYPE_LTE + def getSignalStrength(self) -> int: return 4 + def listen(self, listener, events: int) -> None: pass + + +# ============================================================ +# android.provider 合约 +# ============================================================ + +class Settings: + """模拟 android.provider.Settings""" + + class System: + @staticmethod + def getString(cr: ContentResolver, name: str) -> str: + return "" + @staticmethod + def putString(cr: ContentResolver, name: str, value: str) -> bool: + return True + @staticmethod + def getInt(cr: ContentResolver, name: str, default: int = 0) -> int: + return default + + class Secure: + ANDROID_ID = "android_id" + @staticmethod + def getString(cr: ContentResolver, name: str) -> str: + return hashlib.md5(b"operit_compat").hexdigest()[:16] + + class Global: + @staticmethod + def getString(cr: ContentResolver, name: str) -> str: + return "" + @staticmethod + def putString(cr: ContentResolver, name: str, value: str) -> bool: + return True + + +class MediaStore: + """模拟 android.provider.MediaStore""" + + class Images: + class Media: + EXTERNAL_CONTENT_URI = Uri.parse("content://media/external/images/media") + INTERNAL_CONTENT_URI = Uri.parse("content://media/internal/images/media") + DATA = "_data" + DISPLAY_NAME = "_display_name" + SIZE = "_size" + MIME_TYPE = "mime_type" + TITLE = "title" + DATE_ADDED = "date_added" + DATE_MODIFIED = "date_modified" + WIDTH = "width" + HEIGHT = "height" + ORIENTATION = "orientation" + BUCKET_DISPLAY_NAME = "bucket_display_name" + BUCKET_ID = "bucket_id" + + class Video: + class Media: + EXTERNAL_CONTENT_URI = Uri.parse("content://media/external/video/media") + DATA = "_data" + DISPLAY_NAME = "_display_name" + SIZE = "_size" + TITLE = "title" + DURATION = "duration" + DATE_ADDED = "date_added" + + class Audio: + class Media: + EXTERNAL_CONTENT_URI = Uri.parse("content://media/external/audio/media") + DATA = "_data" + TITLE = "title" + ARTIST = "artist" + ALBUM = "album" + DURATION = "duration" + + class Files: + class Media: + EXTERNAL_CONTENT_URI = Uri.parse("content://media/external/files/media") + + +class CalendarContract: + """模拟 android.provider.CalendarContract""" + + class Events: + CONTENT_URI = Uri.parse("content://com.android.calendar/events") + TITLE = "title" + DESCRIPTION = "description" + DTSTART = "dtstart" + DTEND = "dtend" + ALL_DAY = "allDay" + EVENT_TIMEZONE = "eventTimezone" + CALENDAR_ID = "calendar_id" + RRULE = "rrule" + DURATION = "duration" + + +class ContactsContract: + """模拟 android.provider.ContactsContract""" + + class Contacts: + CONTENT_URI = Uri.parse("content://com.android.contacts/contacts") + DISPLAY_NAME = "display_name" + HAS_PHONE_NUMBER = "has_phone_number" + _ID = "_id" + + class CommonDataKinds: + class Phone: + CONTENT_URI = Uri.parse("content://com.android.contacts/data/phones") + NUMBER = "data1" + TYPE = "data2" + LABEL = "data3" + TYPE_HOME = 1 + TYPE_MOBILE = 2 + TYPE_WORK = 3 + + class Email: + CONTENT_URI = Uri.parse("content://com.android.contacts/data/emails") + ADDRESS = "data1" + TYPE = "data2" + TYPE_HOME = 1 + TYPE_WORK = 2 + + +# ============================================================ +# android.os.BatteryManager +# ============================================================ + +class BatteryManager: + """模拟 android.os.BatteryManager""" + BATTERY_PROPERTY_CAPACITY = 0 + BATTERY_PROPERTY_STATUS = 1 + STATUS_CHARGING = 2 + STATUS_DISCHARGING = 3 + STATUS_FULL = 5 + STATUS_NOT_CHARGING = 4 + + def getIntProperty(self, id_: int) -> int: + if id_ == self.BATTERY_PROPERTY_CAPACITY: + return 85 + return self.STATUS_DISCHARGING + + def getLongProperty(self, id_: int) -> int: + return 0 + + +# ============================================================ +# android.net services +# ============================================================ + +class ConnectivityManager: + """模拟 android.net.ConnectivityManager""" + TYPE_WIFI = 1 + TYPE_MOBILE = 0 + TYPE_ETHERNET = 9 + TYPE_BLUETOOTH = 7 + + def getActiveNetworkInfo(self) -> Optional["NetworkInfo"]: + return NetworkInfo() + + def getAllNetworkInfo(self) -> List["NetworkInfo"]: + return [NetworkInfo()] + + def isActiveNetworkMetered(self) -> bool: + return False + + def getNetworkCapabilities(self, network) -> Optional["NetworkCapabilities"]: + return NetworkCapabilities() + + +class NetworkInfo: + def __init__(self): + self._type = ConnectivityManager.TYPE_ETHERNET + self._subtype = 0 + self._isConnected = True + self._isAvailable = True + self._typeName = "ETHERNET" + self._subtypeName = "" + self._state = "CONNECTED" + self._reason = "" + + def getType(self) -> int: return self._type + def getSubtype(self) -> int: return self._subtype + def isConnected(self) -> bool: return self._isConnected + def isConnectedOrConnecting(self) -> bool: return True + def isAvailable(self) -> bool: return self._isAvailable + def getTypeName(self) -> str: return self._typeName + def getState(self) -> str: return self._state + def getExtraInfo(self) -> str: return "" + + +class NetworkCapabilities: + def hasCapability(self, cap: int) -> bool: + return True + def hasTransport(self, transport: int) -> bool: + return True + + +class WifiManager: + """模拟 android.net.wifi.WifiManager""" + def getConnectionInfo(self) -> "WifiInfo": + return WifiInfo() + + def startScan(self) -> bool: + return True + + def getScanResults(self) -> List: + return [] + + def isWifiEnabled(self) -> bool: + return True + + def setWifiEnabled(self, enabled: bool) -> bool: + return True + + def isWifiEnabled(self) -> bool: + return True + + +class WifiInfo: + def __init__(self): + self._ssid = "OperitCompat_Network" + self._bssid = "00:00:00:00:00:00" + self._rssi = -50 + self._linkSpeed = 300 + self._ipAddress = int.from_bytes(socket.inet_aton("192.168.1.100"), "big") + self._networkId = 1 + + 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._linkSpeed + def getIpAddress(self) -> int: return self._ipAddress + def getNetworkId(self) -> int: return self._networkId + def getSupplicantState(self) -> str: return "COMPLETED" + + def getMacAddress(self) -> str: + import uuid + return ":".join(f"{(uuid.getnode() >> (8 * i)) & 0xFF:02X}" for i in range(6)) + + +# ============================================================ +# android.os.storage +# ============================================================ + +class StorageManager: + """模拟 android.os.storage.StorageManager""" + def getStorageVolumes(self) -> List: + volumes = [] + vol = StorageVolume("Internal shared storage", _sdcard_root, True, False) + volumes.append(vol) + return volumes + + def getPrimaryStorageVolume(self) -> "StorageVolume": + return StorageVolume("Internal shared storage", _sdcard_root, True, False) + + def getVolumeList(self) -> List: + return self.getStorageVolumes() + + +class StorageVolume: + def __init__(self, desc: str, path: str, primary: bool, removable: bool): + self._description = desc + self._path = path + self._primary = primary + self._removable = removable + self._state = "mounted" + self._uuid = hashlib.md5(path.encode()).hexdigest() + self._maxFileSize = 4 * 1024 * 1024 * 1024 * 1024 # 4TB + + def getDescription(self, context: Context = None) -> str: return self._description + def getPath(self) -> str: return self._path + def isPrimary(self) -> bool: return self._primary + def isRemovable(self) -> bool: return self._removable + def getState(self) -> str: return self._state + def getUuid(self) -> str: return self._uuid + def getMaxFileSize(self) -> int: return self._maxFileSize + def getUserLabel(self) -> str: return os.path.basename(self._path) + + +# ============================================================ +# android.view services +# ============================================================ + +class WindowManager: + """模拟 android.view.WindowManager""" + def getDefaultDisplay(self) -> "Display": + return self._get_display() + + def _get_display(self) -> "Display": + w, h = 1920, 1080 + if HAS_SCREENINFO: + try: + monitor = screeninfo.get_monitors()[0] + w, h = monitor.width, monitor.height + except Exception: + pass + return Display(w, h) + + def getCurrentWindowMetrics(self) -> "WindowMetrics": + display = self._get_display() + return WindowMetrics(Rect(0, 0, display._width, display._height)) + + +class Display: + def __init__(self, w: int = 1920, h: int = 1080): + self._width = w + self._height = h + self._refreshRate = 60.0 + self._density = 2.0 + self._densityDpi = 320 + self._rotation = 0 + + def getWidth(self) -> int: return self._width + def getHeight(self) -> int: return self._height + def getRefreshRate(self) -> float: return self._refreshRate + def getRotation(self) -> int: return self._rotation + def getRealSize(self, size: "Point") -> None: + size.x, size.y = self._width, self._height + def getSize(self, size: "Point") -> None: + size.x, size.y = self._width, self._height + + class Metrics: + def __init__(self): + self.widthPixels = 1080 + self.heightPixels = 1920 + self.density = 2.0 + self.densityDpi = 320 + self.scaledDensity = 2.0 + self.xdpi = 320.0 + self.ydpi = 320.0 + + def getMetrics(self, metrics: Metrics) -> None: + metrics.widthPixels = self._width + metrics.heightPixels = self._height + metrics.density = self._density + metrics.densityDpi = self._densityDpi + + +class WindowMetrics: + def __init__(self, bounds: Rect): + self._bounds = bounds + + def getBounds(self) -> Rect: + return self._bounds + + +class Point: + def __init__(self, x: int = 0, y: int = 0): + self.x = x + self.y = y + + def set(self, x: int, y: int) -> None: + self.x = x + self.y = y + + +class DisplayManager: + def getDisplay(self, displayId: int) -> Optional[Display]: + return Display() + + def getDisplays(self) -> List[Display]: + return [Display()] + + def getDisplayByName(self, name: str) -> Optional[Display]: + return Display() + + +class InputManager: + """模拟 android.hardware.input.InputManager""" + def injectInputEvent(self, event: Any, mode: int) -> bool: + return True + + +class LayoutInflater: + def inflate(self, resource, root: View) -> View: + return View() + + +class ClipboardManager: + """模拟 android.content.ClipboardManager""" + + def __init__(self): + self._text = "" + + def setText(self, text: str) -> None: + self._text = text + import pyperclip as _pc + _pc.copy(text) + + def getText(self) -> str: + try: + import pyperclip as _pc + return _pc.paste() + except Exception: + return self._text + + def hasText(self) -> bool: + return bool(self.getText()) + + +class DownloadManager: + """模拟 android.app.DownloadManager""" + def enqueue(self, request: "DownloadManager.Request") -> int: + return int(time.time()) + + def query(self, query: "DownloadManager.Query") -> Cursor: + return Cursor() + + class Request: + def __init__(self, uri: Uri): + self._uri = uri + self._title = "" + self._description = "" + self._destination = "" + self._allowedOverMetered = False + self._allowedOverRoaming = False + + def setTitle(self, title: str) -> None: self._title = title + def setDescription(self, desc: str) -> None: self._description = desc + def setDestinationInExternalFilesDir(self, context: Context, dirType: str, subPath: str) -> None: + self._destination = os.path.join(context.getExternalFilesDir(dirType), subPath) + def setAllowedOverMetered(self, allow: bool) -> None: self._allowedOverMetered = allow + def setAllowedOverRoaming(self, allow: bool) -> None: self._allowedOverRoaming = allow + + class Query: + def setFilterById(self, *ids: int) -> None: pass + + +class Toast: + """模拟 android.widget.Toast""" + LENGTH_SHORT = 0 + LENGTH_LONG = 1 + + @staticmethod + def makeText(context: Context, text: str, duration: int) -> "Toast": + return Toast(text) + + def __init__(self, text: str): + self._text = text + + def show(self) -> None: + console.log(f"[Toast] {self._text}") + + +# ============================================================ +# android.print +# ============================================================ + +class PrintManager: + def print(self, jobName: str, documentAdapter: Any, attributes: Any = None) -> bool: + return True + + +# ============================================================ +# android.accounts +# ============================================================ + +class AccountManager: + """模拟 android.accounts.AccountManager""" + def getAccounts(self) -> List: + return [Account("user@operit.com", "com.google")] + + def getAccountsByType(self, type_: str) -> List: + return [Account("user@operit.com", type_)] + + def addAccount(self, accountType: str, authTokenType: str, requiredFeatures: List, + addAccountAuthTokenType: str, options: Dict, activity: Any, + callback: Callable, handler: Any) -> "AccountManagerFuture": + return AccountManagerFuture() + + +class Account: + def __init__(self, name: str, type_: str): + self.name = name + self.type = type_ + + def toString(self) -> str: + return f"Account{{name={self.name}, type={self.type}}}" + + +class AccountManagerFuture: + def getResult(self) -> Any: + return None + + +# ============================================================ +# android.hardware.biometrics +# ============================================================ + +class BiometricPrompt: + """模拟 android.hardware.biometrics.BiometricPrompt""" + BIOMETRIC_STRONG = 15 + BIOMETRIC_WEAK = 255 + + def authenticate(self, crypto: Any = None, cancel: Any = None) -> None: + console.log("[BiometricPrompt] 模拟生物识别认证") + if self._callback: + self._callback.onAuthenticationSucceeded( + BiometricPrompt.AuthenticationResult() + ) + + def setCallback(self, callback: "AuthenticationCallback") -> None: + self._callback = callback + + class AuthenticationResult: + def getAuthenticationType(self) -> int: + return BiometricPrompt.BIOMETRIC_STRONG + + class AuthenticationCallback: + def onAuthenticationError(self, errorCode: int, errString: str) -> None: pass + def onAuthenticationSucceeded(self, result: "AuthenticationResult") -> None: pass + def onAuthenticationFailed(self) -> None: pass + + +# ============================================================ +# DisplayMetrics 辅助类 +# ============================================================ + +class DisplayMetrics: + """辅助类""" + def __init__(self): + self.widthPixels = 1080 + self.heightPixels = 1920 + self.density = 2.0 + self.densityDpi = 320 + self.scaledDensity = 2.0 + self.xdpi = 320.0 + self.ydpi = 320.0 + + +# ============================================================ +# java.io 兼容 +# ============================================================ + +class File: + """跨平台文件操作""" + separator = os.sep + + def __init__(self, *args): + self._path = "" + if len(args) == 1: + self._path = self._normalize(args[0]) + elif len(args) >= 2: + self._path = self._normalize(os.path.join(args[0], *args[1:])) + + @staticmethod + def _normalize(p: str) -> str: + if p.startswith("/sdcard"): + return os.path.join(_sdcard_root, p[7:].lstrip("/")) + return p + + def getAbsolutePath(self) -> str: + return os.path.abspath(self._path) + + def getName(self) -> str: + return os.path.basename(self._path) + + def getParent(self) -> str: + return os.path.dirname(self._path) + + def exists(self) -> bool: + return os.path.exists(self._path) + + def isFile(self) -> bool: + return os.path.isfile(self._path) + + def isDirectory(self) -> bool: + return os.path.isdir(self._path) + + def length(self) -> int: + return os.path.getsize(self._path) if os.path.isfile(self._path) else 0 + + def list(self) -> List[str]: + return os.listdir(self._path) if os.path.isdir(self._path) else [] + + def listFiles(self) -> List["File"]: + return [File(p) for p in (os.listdir(self._path) if os.path.isdir(self._path) else [])] + + def mkdir(self) -> bool: + try: + os.mkdir(self._path) + return True + except Exception: + return False + + def mkdirs(self) -> bool: + try: + os.makedirs(self._path, exist_ok=True) + return True + except Exception: + return False + + def delete(self) -> bool: + try: + if os.path.isfile(self._path): + os.remove(self._path) + elif os.path.isdir(self._path): + os.rmdir(self._path) + return True + except Exception: + return False + + def renameTo(self, dest: "File") -> bool: + try: + os.rename(self._path, dest._path) + return True + except Exception: + return False + + def lastModified(self) -> int: + return int(os.path.getmtime(self._path) * 1000) if os.path.exists(self._path) else 0 + + def canRead(self) -> bool: + return os.access(self._path, os.R_OK) + + def canWrite(self) -> bool: + return os.access(self._path, os.W_OK) + + def canExecute(self) -> bool: + return os.access(self._path, os.X_OK) + + def getFreeSpace(self) -> int: + stat = os.statvfs(self._path) if hasattr(os, "statvfs") else None + if stat: + return stat.f_bavail * stat.f_frsize + return 1024 * 1024 * 1024 * 50 # 50GB + + def getTotalSpace(self) -> int: + return 1024 * 1024 * 1024 * 256 # 256GB + + def toPath(self) -> str: + return self._path + + def toURI(self) -> str: + return Uri.fromFile(self._path).toString() + + def __str__(self): + return self._path + + @staticmethod + def createTempFile(prefix: str, suffix: str) -> "File": + fd, path = tempfile.mkstemp(suffix=suffix, prefix=prefix) + os.close(fd) + return File(path) + + +class FileInputStream: + def __init__(self, file_or_path): + if isinstance(file_or_path, File): + self._path = file_or_path._path + else: + self._path = str(file_or_path) + self._file = open(self._path, "rb") + + def read(self, length: int = -1) -> bytes: + return self._file.read(length) if length >= 0 else self._file.read() + + def available(self) -> int: + import stat + return os.stat(self._path).st_size - self._file.tell() + + def close(self) -> None: + self._file.close() + + +class FileOutputStream: + def __init__(self, file_or_path, append: bool = False): + if isinstance(file_or_path, File): + self._path = file_or_path._path + else: + self._path = str(file_or_path) + self._file = open(self._path, "ab" if append else "wb") + + def write(self, data) -> None: + if isinstance(data, bytes): + self._file.write(data) + elif isinstance(data, str): + self._file.write(data.encode("utf-8")) + else: + self._file.write(bytes([data])) + + def flush(self) -> None: + self._file.flush() + + def close(self) -> None: + self._file.close() + + +class BufferedReader: + def __init__(self, reader, buffer_size: int = 8192): + self._reader = reader + self._buffer = "" + + def readLine(self) -> Optional[str]: + lines = self._reader.read().split("\n") + if lines: + return lines[0] + return None + + def read(self) -> str: + return self._reader.read() + + def close(self) -> None: + self._reader.close() + + +class InputStreamReader: + def __init__(self, stream, charset: str = "UTF-8"): + self._stream = stream + self._charset = charset + + def read(self, length: int = -1) -> str: + data = self._stream.read(length) + return data.decode(self._charset) if isinstance(data, bytes) else data + + def close(self) -> None: + self._stream.close() + + +class OutputStreamWriter: + def __init__(self, stream, charset: str = "UTF-8"): + self._stream = stream + self._charset = charset + + def write(self, text: str) -> None: + self._stream.write(text.encode(self._charset)) + + def flush(self) -> None: + self._stream.flush() + + def close(self) -> None: + self._stream.close() + + +class BufferedWriter: + def __init__(self, writer, buffer_size: int = 8192): + self._writer = writer + self._buffer = "" + + def write(self, text: str) -> None: + self._writer.write(text) + + def newLine(self) -> None: + self._writer.write("\n") + + def flush(self) -> None: + self._writer.flush() + + def close(self) -> None: + self._writer.close() + + +# ============================================================ +# 通用辅助工具 +# ============================================================ + +class Log: + """android.util.Log""" + ASSERT = 7 + DEBUG = 3 + ERROR = 6 + INFO = 4 + VERBOSE = 2 + WARN = 5 + + @staticmethod + def d(tag: str, msg: str) -> int: + print(f"DEBUG/{tag}: {msg}") + return 0 + + @staticmethod + def e(tag: str, msg: str) -> int: + print(f"ERROR/{tag}: {msg}", file=sys.stderr) + return 0 + + @staticmethod + def i(tag: str, msg: str) -> int: + print(f"INFO/{tag}: {msg}") + return 0 + + @staticmethod + def w(tag: str, msg: str) -> int: + print(f"WARN/{tag}: {msg}") + return 0 + + @staticmethod + def v(tag: str, msg: str) -> int: + print(f"VERBOSE/{tag}: {msg}") + return 0 + + @staticmethod + def wtf(tag: str, msg: str) -> int: + print(f"WTF/{tag}: {msg}", file=sys.stderr) + return 0 + + +class Bundle: + """android.os.Bundle""" + def __init__(self): + self._data: Dict[str, Any] = {} + + def getString(self, key: str, default: str = None) -> Optional[str]: + return str(self._data.get(key, default)) if default or key in self._data else self._data.get(key) + + def getInt(self, key: str, default: int = 0) -> int: + return int(self._data.get(key, default)) + + def getBoolean(self, key: str, default: bool = False) -> bool: + return bool(self._data.get(key, default)) + + def putString(self, key: str, value: str) -> None: + self._data[key] = value + + def putInt(self, key: str, value: int) -> None: + self._data[key] = value + + def putBoolean(self, key: str, value: bool) -> None: + self._data[key] = value + + def containsKey(self, key: str) -> bool: + return key in self._data + + def isEmpty(self) -> bool: + return len(self._data) == 0 + + def size(self) -> int: + return len(self._data) + + def keySet(self) -> set: + return set(self._data.keys()) + + +class Handler: + """android.os.Handler""" + def __init__(self, callback: Callable = None): + self._callback = callback + + def post(self, action: Callable) -> bool: + action() + return True + + def postDelayed(self, action: Callable, delayMs: int) -> bool: + import threading + timer = threading.Timer(delayMs / 1000, action) + timer.start() + return True + + def removeCallbacks(self, action: Callable = None) -> None: + pass + + def obtainMessage(self, what: int = 0, arg1: int = 0, arg2: int = 0, obj: Any = None) -> "Message": + return Message(what, arg1, arg2, obj) + + +class Message: + def __init__(self, what: int = 0, arg1: int = 0, arg2: int = 0, obj: Any = None): + self.what = what + self.arg1 = arg1 + self.arg2 = arg2 + self.obj = obj + + def sendToTarget(self) -> None: + pass + + +class Looper: + """android.os.Looper""" + @staticmethod + def getMainLooper() -> "Looper": + return Looper() + + @staticmethod + def myLooper() -> Optional["Looper"]: + return Looper() + + @staticmethod + def prepare() -> None: + pass + + @staticmethod + def loop() -> None: + pass + + def quit(self) -> None: + pass + + +class ParcelFileDescriptor: + """android.os.ParcelFileDescriptor""" + MODE_READ_ONLY = 0x10000000 + MODE_WRITE_ONLY = 0x20000000 + MODE_READ_WRITE = 0x30000000 + + @staticmethod + def open(file: File, mode: int) -> "ParcelFileDescriptor": + return ParcelFileDescriptor(file) + + def __init__(self, file: File = None): + self._file = file + + def getFileDescriptor(self) -> Any: + return None + + def close(self) -> None: + pass + + @staticmethod + def parseUri(uri: Uri, mode: int) -> "ParcelFileDescriptor": + return ParcelFileDescriptor() + + +# ============================================================ +# AndroidUtil - 常用工具函数桥接 +# ============================================================ + +class AndroidUtil: + """将 Android 路径映射到桌面路径的工具类""" + + @staticmethod + def resolve_path(path: str) -> str: + """解析 Android 路径到桌面路径""" + if path.startswith("/sdcard"): + return os.path.join(_sdcard_root, path[7:].lstrip("/")) + if path.startswith("content://"): + uri = Uri.parse(path) + return uri.getPath() + if path.startswith("file://"): + return urllib.parse.unquote(path[7:]) + return path + + @staticmethod + def to_android_path(desktop_path: str) -> str: + """将桌面路径映射回 Android 路径""" + if desktop_path.startswith(_sdcard_root): + return desktop_path.replace(_sdcard_root, "/sdcard") + return desktop_path + + @staticmethod + def get_app_draw_dir(name: str) -> str: + """获取应用绘图输出目录(桌面版)""" + path = os.path.join(_sdcard_root, "Download", "Operit", "plugins", "draw", name, "draws") + os.makedirs(path, exist_ok=True) + return path + + @staticmethod + def save_image_to_draw_dir(script_name: str, data: bytes, ext: str = "png") -> str: + """保存图片到绘图目录""" + dir_path = AndroidUtil.get_app_draw_dir(script_name) + filename = f"{int(time.time() * 1000)}_{random.randint(1000,9999)}.{ext}" + filepath = os.path.join(dir_path, filename) + with open(filepath, "wb") as f: + f.write(data) + return filepath + + +# ═══════════════════════════════════════════════════════════════ +# GLOBAL INSTANCE - 单例上下文 +# ═══════════════════════════════════════════════════════════════ + +# 全局应用上下文 +application_context = Context(_package_name) + +# 全局 Java.type 桥接(只在 jpype 可用时有效) +_jpype_available = False +try: + import jpype + import jpype.imports + _jpype_available = True +except ImportError: + pass + + +def java_type(class_name: str) -> Any: + """ + 模拟 Java.type() 调用。 + 如果安装了 jpype,则尝试真实加载 Java 类; + 否则返回 AndroidUnsupportedStub。 + """ + if _jpype_available and jpype.isJVMStarted(): + try: + return jpype.JClass(class_name) + except Exception: + pass + return AndroidUnsupportedStub + + +class AndroidUnsupportedStub: + """ + 通用 Android 不支持存根。 + 替换所有无法在桌面端实现的 Java 类。 + 任何方法调用都返回自身,支持链式调用。 + """ + def __init__(self, *args, **kwargs): + self._name = self.__class__.__name__ + + def __getattr__(self, name: str) -> "AndroidUnsupportedStub": + if name.startswith("_"): + raise AttributeError(name) + return AndroidUnsupportedStub() + + def __call__(self, *args, **kwargs) -> "AndroidUnsupportedStub": + return self + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return f"[Android-only]" + + # 模拟常见返回类型 + def toString(self) -> str: + return f"[Android-only: {self._name}]" + + def intValue(self) -> int: + return 0 + + def longValue(self) -> int: + return 0 + + def booleanValue(self) -> bool: + return False + + def doubleValue(self) -> float: + return 0.0 + + def get(self, *args) -> "AndroidUnsupportedStub": + return self + + def length(self) -> int: + return 0 + + def size(self) -> int: + return 0 + + def isEmpty(self) -> bool: + return True + + def close(self) -> None: + pass + + def release(self) -> None: + pass + + def recycle(self) -> None: + pass + + +# 全局别名 - 让脚本中的 `Java.type("xxx")` 可以直接调用 +Java = type("Java", (), {"type": staticmethod(java_type)}) + + +# ═══════════════════════════════════════════════════════════════ +# 导出给 operit_runtime.py 的重要对象 +# ═══════════════════════════════════════════════════════════════ + +def get_android_compat_globals() -> dict: + """返回要注入到脚本命名空间的 Android 兼容对象""" + return { + # android 核心包 + "Context": Context, + "Intent": Intent, + "IntentAction": IntentAction, + "BroadcastReceiver": BroadcastReceiver, + "IntentFilter": IntentFilter, + "ContentValues": ContentValues, + "ContentResolver": ContentResolver, + "Cursor": Cursor, + "Uri": Uri, + "Uri_parse": Uri.parse, + "Uri_fromFile": Uri.fromFile, + + # android.os + "Build": Build, + "Environment": Environment, + "BatteryManager": BatteryManager, + "PowerManager": PowerManager, + "WakeLock": WakeLock, + "VibrationEffect": VibrationEffect, + "Vibrator": Vibrator, + "Bundle": Bundle, + "Handler": Handler, + "Message": Message, + "Looper": Looper, + "ParcelFileDescriptor": ParcelFileDescriptor, + + # android.graphics + "Bitmap": Bitmap, + "BitmapFactory": BitmapFactory, + "Bitmap_Config": Bitmap.Config, + "Canvas": Canvas, + "Color": Color, + "Paint": Paint, + "Rect": Rect, + "Matrix": Matrix, + "Typeface": Typeface, + + # android.graphics.pdf + "PdfRenderer": PdfRenderer, + + # android.app + "ActivityManager": ActivityManager, + "NotificationManager": NotificationManager, + "NotificationChannel": NotificationChannel, + "AlarmManager": AlarmManager, + "DownloadManager": DownloadManager, + + # android.view + "View": View, + "TextView": TextView, + "EditText": EditText, + "Button": Button, + "ImageView": ImageView, + "ScrollView": ScrollView, + "RecyclerView": RecyclerView, + "ViewGroup": ViewGroup, + "LinearLayout": LinearLayout, + "FrameLayout": FrameLayout, + "RelativeLayout": RelativeLayout, + "WindowManager": WindowManager, + "Display": Display, + "DisplayMetrics": DisplayMetrics, + "Point": Point, + "InputManager": InputManager, + "LayoutInflater": LayoutInflater, + "ClipboardManager": ClipboardManager, + "WindowMetrics": WindowMetrics, + + # android.content.pm + "PackageManager": PackageManager, + "PackageInfo": PackageInfo, + "ResolveInfo": ResolveInfo, + "ActivityInfo": ActivityInfo, + "ApplicationInfo": ApplicationInfo, + "SharedPreferences": SharedPreferences, + + # android.provider + "Settings": Settings, + "MediaStore": MediaStore, + "CalendarContract": CalendarContract, + "ContactsContract": ContactsContract, + + # android.location + "LocationManager": LocationManager, + "Location": Location, + "Geocoder": Geocoder, + + # android.media + "MediaPlayer": MediaPlayer, + "AudioRecord": AudioRecord, + "AudioManager": AudioManager, + + # android.hardware + "SensorManager": SensorManager, + "Sensor": Sensor, + "SensorEvent": SensorEvent, + "CameraManager": CameraManager, + "BiometricPrompt": BiometricPrompt, + + # android.net + "ConnectivityManager": ConnectivityManager, + "NetworkInfo": NetworkInfo, + "NetworkCapabilities": NetworkCapabilities, + "WifiManager": WifiManager, + "WifiInfo": WifiInfo, + + # android.os.storage + "StorageManager": StorageManager, + "StorageVolume": StorageVolume, + + # android.telephony + "TelephonyManager": TelephonyManager, + + # android.widget + "Toast": Toast, + + # android.print + "PrintManager": PrintManager, + + # android.accounts + "AccountManager": AccountManager, + "Account": Account, + + # android.util + "Log": Log, + + # java.io + "File": File, + "FileInputStream": FileInputStream, + "FileOutputStream": FileOutputStream, + "BufferedReader": BufferedReader, + "BufferedWriter": BufferedWriter, + "InputStreamReader": InputStreamReader, + "OutputStreamWriter": OutputStreamWriter, + + # 辅助工具 + "AndroidUtil": AndroidUtil, + "AndroidUnsupportedStub": AndroidUnsupportedStub, + + # Java 桥接 + "Java": Java, + + # UINode 自动化 + "UINode": UINode, + "AccessibilityNodeInfo": AccessibilityNodeInfo, + "AccessibilityPage": AccessibilityPage, + + # 运行时对象 + "androidContext": application_context, + + # Android 路径常量 + "SANDBOX_EXTERNAL_PACKAGES_DIR": os.path.join(_sdcard_root, "Android", "data", + _package_name, "files", "packages"), + "SDCARD_ROOT": _sdcard_root, + "EXTERNAL_STORAGE": _sdcard_root, + } \ No newline at end of file diff --git a/operit_runtime.py b/operit_runtime.py index 629257c..492b0ec 100644 --- a/operit_runtime.py +++ b/operit_runtime.py @@ -37,10 +37,19 @@ import zipfile import sqlite3 import traceback import inspect +import importlib from pathlib import Path from datetime import datetime, timezone, timedelta from typing import Any, Dict, List, Optional, Union, Callable +# 导入 Android 兼容层 +_ANDROID_COMPAT_AVAILABLE = False +try: + import android_compat + _ANDROID_COMPAT_AVAILABLE = True +except ImportError: + android_compat = None + # ── 第三方依赖(可选导入) ── try: import requests @@ -1246,6 +1255,22 @@ class OperitScriptLoader: "requests": requests, "httpx": httpx, } + # 注入 Android 兼容层(如果可用) + if _ANDROID_COMPAT_AVAILABLE and android_compat: + android_globals = android_compat.get_android_compat_globals() + script_globals.update(android_globals) + # 特别注入 context 和 service 实例 + script_globals["androidContext"] = android_compat.application_context + else: + # 没有 android_compat 时提供空桩 + script_globals["Java"] = type("Java", (), {"type": staticmethod( + lambda cls: type("Stub", (), { + "__getattr__": lambda s, n: s, + "__call__": lambda s, *a, **kw: s, + "toString": lambda s: "[Android-only]", + })()) + }) + try: with open(script_path, "r", encoding="utf-8") as f: code = f.read() @@ -1417,6 +1442,22 @@ if __name__ == "__main__": print(f" 平台: {platform.system()} {platform.release()}") print(f" Python: {sys.version.split()[0]}") + # Android 兼容层状态 + try: + import android_compat as _ac + ag = _ac.get_android_compat_globals() + print(f" Android兼容层: 已加载 ({len(ag)} 个 API)") + print(f" ├─ Android核心API: Intent, Context, Uri, ContentResolver, 等") + print(f" ├─ Android系统服务: ActivityManager, PackageManager, 等") + print(f" ├─ Android图形API: Bitmap, Canvas, Color, Paint, PdfRenderer") + print(f" ├─ Android视图体系: View, TextView, UINode, AccessibilityNodeInfo") + print(f" ├─ Android媒体/定位/通信: MediaPlayer, LocationManager, TelephonyManager") + print(f" ├─ Java桥接: Java.type() - 直接调用Java类 (需jpype)") + print(f" ├─ java.io兼容: File, FileInputStream, FileOutputStream") + print(f" └─ 路径映射: /sdcard → {_ac._sdcard_root}") + except Exception as _e: + print(f" Android兼容层: 未安装 (android_compat.py 不可用, {_e})") + # 如果脚本目录存在,扫描加载 if os.path.isdir(loader.scripts_dir): result = loader.scan_directory()