mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 18:07:22 +01:00
Real fixes: split over-long functions (lock-decision chain, manual sync, validation, phone verification, status), hoist a dead lazy import, context-manage Xvfb and temp dirs, add encodings, share the silent-failure argv loop. lock_grab_handoff read lock._recovery, which gatelock's LockWindow does not have (its public property is recovery); a contract test now checks every attribute the handoff reads against the real class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
"""ADB transport for phone verification: connect, reconnect, run commands.
|
|
|
|
Split out of :mod:`screen_locker._phone_verification` to keep every file under
|
|
the 250-line cap. Composed back into ``PhoneVerificationMixin`` there, so
|
|
callers see no change.
|
|
|
|
Everything here is plumbing: reaching the phone and running a command on it.
|
|
Deciding whether what came back counts as a workout stays in
|
|
``_phone_verification``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from concurrent.futures import (
|
|
ThreadPoolExecutor,
|
|
as_completed,
|
|
)
|
|
import contextlib
|
|
import logging
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
|
|
from screen_locker._constants import ADB_TIMEOUT
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AdbTransportMixin:
|
|
"""Finds the phone over USB or wifi and runs adb commands against it."""
|
|
|
|
def _run_adb(self, args: list[str]) -> tuple[bool, str]:
|
|
"""Run an ADB command and return success flag and stdout."""
|
|
adb = shutil.which("adb") or "adb"
|
|
# When multiple devices are connected (e.g. USB + wireless), pin to
|
|
# the wireless device's serial to avoid "more than one device" errors.
|
|
_discovery_cmds = {"devices", "connect", "disconnect", "kill-server"}
|
|
serial = (
|
|
self._get_wireless_serial()
|
|
if args and args[0] not in _discovery_cmds
|
|
else None
|
|
)
|
|
serial_args = ["-s", serial] if serial else []
|
|
try:
|
|
result = subprocess.run(
|
|
[adb, *serial_args, *args],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=ADB_TIMEOUT,
|
|
check=False,
|
|
)
|
|
except (FileNotFoundError, OSError) as exc:
|
|
_logger.warning("ADB not available: %s", exc)
|
|
return False, ""
|
|
except subprocess.TimeoutExpired:
|
|
_logger.warning("ADB command timed out: %s", args)
|
|
return False, ""
|
|
return not result.returncode, result.stdout
|
|
|
|
def _adb_shell(self, command: str, *, root: bool = False) -> tuple[bool, str]:
|
|
"""Run a shell command on the connected Android device."""
|
|
if root:
|
|
return self._run_adb(["shell", "su", "-c", command])
|
|
return self._run_adb(["shell", command])
|
|
|
|
def _get_wireless_serial(self) -> str | None:
|
|
"""Return the serial (ip:port) of the first connected wireless ADB device."""
|
|
success, output = self._run_adb(["devices"])
|
|
if not success:
|
|
return None
|
|
for line in output.strip().split("\n")[1:]:
|
|
parts = line.split()
|
|
if parts and ":" in parts[0] and "device" in line and "offline" not in line:
|
|
return parts[0]
|
|
return None
|
|
|
|
def _has_adb_device(self) -> bool:
|
|
"""Return True if adb devices shows at least one connected device."""
|
|
success, output = self._run_adb(["devices"])
|
|
if not success:
|
|
return False
|
|
lines = output.strip().split("\n")[1:]
|
|
return any("device" in line and "offline" not in line for line in lines)
|
|
|
|
def _try_adb_connect(self, address: str) -> bool:
|
|
"""Run adb connect to address. Returns True on success."""
|
|
_, output = self._run_adb(["connect", address])
|
|
lower = output.lower()
|
|
return "connected" in lower and "unable" not in lower and "failed" not in lower
|
|
|
|
def _get_local_subnet_prefix(self) -> str | None:
|
|
"""Detect the local /24 network prefix (e.g. '192.168.1')."""
|
|
with (
|
|
contextlib.suppress(OSError),
|
|
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock,
|
|
):
|
|
sock.connect(("8.8.8.8", 80))
|
|
return ".".join(sock.getsockname()[0].split(".")[:3])
|
|
return None
|
|
|
|
def _try_wireless_reconnect(self) -> bool:
|
|
"""Scan local /24 subnet on port 5555 and attempt ADB connect to phone."""
|
|
prefix = self._get_local_subnet_prefix()
|
|
if prefix is None:
|
|
_logger.info("Could not determine local subnet for wireless scan")
|
|
return False
|
|
|
|
def probe(i: int) -> bool:
|
|
ip = f"{prefix}.{i}"
|
|
with (
|
|
contextlib.suppress(OSError),
|
|
socket.create_connection((ip, 5555), timeout=0.5),
|
|
):
|
|
if self._try_adb_connect(f"{ip}:5555"):
|
|
return self._has_adb_device()
|
|
return False
|
|
|
|
_logger.info("Scanning %s.1-254:5555 for phone...", prefix)
|
|
with ThreadPoolExecutor(max_workers=64) as executor:
|
|
for future in as_completed(
|
|
executor.submit(probe, i) for i in range(1, 255)
|
|
):
|
|
if future.result():
|
|
return True
|
|
return False
|
|
|
|
def _is_phone_connected(self) -> bool:
|
|
"""Check if an Android device is connected via ADB.
|
|
|
|
If no device is visible, attempts wireless reconnection via subnet scan.
|
|
"""
|
|
if self._has_adb_device():
|
|
return True
|
|
_logger.info("No ADB device detected — attempting wireless reconnect...")
|
|
return self._try_wireless_reconnect()
|
|
|
|
# ── ADB verification ──────────────────────────────────────────────────────
|