idevice 0.1.0__tar.gz

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.
Files changed (38) hide show
  1. idevice-0.1.0/.cursor/rules/AGENTS.md +55 -0
  2. idevice-0.1.0/.github/workflows/workflow.yml +49 -0
  3. idevice-0.1.0/.gitignore +25 -0
  4. idevice-0.1.0/.python-version +1 -0
  5. idevice-0.1.0/AGENTS.md +13 -0
  6. idevice-0.1.0/PKG-INFO +17 -0
  7. idevice-0.1.0/README.md +5 -0
  8. idevice-0.1.0/examples/README.md +34 -0
  9. idevice-0.1.0/examples/android_device_install.py +131 -0
  10. idevice-0.1.0/idevice/__init__.py +5 -0
  11. idevice-0.1.0/idevice/device/__init__.py +29 -0
  12. idevice-0.1.0/idevice/device/android/__init__.py +5 -0
  13. idevice-0.1.0/idevice/device/android/device.py +217 -0
  14. idevice-0.1.0/idevice/device/base/__init__.py +20 -0
  15. idevice-0.1.0/idevice/device/base/device.py +151 -0
  16. idevice-0.1.0/idevice/device/base/errors.py +32 -0
  17. idevice-0.1.0/idevice/device/base/runner.py +83 -0
  18. idevice-0.1.0/idevice/device/cache.py +52 -0
  19. idevice-0.1.0/idevice/device/config.py +26 -0
  20. idevice-0.1.0/idevice/device/factory.py +37 -0
  21. idevice-0.1.0/idevice/device/ios/__init__.py +6 -0
  22. idevice-0.1.0/idevice/device/ios/device.py +202 -0
  23. idevice-0.1.0/idevice/device/windows/__init__.py +5 -0
  24. idevice-0.1.0/idevice/device/windows/device.py +155 -0
  25. idevice-0.1.0/idevice/uiauto/__init__.py +15 -0
  26. idevice-0.1.0/idevice/uiauto/android/__init__.py +5 -0
  27. idevice-0.1.0/idevice/uiauto/android/automation.py +100 -0
  28. idevice-0.1.0/idevice/uiauto/android/dialogs.py +44 -0
  29. idevice-0.1.0/idevice/uiauto/android/hierarchy.py +87 -0
  30. idevice-0.1.0/idevice/uiauto/base/__init__.py +6 -0
  31. idevice-0.1.0/idevice/uiauto/base/automation.py +43 -0
  32. idevice-0.1.0/idevice/uiauto/base/errors.py +9 -0
  33. idevice-0.1.0/idevice/uiauto/config.py +15 -0
  34. idevice-0.1.0/idevice/uiauto/factory.py +27 -0
  35. idevice-0.1.0/pyproject.toml +38 -0
  36. idevice-0.1.0/tests/test_config.py +27 -0
  37. idevice-0.1.0/tests/test_factory.py +23 -0
  38. idevice-0.1.0/uv.lock +351 -0
@@ -0,0 +1,55 @@
1
+ ---
2
+ description: "Cursor rules for Python development with projects guide integration."
3
+ globs: **/*
4
+ alwaysApply: true
5
+ ---
6
+ You are an AI assistant specialized in Python development. Your approach emphasizes:
7
+
8
+ 1. Clear project structure with monorepo.
9
+ 3. Configuration management using environment variables.
10
+ 4. Robust error handling and logging use fstring, including context capture.
11
+ 5. Comprehensive testing with pytest.
12
+ 6. Detailed documentation using docstrings and README files.
13
+ 7. Dependency management via https://docs.astral.sh/uv and virtual environments.
14
+ 8. Code style consistency using Ruff.
15
+ 9. CI/CD implementation with GitHub Actions or GitLab CI.
16
+ 10. AI-friendly coding practices:
17
+ - Descriptive variable and function names
18
+ - Type hints
19
+ - Detailed comments for complex logic
20
+ - Rich error context for debugging
21
+
22
+ You provide code snippets and explanations tailored to these principles, optimizing for clarity and AI-assisted development.
23
+
24
+ ## Project Structure
25
+ - Use src-layout with `src/your_package_name/`
26
+ - Place tests in `tests/` directory parallel to `src/`
27
+ - Keep configuration in `config/` or as environment variables
28
+ - Store requirements in `pyproject.toml`
29
+ - Place static files in `static/` directory
30
+
31
+ ## Code Style
32
+ - Follow Black code formatting
33
+ - Use isort for import sorting
34
+ - Follow PEP 8 naming conventions:
35
+ - snake_case for functions and variables
36
+ - PascalCase for classes
37
+ - UPPER_CASE for constants
38
+ - Maximum line length of 88 characters (Black default)
39
+ - Use absolute imports over relative imports
40
+
41
+ ## Type Hints
42
+ - Use type hints for all function parameters and returns
43
+ - Import types from `typing` module
44
+ - Use `Optional[Type]` instead of `Type | None`
45
+ - Use `TypeVar` for generic types
46
+ - Define custom types in `types.py`
47
+ - Use `Protocol` for duck typing
48
+
49
+ ## Documentation
50
+ - Use Google-style docstrings
51
+ - Document all public APIs
52
+ - Keep README.md updated
53
+ - Use proper inline comments
54
+ - Generate API documentation
55
+ - Document environment setup
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v6
18
+ with:
19
+ python-version: "3.14"
20
+ enable-cache: true
21
+
22
+ - name: Install dependencies
23
+ run: uv sync --extra dev --frozen
24
+
25
+ - name: Lint
26
+ run: uv run ruff check .
27
+
28
+ - name: Test
29
+ run: uv run pytest --import-mode=importlib
30
+
31
+ publish:
32
+ needs: test
33
+ runs-on: ubuntu-latest
34
+ environment: pypi
35
+ permissions:
36
+ id-token: write
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+
40
+ - name: Install uv
41
+ uses: astral-sh/setup-uv@v6
42
+ with:
43
+ python-version: "3.14"
44
+
45
+ - name: Build
46
+ run: uv build
47
+
48
+ - name: Publish to PyPI
49
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,25 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # IDEs
13
+ .vscode/
14
+ .idea/
15
+
16
+ # OS generated files
17
+ .DS_Store
18
+ Thumbs.db
19
+ ehthumbs.db
20
+ Desktop.ini
21
+ Icon?
22
+ $RECYCLE.BIN/
23
+ $SystemVolumeInformation/
24
+ $Windows.~WS/
25
+ $Windows.~WS/
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,13 @@
1
+ # Agents
2
+
3
+ This repository uses VS Code Copilot agents and customization files to assist with development tasks.
4
+
5
+ ## Purpose
6
+
7
+ - Document available agents and how to use them in this workspace.
8
+ - Provide a reference for contributors.
9
+
10
+ ## Notes
11
+
12
+ - The workspace contains a `.cursor/rules/AGENTS.md` file for agent behavior rules.
13
+ - Add any high-level agent usage or customization details here as needed.
idevice-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: idevice
3
+ Version: 0.1.0
4
+ Summary: Cross-platform device automation for end-to-end test workflows
5
+ Requires-Python: >=3.14
6
+ Requires-Dist: uiautomator2>=3.5.2
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest-mock>=3.14; extra == 'dev'
9
+ Requires-Dist: pytest>=8.0; extra == 'dev'
10
+ Requires-Dist: ruff>=0.9; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # idevice
14
+
15
+ Cross-platform device automation for end-to-end test workflows: install and manage apps on physical devices, and drive UI interactions through a small, platform-agnostic API.
16
+
17
+ **Status:** iOS is implemented (go-ios for package management, WebDriverAgent for UI). Android, Windows, macOS, and HarmonyOS are planned.
@@ -0,0 +1,5 @@
1
+ # idevice
2
+
3
+ Cross-platform device automation for end-to-end test workflows: install and manage apps on physical devices, and drive UI interactions through a small, platform-agnostic API.
4
+
5
+ **Status:** iOS is implemented (go-ios for package management, WebDriverAgent for UI). Android, Windows, macOS, and HarmonyOS are planned.
@@ -0,0 +1,34 @@
1
+ # Examples
2
+
3
+ ## Android: install an APK
4
+
5
+ Install a package on a connected device and verify it with `pm list packages`:
6
+
7
+ ```bash
8
+ uv run python examples/android_device_install.py \
9
+ --serial "$(adb devices | awk 'NR>2 && $2=="device" {print $1; exit}')" \
10
+ --apk tests/apk/app.apk \
11
+ --package com.Unity.TrashDash
12
+ ```
13
+
14
+ Optional: dismiss OEM post-install dialogs after install:
15
+
16
+ ```bash
17
+ uv run python examples/android_device_install.py \
18
+ --serial e8b2b043 \
19
+ --apk tests/apk/app.apk \
20
+ --package com.Unity.TrashDash \
21
+ --dismiss-dialogs
22
+ ```
23
+
24
+ Minimal Python usage:
25
+
26
+ ```python
27
+ from pathlib import Path
28
+
29
+ from idevice.device.android.device import AndroidDevice
30
+
31
+ device = AndroidDevice("e8b2b043")
32
+ device.install(Path("tests/apk/app.apk"), app_id="com.Unity.TrashDash")
33
+ assert device.is_installed("com.Unity.TrashDash")
34
+ ```
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env python3
2
+ """Install an APK on a connected Android device using :class:`AndroidDevice`.
3
+
4
+ Prerequisites:
5
+ - ``adb`` on PATH (or set ``IDEVICE_ADB_BINARY``)
6
+ - Device visible in ``adb devices`` with state ``device``
7
+ - ``uiautomator2`` (installed with idevice)
8
+
9
+ Example:
10
+ uv run python examples/android_device_install.py \\
11
+ --serial e8b2b043 \\
12
+ --apk tests/apk/app.apk \\
13
+ --package com.Unity.TrashDash
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import logging
20
+ import subprocess
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ from idevice.device.android.device import AndroidDevice, AndroidDeviceError
25
+ from idevice.device.config import adb_binary
26
+ from idevice.uiauto.android.automation import AndroidUIAuto
27
+
28
+ REPO_ROOT = Path(__file__).resolve().parent.parent
29
+ DEFAULT_APK = REPO_ROOT / "tests" / "apk" / "app.apk"
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def _resolve_serial(explicit: str | None) -> str:
35
+ if explicit:
36
+ return explicit
37
+ try:
38
+ completed = subprocess.run(
39
+ [adb_binary(), "devices"],
40
+ capture_output=True,
41
+ text=True,
42
+ timeout=30,
43
+ check=True,
44
+ )
45
+ except (OSError, subprocess.TimeoutExpired) as exc:
46
+ raise SystemExit(f"Failed to list devices: {exc}") from exc
47
+
48
+ for line in completed.stdout.splitlines():
49
+ parts = line.split()
50
+ if len(parts) >= 2 and parts[1] == "device":
51
+ return parts[0]
52
+ raise SystemExit("No connected device found. Pass --serial or connect a device via adb.")
53
+
54
+
55
+ def main(argv: list[str] | None = None) -> int:
56
+ parser = argparse.ArgumentParser(
57
+ description="Install an APK on an Android device via idevice.",
58
+ )
59
+ parser.add_argument(
60
+ "--serial",
61
+ help="adb device serial (default: first device in `adb devices`)",
62
+ )
63
+ parser.add_argument(
64
+ "--apk",
65
+ type=Path,
66
+ default=DEFAULT_APK,
67
+ help=f"Path to the APK (default: {DEFAULT_APK.relative_to(REPO_ROOT)})",
68
+ )
69
+ parser.add_argument(
70
+ "--package",
71
+ required=True,
72
+ help="Android package name (applicationId), e.g. com.example.app",
73
+ )
74
+ parser.add_argument(
75
+ "--dismiss-dialogs",
76
+ action="store_true",
77
+ help="Dismiss OEM post-install dialogs after install",
78
+ )
79
+ parser.add_argument(
80
+ "-v",
81
+ "--verbose",
82
+ action="store_true",
83
+ help="Enable debug logging",
84
+ )
85
+ args = parser.parse_args(argv)
86
+
87
+ logging.basicConfig(
88
+ level=logging.DEBUG if args.verbose else logging.INFO,
89
+ format="%(levelname)s %(message)s",
90
+ )
91
+
92
+ serial = _resolve_serial(args.serial)
93
+ apk_path = args.apk.resolve()
94
+ if not apk_path.is_file():
95
+ logger.error("APK not found: %s", apk_path)
96
+ return 1
97
+
98
+ try:
99
+ device = AndroidDevice(serial)
100
+ except AndroidDeviceError as exc:
101
+ logger.error("%s", exc)
102
+ return 1
103
+
104
+ logger.info("Installing %s on %s", apk_path.name, serial)
105
+ try:
106
+ device.install(apk_path, app_id=args.package)
107
+ except (AndroidDeviceError, FileNotFoundError) as exc:
108
+ logger.error("Install failed: %s", exc)
109
+ return 1
110
+
111
+ if not device.is_installed(args.package):
112
+ logger.error("Package %s not reported as installed", args.package)
113
+ return 1
114
+
115
+ pkg_file = device.get_installed_pkg_name(args.package)
116
+ logger.info(
117
+ "Installed %s (recorded package file: %s)",
118
+ args.package,
119
+ pkg_file or apk_path.name,
120
+ )
121
+
122
+ if args.dismiss_dialogs:
123
+ uiauto = AndroidUIAuto(serial)
124
+ dismissed = uiauto.dismiss_post_install_dialogs()
125
+ logger.info("Dismissed %s post-install dialog(s)", dismissed)
126
+
127
+ return 0
128
+
129
+
130
+ if __name__ == "__main__":
131
+ sys.exit(main())
@@ -0,0 +1,5 @@
1
+ """Cross-platform device automation library."""
2
+
3
+ from idevice import device, uiauto
4
+
5
+ __all__ = ["device", "uiauto"]
@@ -0,0 +1,29 @@
1
+ """Public API for ``DeviceBase`` and platform-specific device implementations."""
2
+
3
+ from idevice.device.android.device import AndroidDevice
4
+ from idevice.device.base.device import DeviceBase
5
+ from idevice.device.base.errors import (
6
+ AppNotInstalledError,
7
+ CommandExecutionError,
8
+ DeviceError,
9
+ DeviceNotFoundError,
10
+ )
11
+ from idevice.device.base.runner import CommandResult, SubprocessRunner
12
+ from idevice.device.factory import Platform, create_device
13
+ from idevice.device.ios.device import IOSDevice
14
+ from idevice.device.windows.device import WindowsDevice
15
+
16
+ __all__ = [
17
+ "AndroidDevice",
18
+ "AppNotInstalledError",
19
+ "CommandExecutionError",
20
+ "CommandResult",
21
+ "DeviceError",
22
+ "DeviceNotFoundError",
23
+ "DeviceBase",
24
+ "IOSDevice",
25
+ "Platform",
26
+ "SubprocessRunner",
27
+ "WindowsDevice",
28
+ "create_device",
29
+ ]
@@ -0,0 +1,5 @@
1
+ """Android ``DeviceBase`` implementation."""
2
+
3
+ from idevice.device.android.device import AndroidDevice
4
+
5
+ __all__ = ["AndroidDevice"]
@@ -0,0 +1,217 @@
1
+ """Android ``DeviceBase`` implementation via adb."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from idevice.device.base.device import DeviceBase
13
+ from idevice.device.base.errors import AppNotInstalledError
14
+ from idevice.device.base.runner import SubprocessRunner
15
+ from idevice.device.config import adb_binary
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class InstallResult:
22
+ ok: bool
23
+ returncode: int
24
+ stdout: str
25
+ stderr: str
26
+
27
+
28
+ class AndroidDeviceError(RuntimeError):
29
+ """Raised when an Android device operation fails."""
30
+
31
+
32
+ class AndroidDevice(DeviceBase):
33
+ """``DeviceBase`` implementation for Android using adb."""
34
+
35
+ DEFAULT_BINARY = "adb"
36
+
37
+ def __init__(self, device_id: str) -> None:
38
+ super().__init__(device_id)
39
+ self._binary = self.DEFAULT_BINARY
40
+ self._runner = SubprocessRunner()
41
+ if shutil.which(self._binary) is None:
42
+ logger.error(f"[AndroidDevice] `{self._binary}` CLI not found on PATH")
43
+ raise AndroidDeviceError(
44
+ f"`{self._binary}` CLI not found on PATH. "
45
+ "Install adb: https://developer.android.com/studio/releases/platform-tools"
46
+ )
47
+ self._installed_pkg_names: dict[str, str] = {}
48
+
49
+ def _base_command(self) -> list[str]:
50
+ return [adb_binary(), "-s", self.device_id]
51
+
52
+ def install(self, package_path: Path, app_id: str | None = None) -> None:
53
+ """Install an APK on the bound device via uiautomator2.
54
+
55
+ Example::
56
+
57
+ from pathlib import Path
58
+
59
+ from idevice.device.android.device import AndroidDevice
60
+
61
+ device = AndroidDevice("e8b2b043")
62
+ apk = Path("tests/apk/app.apk")
63
+ device.install(apk, app_id="com.example.app")
64
+ assert device.is_installed("com.example.app")
65
+ """
66
+ logger.info(f"[AndroidDevice] Installing package on {self.device_id}: {package_path}")
67
+ if not package_path.exists():
68
+ raise FileNotFoundError(f"Package not found: {package_path}")
69
+
70
+ try:
71
+ cmd = self._base_command()
72
+ cmd.extend(["install", "-r", str(package_path)])
73
+ result = self._install_with_uiautomator2(cmd, device_id=self.device_id)
74
+ if not result.ok:
75
+ raise AndroidDeviceError(f"Package install failed on {self.device_id}: {result.stderr}")
76
+ except Exception as exc:
77
+ raise AndroidDeviceError(f"Package install failed on {self.device_id}: {exc}") from exc
78
+
79
+ if app_id:
80
+ self._installed_pkg_names[app_id] = package_path.name
81
+ logger.debug(f"[AndroidDevice] Cached package name for app_id={app_id}")
82
+
83
+ def uninstall(self, app_id: str) -> None:
84
+ logger.info(f"[AndroidDevice] Uninstalling {app_id} on {self.device_id}")
85
+ command = self._base_command()
86
+ command.extend(["uninstall", app_id])
87
+ self._runner.run(command)
88
+ self._installed_pkg_names.pop(app_id, None)
89
+
90
+ def is_installed(self, app_id: str) -> bool:
91
+ command = self._base_command()
92
+ command.extend(["shell", "pm", "list", "packages", app_id])
93
+ result = self._runner.run(command)
94
+ prefix = f"package:{app_id}"
95
+ installed = any(line.strip() == prefix for line in result.stdout.splitlines())
96
+ logger.debug(f"App {app_id} installed on Android device {self.device_id}: {installed}")
97
+ return installed
98
+
99
+ def launch_app(self, app_id: str) -> None:
100
+ if not app_id:
101
+ raise ValueError("app_id is required and must be a non-empty string")
102
+ if not self.is_installed(app_id):
103
+ raise AppNotInstalledError(f"App not installed: {app_id}")
104
+ logger.info(f"[AndroidDevice] Launching {app_id} on {self.device_id}")
105
+ command = self._base_command()
106
+ command.extend(
107
+ [
108
+ "shell",
109
+ "monkey",
110
+ "-p",
111
+ app_id,
112
+ "-c",
113
+ "android.intent.category.LAUNCHER",
114
+ "1",
115
+ ]
116
+ )
117
+ self._runner.run(command)
118
+
119
+ def stop_app(self, app_id: str) -> None:
120
+ if not app_id:
121
+ raise ValueError("app_id is required and must be a non-empty string")
122
+ logger.info(f"Stopping app on Android device {self.device_id}: {app_id}")
123
+ command = self._base_command()
124
+ command.extend(["shell", "am", "force-stop", app_id])
125
+ self._runner.run(command)
126
+
127
+ def get_installed_pkg_name(self, app_id: str) -> str | None:
128
+ if not self.is_installed(app_id):
129
+ return None
130
+ return self._installed_pkg_names.get(app_id)
131
+
132
+ def host_is_running(self) -> bool:
133
+ return True
134
+
135
+ def push(
136
+ self,
137
+ local: Path | str,
138
+ remote: str,
139
+ *,
140
+ app_id: str | None = None,
141
+ ) -> None:
142
+ """Push a local file or directory to the device via ``adb push``."""
143
+ del app_id
144
+ if not remote:
145
+ raise ValueError("remote is required and must be a non-empty string")
146
+ local_path = Path(local)
147
+ if not local_path.exists():
148
+ raise FileNotFoundError(f"Local path not found: {local_path}")
149
+ logger.info(f"[AndroidDevice] Pushing {local_path} to {self.device_id}:{remote}")
150
+ command = self._base_command()
151
+ command.extend(["push", str(local_path), remote])
152
+ self._runner.run(command)
153
+
154
+ def pull(
155
+ self,
156
+ remote: str,
157
+ local: Path | str,
158
+ *,
159
+ app_id: str | None = None,
160
+ ) -> None:
161
+ """Pull a remote file or directory from the device via ``adb pull``."""
162
+ del app_id
163
+ if not remote:
164
+ raise ValueError("remote is required and must be a non-empty string")
165
+ local_path = Path(local)
166
+ local_path.parent.mkdir(parents=True, exist_ok=True)
167
+ logger.info(f"[AndroidDevice] Pulling {self.device_id}:{remote} to {local_path}")
168
+ command = self._base_command()
169
+ command.extend(["pull", remote, str(local_path)])
170
+ self._runner.run(command)
171
+
172
+ def _install_with_uiautomator2(self, cmd: list[str], *, device_id: str | None) -> InstallResult:
173
+ """Use uiautomator2 WatchContext (builtin + extra) while adb install runs."""
174
+ import uiautomator2 as u2
175
+
176
+ logger.info(f"install with uiautomator2: {cmd}")
177
+ d = u2.connect(device_id) if device_id else u2.connect()
178
+ # autostart=False so we can register rules before the background thread runs.
179
+ with d.watch_context(builtin=True, autostart=False) as ctx:
180
+ # Builtin rules cover common install prompts (继续安装, ALLOW, Agree, …).
181
+ ctx.when("仍要安装").click()
182
+ ctx.when("Install").click()
183
+ ctx.start()
184
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
185
+
186
+ if p.returncode == 0:
187
+ self._dismiss_post_install_popups(d)
188
+
189
+ out = (p.stdout or "").strip()
190
+ err = (p.stderr or "").strip()
191
+ ok = p.returncode == 0
192
+ return InstallResult(ok, p.returncode, out, err)
193
+
194
+ def _dismiss_post_install_popups(self, d) -> None:
195
+ """Dismiss OEM dialogs after adb install; watcher already stopped."""
196
+ stable = float(os.environ.get("GAUTO_APK_POST_INSTALL_STABLE_SEC", "2"))
197
+ timeout_sec = float(os.environ.get("GAUTO_APK_POST_INSTALL_TIMEOUT_SEC", "30"))
198
+ with d.watch_context(builtin=True, autostart=False) as ctx:
199
+ # Prefer dismiss over launching the app when both exist.
200
+ ctx.when("完成").click()
201
+ ctx.when("完成安装").click()
202
+ ctx.when("知道了").click()
203
+ ctx.when("我知道了").click()
204
+ ctx.when("以后再说").click()
205
+ ctx.when("稍后").click()
206
+ ctx.when("暂不").click()
207
+ ctx.when("跳过").click()
208
+ ctx.when("关闭").click()
209
+ ctx.when("Done").click()
210
+ ctx.when("OPEN").click()
211
+ ctx.when("打开").click()
212
+ ctx.when("立即打开").click()
213
+ ctx.start()
214
+ try:
215
+ ctx.wait_stable(seconds=stable, timeout=timeout_sec)
216
+ except TimeoutError:
217
+ logger.debug(f"post-install popups did not stabilize within {timeout_sec}s")
@@ -0,0 +1,20 @@
1
+ """``DeviceBase`` and shared utilities for device control."""
2
+
3
+ from idevice.device.base.device import DeviceBase
4
+ from idevice.device.base.errors import (
5
+ AppNotInstalledError,
6
+ CommandExecutionError,
7
+ DeviceError,
8
+ DeviceNotFoundError,
9
+ )
10
+ from idevice.device.base.runner import CommandResult, SubprocessRunner
11
+
12
+ __all__ = [
13
+ "AppNotInstalledError",
14
+ "CommandExecutionError",
15
+ "CommandResult",
16
+ "DeviceError",
17
+ "DeviceNotFoundError",
18
+ "DeviceBase",
19
+ "SubprocessRunner",
20
+ ]