feat: Linux 真实实现层 — 从存根库变为可用生产力工具

新增 _linux_impl.py (800+行):
- LinuxNetwork: 真实IP/接口/DNS/网关/MAC (ip命令+/proc/net)
- LinuxBluetooth: 真实蓝牙状态/扫描/配对/连接 (bluetoothctl)
- LinuxSensors: 真实温度/CPU频率/风扇 (/sys/class/hwmon)
- LinuxAudio: 真实音量控制/静音 (amixer)
- LinuxClipboard: 真实剪贴板读写 (xclip+tkinter回退)
- LinuxPower: 真实CPU/内存/电池 (psutil+/sys/class/power_supply)
- LinuxLocation: 真实IP地理定位 (ip-api.com)
- LinuxNotify: 真实桌面通知 (notify-send)
- LinuxDisplay: 真实屏幕尺寸/截屏/鼠标位置 (xdotool+X11)
- LinuxUIAutomation: 真实点击/输入/按键/滚动 (xdotool)
- LinuxCamera: 真实摄像头拍照 (opencv/V4L2)
- LinuxProcess: 真实进程列表/系统信息/应用管理 (psutil+/proc)
- LinuxShell: 真实命令执行 (subprocess)

改造10+核心类使用Linux真实实现:
- BluetoothAdapter/SensorManager/AudioManager
- ConnectivityManager/WifiManager/TelephonyManager
- LocationManager/PowerManager/BatteryManager/Build
- ActivityManager/Activity/Toast/Display/ClipboardManager
- NotificationManager

15项真实行为测试全部通过:
- 真实IP: 10.60.218.244
- 真实位置: 北京 39.9042, 116.407
- 真实主机名: work-41-vci-g427c
- 真实内核: 5.15.120.byteatom-ck.14
- 真实内存: 1.8GB/3.8GB
- 真实进程列表
This commit is contained in:
shuiyu1123 committed 2026-06-24 15:40:49 +00:00
1 parent 8c3bd3c415
commit 690022cea3
17 files changed
+2083 -8

No files matched your search

File diff suppressed because it is too large. Load diff
+62 -4
View File
@@ -19,6 +19,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
try:
from .._linux_impl import IS_LINUX, LinuxNotify, LinuxShell, LinuxProcess, LinuxDisplay, LinuxUIAutomation, LinuxPower
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Intent
@@ -834,10 +840,16 @@ class NotificationManager(Stub):
def notify(self, id_: int, notification: Notification):
self._notifications[id_] = notification
sys.stderr.write(
f"[NotificationManager] notify id={id_}: "
f"title={notification._title} text={notification._text}\n"
)
if _USE_LINUX:
title = getattr(notification, "_title", "") or ""
text = getattr(notification, "_text", "") or ""
if title or text:
LinuxNotify.send(title, text)
else:
sys.stderr.write(
f"[NotificationManager] notify id={id_}: "
f"title={notification._title} text={notification._text}\n"
)
def notifyAsPackage(self, package_name: str, tag: Optional[str], id_: int, notification: Notification):
self.notify(id_, notification)
@@ -1821,6 +1833,48 @@ class Activity(Stub):
def getTheme(self):
return getattr(self, '_theme', 0)
def getDeviceInfo(self):
"""获取设备信息"""
if _USE_LINUX:
return LinuxProcess.get_system_info()
return {}
def executeShell(self, command):
"""执行 Shell 命令"""
if _USE_LINUX:
return LinuxShell.exec(command)
return {"success": False, "output": "Shell not available"}
def screenshot(self, path=None):
"""截屏"""
if _USE_LINUX:
return LinuxDisplay.screenshot(path)
return None
def getScreenSize(self):
"""获取屏幕尺寸"""
if _USE_LINUX:
return LinuxDisplay.get_screen_size()
return (1920, 1080)
def click(self, x, y):
"""点击屏幕坐标"""
if _USE_LINUX:
return LinuxUIAutomation.click(x, y)
return False
def typeText(self, text):
"""输入文本"""
if _USE_LINUX:
return LinuxUIAutomation.type_text(text)
return False
def pressKey(self, key):
"""按键"""
if _USE_LINUX:
return LinuxUIAutomation.press_key(key)
return False
# ═══════════════════════════════════════════════════════════════
# Service
@@ -2009,9 +2063,13 @@ class ActivityManager(Stub):
self._memory_info = self.MemoryInfo()
def getMemoryInfo(self) -> "ActivityManager.MemoryInfo":
if _USE_LINUX:
return LinuxPower.get_memory_info()
return self._memory_info
def getRunningAppProcesses(self) -> Optional[List["ActivityManager.RunningAppProcessInfo"]]:
if _USE_LINUX:
return LinuxProcess.list_processes()
return []
def killBackgroundProcesses(self, package_name: str):
+15
View File
@@ -13,6 +13,12 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
from .._core import Stub, AndroidOnlyError
try:
from .._linux_impl import IS_LINUX, LinuxProcess, LinuxPower
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
__all__ = [
"TaskManager", "TaskInfo", "TaskStackListener", "TaskSnapshot",
"TaskDescription", "ActivityManager",
@@ -908,9 +914,18 @@ class ActivityManager(Stub):
return list(self._services.values())[:maxNum]
def getRunningAppProcesses(self) -> List[RunningAppProcessInfo]:
if _USE_LINUX:
return LinuxProcess.list_processes()
return list(self._processes.values())
def getMemoryInfo(self, outInfo: MemoryInfo):
if _USE_LINUX:
mem = LinuxPower.get_memory_info()
outInfo.availMem = mem.get("available", 0)
outInfo.totalMem = mem.get("total", 0)
outInfo.threshold = int(mem.get("total", 0) * 0.1)
outInfo.lowMemory = mem.get("percent", 0) > 90.0
return outInfo
import psutil
try:
import psutil
+50
View File
@@ -14,6 +14,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxBluetooth, HAS_BLUETOOTHCTL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# BluetoothProfile
@@ -645,14 +652,20 @@ class BluetoothAdapter(Stub):
return BluetoothAdapter._default_adapter
def isEnabled(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.get_state() == LinuxBluetooth.STATE_ON
return self._enabled
def enable(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.enable()
self._enabled = True
self._state = self.STATE_ON
return True
def disable(self) -> bool:
if _USE_LINUX:
return LinuxBluetooth.disable()
self._enabled = False
self._state = self.STATE_OFF
return True
@@ -668,13 +681,36 @@ class BluetoothAdapter(Stub):
return True
def getBondedDevices(self) -> Set[BluetoothDevice]:
if _USE_LINUX:
devices = set()
for d in LinuxBluetooth.get_bonded_devices():
dev = BluetoothDevice(d.get("address", "00:00:00:00:00:00"))
dev._name = d.get("name", "Unknown")
dev._bond_state = BluetoothDevice.BOND_BONDED
devices.add(dev)
return devices
return set(self._bonded_devices)
def getBondedDeviceAddresses(self) -> Set[str]:
"""获取已配对设备地址集合"""
if _USE_LINUX:
return {d.get("address", "") for d in LinuxBluetooth.get_bonded_devices()}
return {d.getAddress() for d in self._bonded_devices}
def startDiscovery(self) -> bool:
if _USE_LINUX:
try:
LinuxBluetooth.scan(timeout=5)
return True
except Exception:
return False
self._discovering = True
return True
def cancelDiscovery(self) -> bool:
if _USE_LINUX:
# bluetoothctl scan off 在 LinuxBluetooth.scan 内部已处理
return True
self._discovering = False
return True
@@ -694,6 +730,8 @@ class BluetoothAdapter(Stub):
pass
def getState(self) -> int:
if _USE_LINUX:
return LinuxBluetooth.get_state()
return self._state
def listenUsingRfcommWithServiceRecord(self, name: str, uuid_str: str) -> BluetoothServerSocket:
@@ -704,6 +742,18 @@ class BluetoothAdapter(Stub):
return BluetoothServerSocket()
def getRemoteDevice(self, address: str) -> Optional[BluetoothDevice]:
if _USE_LINUX:
# 从已配对设备中查找
for d in LinuxBluetooth.get_bonded_devices():
if d.get("address", "").upper() == address.upper():
dev = BluetoothDevice(d.get("address", address))
dev._name = d.get("name", "Unknown")
dev._bond_state = BluetoothDevice.BOND_BONDED
return dev
# 未配对但地址合法则返回一个设备对象
if BluetoothAdapter.checkBluetoothAddress(address):
return BluetoothDevice(address)
return None
if not BluetoothAdapter.checkBluetoothAddress(address):
return None
return BluetoothDevice(address)
+33
View File
@@ -16,6 +16,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
try:
from .._linux_impl import IS_LINUX, LinuxClipboard, LinuxNotify, LinuxLocation, LinuxProcess
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Uri
@@ -466,6 +472,7 @@ class Context(Stub):
INPUT_METHOD_SERVICE = "input_method"
CAMERA_SERVICE = "camera"
BATTERY_SERVICE = "battery"
CLIPBOARD_SERVICE = "clipboard"
# Mode 常量
MODE_PRIVATE = 0x0000
@@ -642,6 +649,8 @@ class Context(Stub):
if name == Context.BATTERY_SERVICE:
from .os import BatteryManager
return BatteryManager()
if name == Context.CLIPBOARD_SERVICE:
return ClipboardManager()
return self._services.get(name)
def startService(self, intent: Any) -> Optional[Any]:
@@ -1708,6 +1717,29 @@ class SyncResult(Stub):
return f"SyncResult({self.stats.numEntries} entries)"
# ═══════════════════════════════════════════════════════════════
# ClipboardManager
# ═══════════════════════════════════════════════════════════════
class ClipboardManager(Stub):
"""ClipboardManager — 剪贴板管理器"""
def getText(self):
if _USE_LINUX:
return LinuxClipboard.read()
return ""
def setText(self, text):
if _USE_LINUX:
return LinuxClipboard.write(text)
return False
def hasText(self):
if _USE_LINUX:
return len(LinuxClipboard.read()) > 0
return False
# ═══════════════════════════════════════════════════════════════
# 替换 _core.py 中的 _GlobalContext
# ═══════════════════════════════════════════════════════════════
@@ -1748,4 +1780,5 @@ __all__ = [
"DialogInterface",
"SyncAdapter",
"SyncResult",
"ClipboardManager",
]
+82
View File
@@ -10,6 +10,13 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxBluetooth, LinuxSensors, HAS_BLUETOOTHCTL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Sensor
@@ -251,14 +258,89 @@ class SensorManager(Stub):
self._sensors[type_] = Sensor(type_)
def getDefaultSensor(self, type_: int) -> Optional[Sensor]:
if _USE_LINUX:
sensor = self._build_linux_sensor(type_)
if sensor is not None:
return sensor
return self._sensors.get(type_)
def _build_linux_sensor(self, type_: int) -> Optional[Sensor]:
"""根据 Linux 真实传感器数据构造 Sensor 对象"""
try:
if type_ == Sensor.TYPE_AMBIENT_TEMPERATURE:
temps = LinuxSensors.get_temperatures()
if temps:
t = temps[0]
return Sensor(Sensor.TYPE_AMBIENT_TEMPERATURE,
name=t.get("name", "Linux Temperature"),
vendor="Linux", resolution=0.01,
max_range=125.0, power=0.0, min_delay=1000)
return None
if type_ == Sensor.TYPE_LIGHT:
light = LinuxSensors.get_light_level()
if light is not None:
return Sensor(Sensor.TYPE_LIGHT, name="Linux Light Sensor",
vendor="Linux", resolution=0.001,
max_range=40000.0, power=0.0, min_delay=1000)
return None
except Exception:
return None
return None
def getSensorList(self, type_: int) -> List[Sensor]:
if _USE_LINUX:
return self._get_linux_sensor_list(type_)
if type_ == -1: # ALL
return list(self._sensors.values())
sensor = self._sensors.get(type_)
return [sensor] if sensor else []
def _get_linux_sensor_list(self, type_: int) -> List[Sensor]:
"""从 Linux 真实传感器数据生成传感器列表"""
sensors: List[Sensor] = []
try:
if type_ == -1 or type_ == Sensor.TYPE_AMBIENT_TEMPERATURE:
for t in LinuxSensors.get_temperatures():
sensors.append(Sensor(
Sensor.TYPE_AMBIENT_TEMPERATURE,
name=t.get("name", "Linux Temperature"),
vendor="Linux", resolution=0.01,
max_range=125.0, power=0.0, min_delay=1000))
if type_ == -1 or type_ == Sensor.TYPE_LIGHT:
light = LinuxSensors.get_light_level()
if light is not None:
sensors.append(Sensor(
Sensor.TYPE_LIGHT, name="Linux Light Sensor",
vendor="Linux", resolution=0.001,
max_range=40000.0, power=0.0, min_delay=1000))
# CPU 频率作为附加传感器 (映射到压力传感器类型以承载真实数据)
if type_ == -1 or type_ == Sensor.TYPE_PRESSURE:
for f in LinuxSensors.get_cpu_frequency():
sensors.append(Sensor(
Sensor.TYPE_PRESSURE,
name=f"CPU{f.get('cpu', 0)} Frequency",
vendor="Linux", resolution=1.0,
max_range=10000.0, power=0.0, min_delay=1000))
# 风扇转速作为附加传感器 (映射到相对湿度类型以承载真实数据)
if type_ == -1 or type_ == Sensor.TYPE_RELATIVE_HUMIDITY:
for fan in LinuxSensors.get_fan_speeds():
sensors.append(Sensor(
Sensor.TYPE_RELATIVE_HUMIDITY,
name=f"Fan {fan.get('name', '')}",
vendor="Linux", resolution=1.0,
max_range=10000.0, power=0.0, min_delay=1000))
except Exception:
pass
# 若未取到真实数据则回退到模拟传感器
if not sensors:
if type_ == -1:
sensors = list(self._sensors.values())
else:
sensor = self._sensors.get(type_)
if sensor:
sensors = [sensor]
return sensors
def getSensors(self) -> List[Sensor]:
return list(self._sensors.values())
+31
View File
@@ -16,6 +16,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
try:
from .._linux_impl import IS_LINUX, LinuxLocation
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Location
@@ -952,6 +958,15 @@ class LocationManager(Stub):
self.PASSIVE_PROVIDER, self.FUSED_PROVIDER}
def getLastKnownLocation(self, provider: str) -> Optional[Location]:
if _USE_LINUX:
data = LinuxLocation.get_location()
loc = Location(provider or "network")
loc.setLatitude(data.get("latitude", 0.0))
loc.setLongitude(data.get("longitude", 0.0))
loc.setAccuracy(data.get("accuracy", 0))
loc.setTime(int(datetime.now().timestamp() * 1000))
loc.setProvider(provider or "network")
return loc
if provider in self._last_known_locations:
return self._last_known_locations[provider]
loc = Location(provider)
@@ -969,6 +984,18 @@ class LocationManager(Stub):
provider = provider_or_request
else:
provider = self.GPS_PROVIDER
if _USE_LINUX and listener_or_callback is not None:
data = LinuxLocation.get_location()
loc = Location(provider or "network")
loc.setLatitude(data.get("latitude", 0.0))
loc.setLongitude(data.get("longitude", 0.0))
loc.setAccuracy(data.get("accuracy", 0))
loc.setTime(int(datetime.now().timestamp() * 1000))
loc.setProvider(provider or "network")
try:
listener_or_callback.onLocationChanged(loc)
except Exception:
pass
if listener_or_callback is not None:
if provider not in self._listeners:
self._listeners[provider] = []
@@ -984,9 +1011,13 @@ class LocationManager(Stub):
del self._listeners[provider]
def isProviderEnabled(self, provider: str) -> bool:
if _USE_LINUX:
return provider == "network"
return provider in self._enabled_providers
def getProviders(self, criteria: Optional[Criteria] = None, enabled_only: bool = False) -> List[str]:
if _USE_LINUX:
return ["network"]
providers = list(self._providers.keys())
if enabled_only:
providers = [p for p in providers if p in self._enabled_providers]
+53 -2
View File
@@ -20,6 +20,13 @@ from .._core import Stub, AndroidOnlyError, _app_data_dir, _sdcard_root
from ..java.lang import RuntimeException
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
@@ -517,14 +524,58 @@ class ConnectivityManager(Stub):
self._active_network: Optional[Any] = None
self._all_networks: List[Any] = []
def _get_linux_active_info(self):
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
try:
from .net import NetworkInfo
interfaces = LinuxNetwork.get_interfaces()
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
connected = any(
(iface.get("ifname") or iface.get("name", "")) != "lo"
and iface.get("operstate", "").lower() == "up"
for iface in interfaces
)
if not connected:
connected = LinuxNetwork.get_connectivity()
ip = LinuxNetwork.get_ip_address()
iface_name = ""
net_type = ConnectivityManager.TYPE_ETHERNET
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
state = iface.get("operstate", "")
if name and name != "lo" and state.lower() == "up":
iface_name = iface_name or name
if name and name.startswith(("wl", "wlan", "wlp")):
net_type = ConnectivityManager.TYPE_WIFI
iface_name = name
data = {
"connected": connected,
"ip": ip,
"interface": iface_name,
"type_name": "WIFI" if net_type == ConnectivityManager.TYPE_WIFI else "ETHERNET",
}
return NetworkInfo.from_linux_data(data, network_type=net_type)
except Exception:
return self._active_network_info
def getActiveNetworkInfo(self) -> Optional[Any]:
if _USE_LINUX:
return self._get_linux_active_info()
return self._active_network_info
def getNetworkInfo(self, networkType: int) -> Optional[Any]:
if _USE_LINUX:
info = self._get_linux_active_info()
if info is not None and info.getType() == networkType:
return info
return None
return None
def getNetworkInfo(self, network: Any) -> Optional[Any]:
return None
def getConnectivityStatus(self) -> bool:
"""返回当前网络连通性 (True=已连接)"""
if _USE_LINUX:
return LinuxNetwork.get_connectivity()
return False
def getAllNetworkInfo(self) -> List[Any]:
return self._all_networks
+29
View File
@@ -13,6 +13,13 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, resolve_android_path
from .graphics import PointF
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxAudio, HAS_AMIXER
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# AudioAttributes
@@ -596,15 +603,37 @@ class AudioManager(Stub):
self._parameters: Dict[str, str] = {}
def getStreamVolume(self, streamType: int) -> int:
if _USE_LINUX:
# STREAM_MUSIC 及其它流类型映射到 Linux 主音量
return LinuxAudio.get_volume()
return self._stream_volumes.get(streamType, 0)
def getStreamMaxVolume(self, streamType: int) -> int:
if _USE_LINUX:
return 100
return 15
def setStreamVolume(self, streamType: int, index: int, flags: int = 0):
if _USE_LINUX:
# STREAM_MUSIC 及其它流类型映射到 Linux 主音量
LinuxAudio.set_volume(index)
return
if streamType in self._stream_volumes:
self._stream_volumes[streamType] = max(0, min(15, index))
def isMicrophoneMute(self) -> bool:
"""麦克风是否静音"""
if _USE_LINUX:
return LinuxAudio.get_mute()
return False
def setMicrophoneMute(self, on: bool):
"""设置麦克风静音"""
if _USE_LINUX:
LinuxAudio.set_mute(on)
return
# 存根: 无操作
def adjustVolume(self, direction: int, flags: int = 0):
pass
+95
View File
@@ -17,6 +17,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Uri (re-export from content)
@@ -379,6 +386,38 @@ class NetworkInfo(Stub):
}
return names.get(type_, "UNKNOWN")
@classmethod
def from_linux_data(cls, data: Optional[dict] = None,
network_type: int = 9) -> "NetworkInfo":
"""从 Linux 真实网络数据构造 NetworkInfo
data 字典可包含: ip, interface, connected, type_name 等
"""
if data is None:
data = {}
connected = bool(data.get("connected", False))
state = "CONNECTED" if connected else "DISCONNECTED"
detailed_state = "CONNECTED" if connected else "DISCONNECTED"
type_name = data.get("type_name") or cls._default_type_name_static(network_type)
extra_info = data.get("ip") or data.get("interface")
return cls(
type_=network_type,
state=state,
detailed_state=detailed_state,
type_name=type_name,
extra_info=extra_info,
is_available=True,
roaming=False,
)
@staticmethod
def _default_type_name_static(type_: int) -> str:
names = {
0: "MOBILE", 1: "WIFI", 6: "WIMAX", 7: "BLUETOOTH",
9: "ETHERNET", 17: "VPN",
}
return names.get(type_, "UNKNOWN")
def getType(self) -> int:
return self._type
@@ -1230,15 +1269,71 @@ class ConnectivityManager(Stub):
self._network_caps: Dict[int, NetworkCapabilities] = {}
self._link_props: Dict[int, LinkProperties] = {}
def _get_linux_active_info(self) -> Optional[NetworkInfo]:
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
try:
interfaces = LinuxNetwork.get_interfaces()
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
connected = any(
(iface.get("ifname") or iface.get("name", "")) != "lo"
and iface.get("operstate", "").lower() == "up"
for iface in interfaces
)
# 若有活跃接口但 get_connectivity 为 False (外网不可达), 仍视为已连接
if not connected:
connected = LinuxNetwork.get_connectivity()
ip = LinuxNetwork.get_ip_address()
iface_name = ""
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
state = iface.get("operstate", "")
if name and name != "lo" and state.lower() == "up":
iface_name = name
break
# 判断网络类型 (无线接口 -> WIFI, 否则 ETHERNET)
net_type = self.TYPE_ETHERNET
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
if name and name.startswith(("wl", "wlan", "wlp")):
net_type = self.TYPE_WIFI
iface_name = iface_name or name
break
data = {
"connected": connected,
"ip": ip,
"interface": iface_name,
"type_name": "WIFI" if net_type == self.TYPE_WIFI else "ETHERNET",
}
return NetworkInfo.from_linux_data(data, network_type=net_type)
except Exception:
return self._active_network_info
def getActiveNetworkInfo(self) -> Optional[NetworkInfo]:
if _USE_LINUX:
return self._get_linux_active_info()
return self._active_network_info
def getNetworkInfo(self, network_type: int) -> Optional[NetworkInfo]:
if _USE_LINUX:
info = self._get_linux_active_info()
if info is not None and info.getType() == network_type:
return info
# 请求的类型与活跃网络不同时, 返回断开状态
return NetworkInfo(
type_=network_type, state="DISCONNECTED",
detailed_state="DISCONNECTED", is_available=False,
)
network = self._networks.get(network_type)
if network:
return NetworkInfo(type_=network_type, state="CONNECTED", is_available=True)
return self._active_network_info
def getConnectivityStatus(self) -> bool:
"""返回当前网络连通性 (True=已连接)"""
if _USE_LINUX:
return LinuxNetwork.get_connectivity()
return True
def getNetworkInfoForUid(self, network: Network, uid: int) -> Optional[NetworkInfo]:
return self._active_network_info
+54
View File
@@ -15,6 +15,13 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
__all__ = [
"Network", "NetworkCapabilities", "NetworkRequest", "NetworkSpecifier",
"MatchAllNetworkSpecifier", "NetworkAgent", "NetworkInfo",
@@ -1291,17 +1298,64 @@ class ConnectivityManager(Stub):
def getActiveNetwork(self) -> Optional[Network]:
return self._active_network
def _get_linux_active_info(self):
"""从 Linux 真实网络信息构造活跃 NetworkInfo"""
try:
from .net import NetworkInfo
interfaces = LinuxNetwork.get_interfaces()
# 有真实网络接口 (非环回) 且 operstate=up 即视为已连接
connected = any(
(iface.get("ifname") or iface.get("name", "")) != "lo"
and iface.get("operstate", "").lower() == "up"
for iface in interfaces
)
if not connected:
connected = LinuxNetwork.get_connectivity()
ip = LinuxNetwork.get_ip_address()
iface_name = ""
net_type = ConnectivityManager.TYPE_ETHERNET
for iface in interfaces:
name = iface.get("ifname") or iface.get("name", "")
state = iface.get("operstate", "")
if name and name != "lo" and state.lower() == "up":
iface_name = iface_name or name
if name and name.startswith(("wl", "wlan", "wlp")):
net_type = ConnectivityManager.TYPE_WIFI
iface_name = name
data = {
"connected": connected,
"ip": ip,
"interface": iface_name,
"type_name": "WIFI" if net_type == ConnectivityManager.TYPE_WIFI else "ETHERNET",
}
return NetworkInfo.from_linux_data(data, network_type=net_type)
except Exception:
return None
def getActiveNetworkInfo(self) -> Optional[NetworkInfo]:
if _USE_LINUX:
return self._get_linux_active_info()
if self._active_network:
return self._network_infos.get(self._active_network.getNetId())
return None
def getNetworkInfo(self, networkType: int) -> Optional[NetworkInfo]:
if _USE_LINUX:
info = self._get_linux_active_info()
if info is not None and info.getType() == networkType:
return info
return None
for info in self._network_infos.values():
if info.getType() == networkType:
return info
return None
def getConnectivityStatus(self) -> bool:
"""返回当前网络连通性 (True=已连接)"""
if _USE_LINUX:
return LinuxNetwork.get_connectivity()
return self._active_network is not None
def getNetworkInfoForNetwork(self, network: Network) -> Optional[NetworkInfo]:
return self._network_infos.get(network.getNetId())
+55
View File
@@ -16,6 +16,13 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, HAS_IP
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# SupplicantState
@@ -620,6 +627,8 @@ class WifiManager(Stub):
self._p2p_channels: List[int] = []
def getConnectionInfo(self) -> Optional[WifiInfo]:
if _USE_LINUX:
return self._get_linux_connection_info()
if self._connection_info is None:
info = WifiInfo()
info._ssid = '"mock_wifi"'
@@ -632,6 +641,34 @@ class WifiManager(Stub):
self._connection_info = info
return self._connection_info
def getWifiInfo(self) -> Optional[WifiInfo]:
"""getWifiInfo — getConnectionInfo 的别名"""
return self.getConnectionInfo()
def _get_linux_connection_info(self) -> WifiInfo:
"""从 LinuxNetwork.get_wifi_info() 构造 WifiInfo"""
info = WifiInfo()
try:
data = LinuxNetwork.get_wifi_info() or {}
ssid = data.get("ssid", "")
info._ssid = f'"{ssid}"' if ssid else "<unknown ssid>"
info._bssid = data.get("bssid", "00:00:00:00:00:00")
# signal 通常是 0-100, 转换为 RSSI (dBm, 约 -100..0)
signal = data.get("signal", 0)
if isinstance(signal, (int, float)) and signal > 0:
info._rssi = int(-100 + (signal / 100.0) * 60)
else:
info._rssi = int(signal) if signal else WifiInfo.INVALID_RSSI
iface = data.get("interface", "")
if iface:
info._frequency = 2412 # 默认 2.4GHz
info._link_speed = data.get("link_speed", 0) or 0
info._network_id = 0
info._supplicant_state = SupplicantState(SupplicantState.COMPLETED)
except Exception:
pass
return info
def getScanResults(self) -> List[ScanResult]:
return list(self._scan_results)
@@ -644,8 +681,26 @@ class WifiManager(Stub):
return True
def isWifiEnabled(self) -> bool:
if _USE_LINUX:
return self._linux_has_wireless_interface()
return self._wifi_enabled
def _linux_has_wireless_interface(self) -> bool:
"""检查是否存在无线网络接口"""
try:
for iface in LinuxNetwork.get_interfaces():
name = iface.get("ifname") or iface.get("name", "")
if name and name.startswith(("wl", "wlan", "wlp")):
return True
# 回退: 检查 /sys/class/net 下的无线接口
import os as _os
for name in _os.listdir("/sys/class/net/") if _os.path.isdir("/sys/class/net") else []:
if name.startswith(("wl", "wlan", "wlp")):
return True
except Exception:
pass
return False
def getWifiState(self) -> int:
return self._wifi_state
+163
View File
@@ -20,6 +20,13 @@ from .._core import (
HAS_PSUTIL,
)
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxPower, LinuxProcess, HAS_PSUTIL as _HAS_PSUTIL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Build
@@ -118,6 +125,50 @@ class Build(Stub):
def getFingerprint() -> str:
return Build.FINGERPRINT
@classmethod
def _populate_from_linux(cls):
"""从 LinuxProcess.get_system_info() 填充真实设备信息"""
try:
info = LinuxProcess.get_system_info() or {}
import platform as _platform
import socket as _socket
hostname = info.get("hostname") or _socket.gethostname()
system = info.get("platform") or _platform.system()
arch = info.get("arch") or _platform.machine()
kernel = info.get("kernel") or _platform.release()
cls.MODEL = hostname
cls.MANUFACTURER = "Linux"
cls.BRAND = system
cls.DEVICE = arch
cls.BOARD = arch
cls.HARDWARE = arch
cls.HOST = hostname
cls.PRODUCT = hostname
cls.DISPLAY = kernel
cls.ID = kernel
# FINGERPRINT: brand/device/device:version/id/build:user/release-keys
cls.FINGERPRINT = (
f"{system}/{hostname}/{arch}:Linux/{kernel}/{hostname}:user/release-keys"
)
# CPU ABI 用架构映射
_abi_map = {
"x86_64": "x86_64", "amd64": "x86_64",
"aarch64": "arm64-v8a", "arm64": "arm64-v8a",
"x86": "x86", "i386": "x86", "i686": "x86",
"armv7l": "armeabi-v7a", "arm": "armeabi-v7a",
}
cls.CPU_ABI = _abi_map.get(arch, arch)
cls.SUPPORTED_ABIS = [cls.CPU_ABI]
cls.SUPPORTED_64_BIT_ABIS = [cls.CPU_ABI] if "64" in cls.CPU_ABI else []
cls.SUPPORTED_32_BIT_ABIS = [cls.CPU_ABI] if "64" not in cls.CPU_ABI else []
except Exception:
pass
# 在 Linux 上用真实系统信息填充 Build
if _USE_LINUX:
Build._populate_from_linux()
# ═══════════════════════════════════════════════════════════════
# SystemClock
@@ -1299,6 +1350,20 @@ class PowerManager(Stub):
return PowerManagerWakeLock(level_and_flags, tag)
def isInteractive(self) -> bool:
if _USE_LINUX:
return self.isScreenOn()
return self._is_interactive
def isScreenOn(self) -> bool:
"""屏幕是否点亮 — 在 Linux 上检查 DISPLAY 环境变量"""
if _USE_LINUX:
import os as _os_mod
display = _os_mod.environ.get("DISPLAY", "")
if display:
return True
# WAYLAND_DISPLAY 作为回退
wayland = _os_mod.environ.get("WAYLAND_DISPLAY", "")
return bool(wayland)
return self._is_interactive
def isPowerSaveMode(self) -> bool:
@@ -1478,8 +1543,58 @@ class BatteryManager(Stub):
self._current_now = -500000
self._current_avg = -450000
self._energy_counter = 15000000
if _USE_LINUX:
self._refresh_from_linux()
_LINUX_STATUS_MAP = {
"charging": 2, # BATTERY_STATUS_CHARGING
"discharging": 3, # BATTERY_STATUS_DISCHARGING
"not charging": 4, # BATTERY_STATUS_NOT_CHARGING
"full": 5, # BATTERY_STATUS_FULL
"unknown": 1, # BATTERY_STATUS_UNKNOWN
}
def _get_linux_battery_info(self) -> dict:
"""从 LinuxPower 获取真实电池信息"""
try:
return LinuxPower.get_battery_info() or {}
except Exception:
return {}
def _refresh_from_linux(self):
"""用 Linux 真实电池信息刷新内部状态"""
info = self._get_linux_battery_info()
if not info:
return
self._present = bool(info.get("present", False))
level = info.get("level", -1)
if self._present and isinstance(level, (int, float)) and level >= 0:
self._level = int(level)
self._capacity = int(level)
else:
# 无电池或电量未知
self._level = -1
self._capacity = -1
status_str = str(info.get("status", "unknown")).lower()
self._status = self._LINUX_STATUS_MAP.get(status_str, self.BATTERY_STATUS_UNKNOWN)
voltage = info.get("voltage", 0)
if isinstance(voltage, (int, float)):
# LinuxPower 返回伏特, Android 期望毫伏
self._voltage = int(voltage * 1000)
temperature = info.get("temperature", 0)
if isinstance(temperature, (int, float)):
# LinuxPower 返回摄氏度, Android 期望十分之一度
self._temperature = int(temperature * 10)
self._technology = info.get("technology", "") or "Li-ion"
# 充电状态决定 plugged
if self._status == self.BATTERY_STATUS_CHARGING:
self._plugged = self.BATTERY_PLUGGED_AC
else:
self._plugged = 0
def getIntProperty(self, prop_id: int) -> int:
if _USE_LINUX:
self._refresh_from_linux()
if prop_id == self.BATTERY_PROPERTY_CAPACITY:
return self._capacity
if prop_id == self.BATTERY_PROPERTY_STATUS:
@@ -1498,9 +1613,57 @@ class BatteryManager(Stub):
return self.getIntProperty(prop_id)
def isCharging(self) -> bool:
if _USE_LINUX:
self._refresh_from_linux()
return self._status == self.BATTERY_STATUS_CHARGING
def isPresent(self) -> bool:
"""电池是否存在"""
if _USE_LINUX:
self._refresh_from_linux()
return self._present
def getLevel(self) -> int:
"""电池电量 (0-100, -1 表示未知)"""
if _USE_LINUX:
self._refresh_from_linux()
return self._level
def getVoltage(self) -> int:
"""电池电压 (毫伏)"""
if _USE_LINUX:
self._refresh_from_linux()
return self._voltage
def getTemperature(self) -> int:
"""电池温度 (十分之一摄氏度)"""
if _USE_LINUX:
self._refresh_from_linux()
return self._temperature
def getTechnology(self) -> str:
"""电池技术"""
if _USE_LINUX:
self._refresh_from_linux()
return self._technology
def getPlugType(self) -> int:
"""充电类型"""
if _USE_LINUX:
self._refresh_from_linux()
return self._plugged
def getStatus(self) -> int:
"""电池状态"""
if _USE_LINUX:
self._refresh_from_linux()
return self._status
def computeChargeTimeRemaining(self) -> int:
if _USE_LINUX:
# 无电池或已充满时返回 -1
if not self._present or self._status == self.BATTERY_STATUS_FULL:
return -1
return 1800000 # 30 minutes
def toString(self) -> str:
+74 -1
View File
@@ -18,6 +18,13 @@ from .._core import Stub, AndroidOnlyError, _app_data_dir
from ..java.lang import RuntimeException
# Linux 真实实现导入
try:
from .._linux_impl import IS_LINUX, LinuxPower, LinuxProcess, HAS_PSUTIL as _HAS_PSUTIL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {})
@@ -505,6 +512,54 @@ class BatteryManager(Stub):
self._discharge_time_remaining = -1
self._is_capacity_estimated = False
self._capacity_percentage = 100.0
if _USE_LINUX:
self._refresh_from_linux()
_LINUX_STATUS_MAP = {
"charging": BATTERY_STATUS_CHARGING,
"discharging": BATTERY_STATUS_DISCHARGING,
"not charging": BATTERY_STATUS_NOT_CHARGING,
"full": BATTERY_STATUS_FULL,
"unknown": BATTERY_STATUS_UNKNOWN,
}
def _get_linux_battery_info(self) -> dict:
"""从 LinuxPower 获取真实电池信息"""
try:
return LinuxPower.get_battery_info() or {}
except Exception:
return {}
def _refresh_from_linux(self):
"""用 Linux 真实电池信息刷新内部状态"""
info = self._get_linux_battery_info()
if not info:
return
self._present = bool(info.get("present", False))
level = info.get("level", -1)
if self._present and isinstance(level, (int, float)) and level >= 0:
self._battery_level = int(level)
self._capacity_percentage = float(level)
# BATTERY_PROPERTY_CAPACITY 在 Android 中返回百分比 (0-100)
self._capacity = int(level)
else:
# 无电池或电量未知
self._battery_level = -1
self._capacity_percentage = 0.0
self._capacity = -1
status_str = str(info.get("status", "unknown")).lower()
self._status = self._LINUX_STATUS_MAP.get(status_str, BATTERY_STATUS_UNKNOWN)
voltage = info.get("voltage", 0)
if isinstance(voltage, (int, float)):
self._voltage = int(voltage * 1000) # V -> mV
temperature = info.get("temperature", 0)
if isinstance(temperature, (int, float)):
self._temperature = temperature # 摄氏度
self._technology = info.get("technology", "") or "Li-ion"
if self._status == BATTERY_STATUS_CHARGING:
self._plugged = BATTERY_PLUGGED_AC
else:
self._plugged = 0
@staticmethod
def getInstance(context: Any) -> "BatteryManager":
@@ -513,6 +568,8 @@ class BatteryManager(Stub):
return BatteryManager._instance
def getIntProperty(self, id: int) -> int:
if _USE_LINUX:
self._refresh_from_linux()
properties = {
BATTERY_PROPERTY_CAPACITY: self._capacity,
BATTERY_PROPERTY_CHARGE_COUNTER: self._charge_counter,
@@ -521,6 +578,9 @@ class BatteryManager(Stub):
BATTERY_PROPERTY_ENERGY_COUNTER: self._energy_counter,
BATTERY_PROPERTY_CHARGE_TIME_REMAINING: self._charge_time_remaining,
BATTERY_PROPERTY_DISCHARGE_TIME_REMAINING: self._discharge_time_remaining,
BATTERY_PROPERTY_STATUS: self._status,
BATTERY_PROPERTY_TEMPERATURE: int(self._temperature),
BATTERY_PROPERTY_VOLTAGE: self._voltage,
}
return properties.get(id, 0)
@@ -528,9 +588,13 @@ class BatteryManager(Stub):
return self.getIntProperty(id)
def isCharging(self) -> bool:
if _USE_LINUX:
self._refresh_from_linux()
return self._status == BATTERY_STATUS_CHARGING or self._status == BATTERY_STATUS_FULL
def getBatteryLevel(self) -> int:
if _USE_LINUX:
self._refresh_from_linux()
return self._battery_level
def getInt(intent: Any, key: str, defaultValue: int) -> int:
@@ -687,9 +751,11 @@ class PowerManager(Stub):
self._shutdown_requested = False
self._thermal_status = THERMAL_STATUS_NONE
self._power_save_state = PowerSaveState()
self._current_wake_up_reason = WAKE_REASON_UNKNOWN
self._current_wake_up_reason = PowerManager.WAKE_REASON_UNKNOWN
def isInteractive(self) -> bool:
if _USE_LINUX:
return self.isScreenOn()
return self._is_interactive
def isPowerSaveMode(self) -> bool:
@@ -705,6 +771,13 @@ class PowerManager(Stub):
return True
def isScreenOn(self) -> bool:
if _USE_LINUX:
import os as _os_mod
display = _os_mod.environ.get("DISPLAY", "")
if display:
return True
wayland = _os_mod.environ.get("WAYLAND_DISPLAY", "")
return bool(wayland)
return self._is_screen_on
def isWakeLockLevelSupported(self, level: int) -> bool:
+31
View File
@@ -14,6 +14,12 @@ from .._core import (
_sdcard_root, _app_data_dir, _package_name,
)
try:
from .._linux_impl import IS_LINUX, LinuxNetwork, LinuxProcess, LinuxLocation
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# CellIdentity 基类集合
@@ -553,6 +559,9 @@ class TelephonyManager(Stub):
self._context = context
def getNetworkOperatorName(self) -> str:
if _USE_LINUX:
loc = LinuxLocation.get_location()
return loc.get("isp", "") or ""
return "MockOperator"
def getNetworkOperator(self) -> str:
@@ -562,9 +571,15 @@ class TelephonyManager(Stub):
return "us"
def getNetworkType(self) -> int:
if _USE_LINUX:
if LinuxNetwork.get_connectivity():
return self.NETWORK_TYPE_IWLAN
return self.NETWORK_TYPE_UNKNOWN
return self.NETWORK_TYPE_LTE
def getPhoneType(self) -> int:
if _USE_LINUX:
return self.PHONE_TYPE_NONE
return self.PHONE_TYPE_GSM
def getDataNetworkType(self) -> int:
@@ -574,6 +589,8 @@ class TelephonyManager(Stub):
return self.NETWORK_TYPE_LTE
def getSimOperator(self) -> str:
if _USE_LINUX:
return ""
return "310260"
def getSimOperatorName(self) -> str:
@@ -583,18 +600,29 @@ class TelephonyManager(Stub):
return "us"
def getSimSerialNumber(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "89014103211118510720"
def getSubscriberId(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "310260123456789"
def getDeviceId(self) -> Optional[str]:
if _USE_LINUX:
return LinuxNetwork.get_mac_address() or None
return "000000000000000"
def getDeviceSoftwareVersion(self) -> Optional[str]:
if _USE_LINUX:
info = LinuxProcess.get_system_info()
return info.get("kernel", "")
return "00.00.00.00"
def getLine1Number(self) -> Optional[str]:
if _USE_LINUX:
return ""
return "15551234567"
def getGroupIdLevel1(self) -> Optional[str]:
@@ -619,6 +647,9 @@ class TelephonyManager(Stub):
return self.SIM_STATE_READY
def getCellLocation(self) -> Any:
# 桌面无基站,Linux 下返回 None
if _USE_LINUX:
return None
return None
def getAllCellInfo(self) -> Optional[List[CellInfo]]:
+16
View File
@@ -10,6 +10,12 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from .._core import Stub, AndroidOnlyError, android_context, HAS_PYAUTOGUI
try:
from .._linux_impl import IS_LINUX, LinuxDisplay, LinuxUIAutomation, HAS_XDOTOOL
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# Gravity
@@ -363,6 +369,12 @@ class View(Stub):
def performClick(self) -> bool:
return self.callOnClick()
def screenshot(self, path=None):
"""截屏"""
if _USE_LINUX:
return LinuxDisplay.screenshot(path)
return None
def performLongClick(self) -> bool:
if self._on_long_click_listener:
self._on_long_click_listener(self)
@@ -3260,9 +3272,13 @@ class Display(Stub):
return self._display_id
def getWidth(self) -> int:
if _USE_LINUX:
return LinuxDisplay.get_screen_size()[0]
return self._width
def getHeight(self) -> int:
if _USE_LINUX:
return LinuxDisplay.get_screen_size()[1]
return self._height
def getRotation(self) -> int:
+13 -1
View File
@@ -14,6 +14,12 @@ from .view import (
ViewGroup as VG,
)
try:
from .._linux_impl import IS_LINUX, LinuxNotify, LinuxClipboard
_USE_LINUX = IS_LINUX
except ImportError:
_USE_LINUX = False
# ═══════════════════════════════════════════════════════════════
# TextView
@@ -3589,9 +3595,15 @@ class Toast(Stub):
return toast
def show(self):
pass
if _USE_LINUX:
LinuxNotify.send("Toast", self._text)
else:
print(f"[Toast] {self._text}")
def cancel(self):
if _USE_LINUX:
# 桌面通知自动消失,无需操作
return
pass
def setGravity(self, gravity: int, x_offset: int, y_offset: int):