新增 android_compat.py (3552行):
全面映射 Android Java API 到 Python 跨平台实现,覆盖 16 个领域、108 个 API 对象:
android.content: Intent(20+actions), Context, ContentResolver, ContentValues,
BroadcastReceiver, IntentFilter, SharedPreferences
android.net: Uri (parse/fromFile/withAppendedPath)
android.os: Build, Environment, PowerManager, BatteryManager, VibrationEffect,
Bundle, Handler, Looper, ParcelFileDescriptor
android.graphics: Bitmap (PIL后端), Canvas, Color, Paint, Rect, Matrix,
Typeface, BitmapFactory
android.graphics.pdf: PdfRenderer (PyMuPDF/pdf2image后端)
android.app: ActivityManager, PackageManager(包查询), NotificationManager,
AlarmManager, DownloadManager
android.view: View, TextView, EditText, Button, ImageView, ScrollView,
ViewGroup, LinearLayout, FrameLayout, RelativeLayout
android.view.accessibility: AccessibilityNodeInfo (UI树+pyautogui点击),
AccessibilityPage, UINode
android.hardware: SensorManager, CameraManager, BiometricPrompt
android.location: LocationManager(模拟GPS), Geocoder, Location(Haversine)
android.media: MediaPlayer, AudioRecord, AudioManager
android.telephony: TelephonyManager
android.provider: Settings, MediaStore, CalendarContract, ContactsContract
android.net.wifi: WifiManager, WifiInfo + ConnectivityManager
android.os.storage: StorageManager, StorageVolume
java.io: File, FileInputStream/OutputStream, BufferedReader, BufferedWriter
Java桥接: Java.type() 支持直调真实Java类(jpype)或返回存根
系统服务: 10+ 模拟服务 (ActivityManager/PowerManager/etc)
路径映射: /sdcard → ~/Android-compat/sdcard
UI自动化: UINode + pyautogui 桌面降级
绘图: Bitmap/Canvas → PIL/Pillow 后端
集成到 operit_runtime.py: 自动检测并注入108个Android API到脚本命名空间
测试结果:
- 语法检查: android_compat.py ✅, operit_runtime.py ✅
- 路径映射: /sdcard → /root/Android-compat/sdcard ✅
- Intent/Uri/Context/Bitmap/File 全API ✅
- 脚本加载: 38脚本 280工具 ✅
3552 lines
111 KiB
Python
3552 lines
111 KiB
Python
"""
|
|
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"<AndroidUnsupportedStub: {self._name}>"
|
|
|
|
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,
|
|
} |