layernav-android 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ from layernav_android._protocol import AdbProtocol
2
+ from layernav_android.base import (
3
+ BaseLayerModel,
4
+ DetectResult,
5
+ KEYCODE_BACK,
6
+ KEYCODE_HOME,
7
+ LayerDef,
8
+ LayerListener,
9
+ POST_TRANSITION_SLEEP,
10
+ )
11
+ from layernav_android.cold_start import (
12
+ APP_DEFAULTS,
13
+ cold_start_app_from_launcher,
14
+ dock_app_icon_coords,
15
+ )
16
+
17
+ __all__ = [
18
+ "AdbProtocol",
19
+ "APP_DEFAULTS",
20
+ "BaseLayerModel",
21
+ "cold_start_app_from_launcher",
22
+ "DetectResult",
23
+ "dock_app_icon_coords",
24
+ "KEYCODE_BACK",
25
+ "KEYCODE_HOME",
26
+ "LayerDef",
27
+ "LayerListener",
28
+ "POST_TRANSITION_SLEEP",
29
+ ]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+
6
+ class AdbProtocol(Protocol):
7
+ """Minimal ADB interface for layer navigation.
8
+
9
+ Users implement or inject their existing ADB client.
10
+ ``collector_phone_android``'s ``AdbClient`` already satisfies
11
+ this protocol — no adapter needed.
12
+ """
13
+
14
+ def screencap(self) -> bytes: ...
15
+ def key_event(self, code: int) -> None: ...
16
+ def foreground_package(self) -> str: ...
17
+ def tap(self, x: int, y: int) -> None: ...
18
+ def _run(self, args: list[str]) -> str: ...
@@ -0,0 +1,402 @@
1
+ """Multi-layer Android task-stack navigation framework.
2
+
3
+ Framework—Task contract:
4
+ Task subclass overrides:
5
+ - ``layers`` — list of :class:`LayerDef`
6
+ - ``detect`` — screenshot-based layer detection
7
+ - ``_on_Lx`` — per-layer handler (business logic + tap)
8
+
9
+ Framework provides:
10
+ Atomic: ``detect`` ``enter_next`` ``back_one`` ``back_recover``
11
+ Combined: ``back`` ``advance`` ``restore``
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import time
18
+ from dataclasses import dataclass
19
+ from typing import Protocol
20
+
21
+ from layernav_android._protocol import AdbProtocol
22
+
23
+ LOG = logging.getLogger("layernav")
24
+
25
+ POST_TRANSITION_SLEEP = 1.5
26
+
27
+ KEYCODE_BACK = 4
28
+ KEYCODE_HOME = 3
29
+
30
+ # ── Layer definition ──────────────────────────────────────────────────────────
31
+
32
+
33
+ @dataclass
34
+ class LayerDef:
35
+ """Description of one layer in the Android task stack."""
36
+
37
+ key: str
38
+ """Layer key: ``"L0"`` | ``"L1"`` | ``"L2"`` | ``"L3"``."""
39
+
40
+ name: str
41
+ """Machine-readable name."""
42
+
43
+ label_cn: str
44
+ """Human-readable Chinese label."""
45
+
46
+ detection: str
47
+ """How this layer is detected (human-readable)."""
48
+
49
+ page_name: str = ""
50
+ """Optional custom page name. Callers can set this per-layer to
51
+ distinguish sub-states within a layer (e.g. main_list vs recent_page for L1).
52
+ Default empty string means no sub-page distinction."""
53
+
54
+ detection_extra: str = ""
55
+ """Optional detail about detection (human-readable, complementary to
56
+ *detection*). Callers can append custom context strings."""
57
+
58
+
59
+ # ── Observer / Listener (inspired by python-statemachine's Listener pattern) ───
60
+
61
+
62
+ class LayerListener(Protocol):
63
+ """Observer interface for layer model lifecycle events.
64
+
65
+ All methods are optional — implement only what you need.
66
+ Inspired by `python-statemachine
67
+ <https://github.com/fgmacedo/python-statemachine>`_'s Listener pattern.
68
+ """
69
+
70
+ def on_transition(
71
+ self, from_layer: str, to_layer: str, method: str,
72
+ ) -> None:
73
+ """Called after an atomic layer transition completes.
74
+
75
+ *method* is one of ``"enter_next"`` or ``"back_one"``.
76
+ """
77
+ ...
78
+
79
+ def on_timeout(
80
+ self, from_layer: str, target_layer: str, elapsed_s: float,
81
+ ) -> None:
82
+ """Called when :meth:`BaseLayerModel.enter_next` polling times out."""
83
+ ...
84
+
85
+ def on_recovery(self, target_layer: str, ok: bool) -> None:
86
+ """Called after :meth:`BaseLayerModel.back_recover` completes.
87
+
88
+ *ok* indicates whether the recovery succeeded.
89
+ """
90
+ ...
91
+
92
+
93
+ # ── Layer detection result ──────────────────────────────────────────────────────
94
+
95
+
96
+ @dataclass
97
+ class DetectResult:
98
+ """Result of :meth:`BaseLayerModel.detect_detail`.
99
+
100
+ Combines layer key with optional page_name from :class:`LayerDef`.
101
+ """
102
+
103
+ layer_key: str
104
+ """Layer key: ``"L0"`` | ``"L1"`` | ``"L2"`` | ``"L3"``."""
105
+
106
+ page_name: str = ""
107
+ """Custom page name from :attr:`LayerDef.page_name`, or ``""``."""
108
+
109
+
110
+ # ── Abstract base ─────────────────────────────────────────────────────────────
111
+
112
+
113
+ class BaseLayerModel:
114
+ """Abstract Android task-stack layer model.
115
+
116
+ Subclass contract:
117
+ - Override :attr:`layers`.
118
+ - Override :meth:`detect`.
119
+ - Override ``_on_L0`` / ``_on_L1`` / ``_on_L2`` / ``_on_L3``.
120
+ - Optionally override :meth:`_cold_start`.
121
+ """
122
+
123
+ layers: list[LayerDef] = []
124
+ _ON_METHODS: tuple[str, ...] = ("_on_L0", "_on_L1", "_on_L2", "_on_L3")
125
+
126
+ def __init__(self) -> None:
127
+ self._listeners: list[LayerListener] = []
128
+
129
+ def add_listener(self, listener: LayerListener) -> None:
130
+ """Register a :class:`LayerListener` to observe lifecycle events."""
131
+ self._listeners.append(listener)
132
+
133
+ def _notify_transition(
134
+ self, from_layer: str, to_layer: str, method: str,
135
+ ) -> None:
136
+ for lst in self._listeners:
137
+ lst.on_transition(from_layer, to_layer, method)
138
+
139
+ def _notify_timeout(
140
+ self, from_layer: str, target_layer: str, elapsed_s: float,
141
+ ) -> None:
142
+ for lst in self._listeners:
143
+ lst.on_timeout(from_layer, target_layer, elapsed_s)
144
+
145
+ def _notify_recovery(self, target_layer: str, ok: bool) -> None:
146
+ for lst in self._listeners:
147
+ lst.on_recovery(target_layer, ok)
148
+
149
+ # ── Subclass overrides ────────────────────────────────────────────────────
150
+
151
+ def detect(self, adb: AdbProtocol, scale_w: float) -> str:
152
+ """Return current layer key. Task MUST override."""
153
+ raise NotImplementedError("subclass must override detect()")
154
+
155
+ def detect_detail(self, adb: AdbProtocol, scale_w: float) -> DetectResult:
156
+ """Return current layer key + custom page_name.
157
+
158
+ Default implementation calls :meth:`detect` and looks up
159
+ :attr:`LayerDef.page_name` from :attr:`layers`. Subclasses
160
+ may override to set page_name dynamically.
161
+ """
162
+ layer_key = self.detect(adb, scale_w)
163
+ page_name = ""
164
+ for ld in self.layers:
165
+ if ld.key == layer_key:
166
+ page_name = ld.page_name
167
+ break
168
+ return DetectResult(layer_key=layer_key, page_name=page_name)
169
+
170
+ def _recover_to_page(
171
+ self,
172
+ layer: str,
173
+ page_name: str,
174
+ adb: AdbProtocol,
175
+ scale_w: float,
176
+ ) -> bool:
177
+ """Navigate to a specific sub-page within *layer* after recovery.
178
+
179
+ Called by :meth:`back_recover` (and :meth:`restore` when already
180
+ on the target layer) after reaching the correct layer. Override
181
+ to handle sub-page navigation (e.g. switching tabs within L1).
182
+
183
+ Default: verify current page via :meth:`detect_detail` — returns
184
+ ``True`` if ``detect_detail().page_name == page_name``.
185
+ """
186
+ result = self.detect_detail(adb, scale_w)
187
+ return result.page_name == page_name
188
+
189
+ def _on_L0(self, adb: AdbProtocol, scale_w: float, *, quick: bool = False) -> str | None:
190
+ """L0 handler: home screen → cold-start App."""
191
+ raise NotImplementedError("subclass must override _on_L0")
192
+
193
+ def _on_L1(self, adb: AdbProtocol, scale_w: float, *, quick: bool = False) -> str | None:
194
+ """L1 handler: App main screen → pick content, tap."""
195
+ raise NotImplementedError("subclass must override _on_L1")
196
+
197
+ def _on_L2(self, adb: AdbProtocol, scale_w: float, *, quick: bool = False) -> str | None:
198
+ """L2 handler: content page → pick sub-content, tap."""
199
+ raise NotImplementedError("subclass must override _on_L2")
200
+
201
+ def _on_L3(self, adb: AdbProtocol, scale_w: float, *, quick: bool = False) -> str | None:
202
+ """L3 handler: deepest layer — typically no further advance."""
203
+ raise NotImplementedError("subclass must override _on_L3")
204
+
205
+ def _call_on_layer(
206
+ self, layer_key: str, adb: AdbProtocol, scale_w: float, *, quick: bool
207
+ ) -> str | None:
208
+ i = self._layer_index(layer_key)
209
+ if i < 0:
210
+ LOG.error("_call_on_layer: unknown layer %s", layer_key)
211
+ return None
212
+ method = getattr(self, self._ON_METHODS[i])
213
+ return method(adb, scale_w, quick=quick)
214
+
215
+ # ── Helpers ───────────────────────────────────────────────────────────────
216
+
217
+ def _layer_index(self, layer_key: str) -> int:
218
+ for i, ld in enumerate(self.layers):
219
+ if ld.key == layer_key:
220
+ return i
221
+ return -1
222
+
223
+ def init(self, adb: AdbProtocol) -> None:
224
+ """One-time initialisation (optional)."""
225
+ pass
226
+
227
+ def _cold_start(
228
+ self, adb: AdbProtocol, target_layer: str, scale_w: float
229
+ ) -> None:
230
+ """Cold-start the target app (override in subclass)."""
231
+ pass
232
+
233
+ # ── Atomic API ────────────────────────────────────────────────────────────
234
+
235
+ def enter_next(
236
+ self,
237
+ adb: AdbProtocol,
238
+ scale_w: float,
239
+ *,
240
+ quick: bool = False,
241
+ max_wait_s: float = 8.0,
242
+ ) -> bool:
243
+ """Advance ONE layer from current position.
244
+
245
+ 1. detect current layer ← **guard** (pre-check)
246
+ 2. call _on_L[cur](quick) — handler does business + tap
247
+ 3. if handler returns None or same layer → stop (success)
248
+ 4. wait POST_TRANSITION_SLEEP, then detect ← **validator** (post-check)
249
+ 5. if not yet on target, poll with increasing intervals up to max_wait_s
250
+
251
+ This handles variable transition times (network loading, animations).
252
+ """
253
+ cur = self.detect(adb, scale_w)
254
+ target = self._call_on_layer(cur, adb, scale_w, quick=quick)
255
+ if target is None or target == cur:
256
+ return True
257
+
258
+ time.sleep(POST_TRANSITION_SLEEP)
259
+ next_cur = self.detect(adb, scale_w)
260
+ if next_cur == target:
261
+ self._notify_transition(cur, next_cur, "enter_next")
262
+ return True
263
+
264
+ poll_start = time.monotonic()
265
+ deadline = poll_start + max_wait_s
266
+ interval = 0.5
267
+ while time.monotonic() < deadline:
268
+ time.sleep(interval)
269
+ next_cur = self.detect(adb, scale_w)
270
+ if next_cur == target:
271
+ self._notify_transition(cur, next_cur, "enter_next")
272
+ return True
273
+ interval = min(interval + 0.5, 2.0)
274
+
275
+ elapsed = time.monotonic() - poll_start
276
+ LOG.warning(
277
+ "enter_next: %s→%s timeout after %.1fs — still on %s",
278
+ cur, target, max_wait_s, next_cur,
279
+ )
280
+ self._notify_timeout(cur, target, elapsed)
281
+ return False
282
+
283
+ def back_one(self, adb: AdbProtocol, scale_w: float) -> str:
284
+ """Send KEYCODE_BACK once, return new layer.
285
+
286
+ 1. detect current layer ← **guard** (pre-check)
287
+ 2. KEYCODE_BACK
288
+ 3. sleep, detect new layer ← **validator** (post-check)
289
+ """
290
+ cur = self.detect(adb, scale_w)
291
+ LOG.debug("back_one: from %s", cur)
292
+ adb.key_event(KEYCODE_BACK)
293
+ time.sleep(1.0)
294
+ next_cur = self.detect(adb, scale_w)
295
+ LOG.debug("back_one: %s → %s", cur, next_cur)
296
+ self._notify_transition(cur, next_cur, "back_one")
297
+ return next_cur
298
+
299
+ def back_recover(
300
+ self,
301
+ adb: AdbProtocol,
302
+ target_layer: str,
303
+ scale_w: float,
304
+ *,
305
+ target_page: str | None = None,
306
+ ) -> bool:
307
+ """Recover after BACK exhaustion: cold-start → fast-forward → page.
308
+
309
+ If *target_page* is given, calls :meth:`_recover_to_page` after
310
+ reaching *target_layer*.
311
+ """
312
+ LOG.warning("back_recover: cold-start → fast-forward → %s (page=%s)",
313
+ target_layer, target_page)
314
+ adb.key_event(KEYCODE_HOME)
315
+ time.sleep(0.8)
316
+ self._cold_start(adb, "L1", scale_w)
317
+
318
+ ok = self.advance(adb, target_layer, scale_w, quick=True)
319
+ if not ok:
320
+ self._notify_recovery(target_layer, False)
321
+ return False
322
+
323
+ if target_page is not None:
324
+ page_ok = self._recover_to_page(
325
+ target_layer, target_page, adb, scale_w,
326
+ )
327
+ if not page_ok:
328
+ LOG.error(
329
+ "back_recover: reached L%s but page=%s recovery failed",
330
+ target_layer, target_page,
331
+ )
332
+ self._notify_recovery(target_layer, False)
333
+ return False
334
+
335
+ result = self.detect(adb, scale_w) == target_layer
336
+ self._notify_recovery(target_layer, result)
337
+ return result
338
+
339
+ # ── Combined API ──────────────────────────────────────────────────────────
340
+
341
+ def back(
342
+ self, adb: AdbProtocol, to_layer: str, scale_w: float, *,
343
+ target_page: str | None = None,
344
+ ) -> bool:
345
+ """Retreat to *to_layer* via repeated BACK."""
346
+ for _ in range(3):
347
+ cur = self.detect(adb, scale_w)
348
+ if cur == to_layer:
349
+ self._call_on_layer(to_layer, adb, scale_w, quick=False)
350
+ if target_page is not None:
351
+ return self._recover_to_page(to_layer, target_page, adb, scale_w)
352
+ return True
353
+ if cur == "L0":
354
+ break
355
+ self.back_one(adb, scale_w)
356
+ return self.back_recover(adb, to_layer, scale_w, target_page=target_page)
357
+
358
+ def advance(
359
+ self, adb: AdbProtocol, target_layer: str, scale_w: float, *,
360
+ quick: bool = False,
361
+ max_wait_s: float = 8.0,
362
+ ) -> bool:
363
+ """Advance layer-by-layer to *target_layer*.
364
+
365
+ Uses :meth:`enter_next` for each step. *quick* is forwarded to
366
+ intermediate layers' handlers. At the target layer, handler is
367
+ always called with ``quick=False``.
368
+ """
369
+ while True:
370
+ cur = self.detect(adb, scale_w)
371
+ if cur == target_layer:
372
+ self._call_on_layer(target_layer, adb, scale_w, quick=False)
373
+ return True
374
+ ok = self.enter_next(adb, scale_w, quick=quick, max_wait_s=max_wait_s)
375
+ if not ok:
376
+ return False
377
+
378
+ def restore(
379
+ self, adb: AdbProtocol, target_layer: str, scale_w: float, *,
380
+ target_page: str | None = None,
381
+ ) -> bool:
382
+ """Restore to *target_layer* (and optionally *target_page*) from any position.
383
+
384
+ If already on *target_layer* but page mismatch, calls
385
+ :meth:`_recover_to_page` without cold-start.
386
+ """
387
+ cur = self.detect(adb, scale_w)
388
+ if cur == target_layer:
389
+ if target_page is not None:
390
+ return self._recover_to_page(target_layer, target_page, adb, scale_w)
391
+ return True
392
+ ci = self._layer_index(cur)
393
+ ti = self._layer_index(target_layer)
394
+ if ci > ti:
395
+ return self.back(adb, target_layer, scale_w, target_page=target_page)
396
+ else:
397
+ ok = self.advance(adb, target_layer, scale_w, quick=True)
398
+ if not ok:
399
+ return False
400
+ if target_page is not None:
401
+ return self._recover_to_page(target_layer, target_page, adb, scale_w)
402
+ return True
@@ -0,0 +1,224 @@
1
+ """Generic cold-start: launch an Android app from the launcher home screen.
2
+
3
+ Three paths tried in priority order:
4
+
5
+ 1. **monkey** — ``monkey -p <package> -c LAUNCHER 1`` (primary)
6
+ 2. **am start Intent** — ``am start -a MAIN -c LAUNCHER <package>`` (backup
7
+ for custom ROMs like MIUI / ColorOS that may restrict monkey)
8
+ 3. **Dock icon tap** — calculated via ``dock_app_icon_coords`` (last resort,
9
+ with 0.5 s pre‑wait and up to 2 retries)
10
+
11
+ After the app enters foreground, optionally taps a session tab to reach the
12
+ app's main content list (e.g. WeChat's bottom "微信" tab).
13
+
14
+ Screen dimensions are always auto‑detected via ``adb shell wm size``.
15
+
16
+ .. code-block:: python
17
+
18
+ from layernav_android.cold_start import cold_start_app_from_launcher
19
+
20
+ ok = cold_start_app_from_launcher(
21
+ adb, "com.tencent.mm",
22
+ app_name="wechat", M=4, N=3,
23
+ session_tab_x=108, session_tab_y=2192,
24
+ )
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ import re
31
+ import time
32
+
33
+ from layernav_android._protocol import AdbProtocol
34
+
35
+ LOG = logging.getLogger("layernav.cold_start")
36
+
37
+ _SZ_RE = re.compile(r"(\d{3,})\s*x\s*(\d{3,})")
38
+
39
+ APP_DEFAULTS: dict[str, dict[str, int]] = {
40
+ "wechat": {"M": 4, "N": 3},
41
+ "xhs": {"M": 4, "N": 1},
42
+ }
43
+
44
+ _DOCK_RETRIES = 2
45
+ _DOCK_PRE_WAIT_S = 0.5
46
+
47
+
48
+ def dock_app_icon_coords(
49
+ screen_w: int,
50
+ screen_h: int,
51
+ scale_w: float,
52
+ *,
53
+ app_name: str = "wechat",
54
+ M: int = 4,
55
+ N: int | None = None,
56
+ ) -> tuple[int, int]:
57
+ """Calculate the centre of the *N*-th Dock slot (1‑indexed) in a Dock with *M* equal-width slots.
58
+
59
+ - *M*: total number of Dock slots (default 4).
60
+ - *N*: 1‑based slot index; defaults from ``APP_DEFAULTS`` (wechat→3, xhs→1).
61
+ - Formula: ``x = round(W * (N - 0.5) / M)``.
62
+ """
63
+ if N is None:
64
+ defaults = APP_DEFAULTS.get(app_name, {})
65
+ N = defaults.get("N", 1)
66
+ M = defaults.get("M", M)
67
+ N = max(1, min(M, N))
68
+ pad_x = max(12, screen_w // 8)
69
+ dx = int(round(screen_w * (N - 0.5) / M))
70
+ dx = max(pad_x, min(screen_w - pad_x, dx))
71
+ dy = screen_h - max(48, int(round(52 * max(scale_w, 1e-6))))
72
+ return dx, dy
73
+
74
+
75
+ def cold_start_app_from_launcher(
76
+ adb: AdbProtocol,
77
+ package: str,
78
+ *,
79
+ app_name: str = "wechat",
80
+ M: int = 4,
81
+ N: int | None = None,
82
+ session_tab_x: int | None = None,
83
+ session_tab_y: int | None = None,
84
+ force_stop_before: bool = True,
85
+ deadline_s: float = 25.0,
86
+ ) -> bool:
87
+ """Cold-start *package* from the Android launcher and optionally tap a session tab.
88
+
89
+ Screen dimensions are always auto‑detected via ``adb shell wm size``.
90
+
91
+ Args:
92
+ adb: :class:`AdbProtocol` client.
93
+ package: Android package name (e.g. ``"com.tencent.mm"``).
94
+ app_name: key in :data:`APP_DEFAULTS` — drives default *M* / *N*.
95
+ M: number of Dock slots.
96
+ N: 1‑based slot index where the app icon lives.
97
+ session_tab_x, session_tab_y: if both are non-``None``, tap this
98
+ coordinate after the app enters foreground (e.g. WeChat's
99
+ 「微信」 bottom tab).
100
+ force_stop_before: issue ``am force-stop`` before cold-start.
101
+ deadline_s: maximum time budget for the whole cold-start.
102
+
103
+ Returns:
104
+ ``True`` if *package* is the foreground app after cold-start.
105
+ """
106
+ screen_w, screen_h = _resolve_screen_size(adb)
107
+ scale_w = screen_w / 1080.0
108
+ LOG.info("cold_start: screen %dx%d scale_w=%.3f", screen_w, screen_h, scale_w)
109
+
110
+ deadline = time.monotonic() + deadline_s
111
+
112
+ if force_stop_before:
113
+ LOG.info("cold_start: force-stop %s", package)
114
+ try:
115
+ adb._run(["shell", "am", "force-stop", package])
116
+ except Exception:
117
+ LOG.warning("cold_start: force-stop %s failed (non-fatal)", package)
118
+ time.sleep(0.65)
119
+
120
+ # -- path 1: monkey LAUNCHER --
121
+ if _try_monkey(adb, package):
122
+ time.sleep(1.5)
123
+ _tap_session_tab(adb, session_tab_x, session_tab_y)
124
+ if time.monotonic() < deadline and _check_foreground(adb, package):
125
+ return True
126
+
127
+ # -- path 2: am start Intent (backup for custom ROMs) --
128
+ if _try_am_start(adb, package):
129
+ time.sleep(1.5)
130
+ _tap_session_tab(adb, session_tab_x, session_tab_y)
131
+ if time.monotonic() < deadline and _check_foreground(adb, package):
132
+ return True
133
+
134
+ # -- path 3: Dock icon tap (last resort, with pre-wait + retry) --
135
+ dx, dy = dock_app_icon_coords(
136
+ screen_w, screen_h, scale_w, app_name=app_name, M=M, N=N,
137
+ )
138
+ if _try_dock_tap_with_retry(adb, package, dx, dy, session_tab_x, session_tab_y):
139
+ if time.monotonic() < deadline and _check_foreground(adb, package):
140
+ return True
141
+
142
+ return False
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Internal helpers
147
+ # ---------------------------------------------------------------------------
148
+
149
+
150
+ def _resolve_screen_size(adb: AdbProtocol) -> tuple[int, int]:
151
+ out = adb._run(["shell", "wm", "size"])
152
+ m = _SZ_RE.search(out)
153
+ if m:
154
+ return int(m.group(1)), int(m.group(2))
155
+ return 1080, 1920
156
+
157
+
158
+ def _try_monkey(adb: AdbProtocol, package: str) -> bool:
159
+ LOG.info("cold_start: monkey LAUNCHER for %s", package)
160
+ try:
161
+ adb._run(
162
+ ["shell", "monkey", "-p", package, "-c",
163
+ "android.intent.category.LAUNCHER", "1"],
164
+ )
165
+ return True
166
+ except Exception as exc:
167
+ LOG.warning("cold_start: monkey failed (%s)", exc)
168
+ return False
169
+
170
+
171
+ def _try_am_start(adb: AdbProtocol, package: str) -> bool:
172
+ LOG.info("cold_start: am start LAUNCHER for %s", package)
173
+ try:
174
+ adb._run(
175
+ ["shell", "am", "start",
176
+ "-a", "android.intent.action.MAIN",
177
+ "-c", "android.intent.category.LAUNCHER",
178
+ package],
179
+ )
180
+ return True
181
+ except Exception as exc:
182
+ LOG.warning("cold_start: am start failed (%s)", exc)
183
+ return False
184
+
185
+
186
+ def _tap_session_tab(
187
+ adb: AdbProtocol,
188
+ session_tab_x: int | None,
189
+ session_tab_y: int | None,
190
+ ) -> None:
191
+ if session_tab_x is not None and session_tab_y is not None:
192
+ LOG.info("cold_start: tap session tab (%d, %d)", session_tab_x, session_tab_y)
193
+ adb.tap(session_tab_x, session_tab_y)
194
+ time.sleep(0.55)
195
+
196
+
197
+ def _try_dock_tap_with_retry(
198
+ adb: AdbProtocol,
199
+ package: str,
200
+ dx: int,
201
+ dy: int,
202
+ session_tab_x: int | None,
203
+ session_tab_y: int | None,
204
+ ) -> bool:
205
+ LOG.info("cold_start: Dock tap at (%d, %d) for %s (pre-wait %.1fs, max %d retries)",
206
+ dx, dy, package, _DOCK_PRE_WAIT_S, _DOCK_RETRIES)
207
+ for attempt in range(1 + _DOCK_RETRIES):
208
+ if attempt > 0:
209
+ time.sleep(_DOCK_PRE_WAIT_S)
210
+ adb.tap(dx, dy)
211
+ time.sleep(1.2)
212
+ _tap_session_tab(adb, session_tab_x, session_tab_y)
213
+ if _check_foreground(adb, package):
214
+ LOG.info("cold_start: Dock tap succeeded on attempt %d", attempt + 1)
215
+ return True
216
+ LOG.warning("cold_start: Dock tap attempt %d failed", attempt + 1)
217
+ return False
218
+
219
+
220
+ def _check_foreground(adb: AdbProtocol, package: str) -> bool:
221
+ try:
222
+ return adb.foreground_package() == package
223
+ except Exception:
224
+ return False
File without changes