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:
1 parent
8c3bd3c415
commit
690022cea3
17 files changed
+2083
-8
No files matched your search
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user