fix(client): read Android 16 foreground exactly
This commit is contained in:
@@ -10,6 +10,7 @@ import json
|
||||
from math import isfinite
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
from typing import Any, Protocol
|
||||
from uuid import uuid4
|
||||
@@ -18,7 +19,7 @@ from adbutils.errors import AdbTimeout
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from uiautomator2.exceptions import HTTPTimeoutError
|
||||
|
||||
from ..device.adb import AdbClient, DeviceConnectionError, DeviceInspection
|
||||
from ..device.adb import AdbClient, CommandRunner, DeviceConnectionError, DeviceInspection
|
||||
from ..device.baseline import (
|
||||
HIERARCHY_PARAMS,
|
||||
PDD_PACKAGE,
|
||||
@@ -52,13 +53,50 @@ class QuantityGate2ReadDevice(Protocol):
|
||||
|
||||
def app_info(self, package_name: str) -> dict[str, Any]: ...
|
||||
|
||||
def app_current(self) -> dict[str, Any]: ...
|
||||
|
||||
def window_size(self) -> tuple[int, int]: ...
|
||||
|
||||
def jsonrpc_call(self, method: str, params: Any = None, timeout: float = 10) -> Any: ...
|
||||
|
||||
|
||||
class QuantityGate2ForegroundReader(Protocol):
|
||||
"""只读取 Android 16 的唯一 resumed activity,不暴露通用 shell。"""
|
||||
|
||||
def read(self, serial: str) -> dict[str, str]: ...
|
||||
|
||||
|
||||
class Android16TopResumedForegroundReader:
|
||||
"""绕开 adbutils 2.12.0 对 Android 16 ``topResumedActivity`` 的误解析。"""
|
||||
|
||||
_TOP_RESUMED_PATTERN = re.compile(
|
||||
r"(?m)^\s*topResumedActivity=ActivityRecord\{[^\r\n}]*?\s+u\d+\s+"
|
||||
r"(?P<package>[^/\s]+)/(?P<activity>[^\s}]+)\s+t\d+\}\s*$"
|
||||
)
|
||||
|
||||
def __init__(self, runner: CommandRunner, timeout_seconds: float) -> None:
|
||||
if not _is_positive_finite(timeout_seconds):
|
||||
raise ValueError("timeout_seconds 必须是大于 0 的有限数值")
|
||||
self._runner = runner
|
||||
self._timeout_seconds = timeout_seconds
|
||||
|
||||
def read(self, serial: str) -> dict[str, str]:
|
||||
if type(serial) is not str or not serial.strip() or serial != serial.strip():
|
||||
raise QuantityGate2EvidenceError("必须显式提供非空设备通道。")
|
||||
result = self._runner.run(
|
||||
("-s", serial, "shell", "dumpsys", "activity", "activities"),
|
||||
self._timeout_seconds,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise QuantityGate2EvidenceError("Android 前台摘要读取失败,未发布证据。")
|
||||
matches = list(self._TOP_RESUMED_PATTERN.finditer(result.stdout))
|
||||
if len(matches) != 1:
|
||||
raise QuantityGate2EvidenceError("Android 前台摘要不唯一,未发布证据。")
|
||||
match = matches[0]
|
||||
return {
|
||||
"package": match.group("package"),
|
||||
"activity": match.group("activity"),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QuantityGate2EvidenceResult:
|
||||
output_directory: Path
|
||||
@@ -75,12 +113,14 @@ class QuantityGate2EvidenceCapturer:
|
||||
self,
|
||||
adb_client: AdbClient,
|
||||
connector: Callable[[str], QuantityGate2ReadDevice],
|
||||
foreground_reader: QuantityGate2ForegroundReader,
|
||||
timeout_seconds: float,
|
||||
) -> None:
|
||||
if not _is_positive_finite(timeout_seconds):
|
||||
raise ValueError("timeout_seconds 必须是大于 0 的有限数值")
|
||||
self._adb_client = adb_client
|
||||
self._connector = connector
|
||||
self._foreground_reader = foreground_reader
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._started = False
|
||||
|
||||
@@ -104,7 +144,10 @@ class QuantityGate2EvidenceCapturer:
|
||||
inspection = self._adb_client.inspect(serial)
|
||||
_require_expected_device(inspection)
|
||||
device = self._connector(serial)
|
||||
initial_app = _require_read_precondition(device)
|
||||
initial_app = _require_read_precondition(
|
||||
device,
|
||||
self._foreground_reader.read(serial),
|
||||
)
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
staging = target.parent / f".{target.name}.staging-{uuid4().hex}"
|
||||
@@ -132,7 +175,10 @@ class QuantityGate2EvidenceCapturer:
|
||||
hierarchy_path = staging / "hierarchy.xml"
|
||||
hierarchy_path.write_text(hierarchy, encoding="utf-8")
|
||||
|
||||
final_app = _require_read_precondition(device)
|
||||
final_app = _require_read_precondition(
|
||||
device,
|
||||
self._foreground_reader.read(serial),
|
||||
)
|
||||
if final_app != initial_app:
|
||||
raise QuantityGate2EvidenceError("数量状态取证期间前台页面漂移,未发布证据。")
|
||||
app_path = staging / "app.json"
|
||||
@@ -216,12 +262,14 @@ def _require_expected_device(inspection: DeviceInspection) -> None:
|
||||
raise QuantityGate2EvidenceError("设备不是已批准取证组合。")
|
||||
|
||||
|
||||
def _require_read_precondition(device: QuantityGate2ReadDevice) -> dict[str, str]:
|
||||
def _require_read_precondition(
|
||||
device: QuantityGate2ReadDevice,
|
||||
current: object,
|
||||
) -> dict[str, str]:
|
||||
info = device.app_info(PDD_PACKAGE)
|
||||
version = (info.get("versionName") or info.get("version_name")) if isinstance(info, dict) else None
|
||||
if version != EXPECTED_PDD_VERSION:
|
||||
raise QuantityGate2EvidenceError("拼多多版本不是已批准取证版本。")
|
||||
current = device.app_current()
|
||||
if not isinstance(current, dict) or current.get("package") != PDD_PACKAGE:
|
||||
raise QuantityGate2EvidenceError("拼多多不在前台。")
|
||||
activity = current.get("activity")
|
||||
|
||||
Reference in New Issue
Block a user