autoglm-gui 0.4.8__tar.gz → 0.4.11__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 (55) hide show
  1. autoglm_gui-0.4.11/AutoGLM_GUI/__init__.py +52 -0
  2. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/__main__.py +63 -15
  3. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/adb_plus/__init__.py +2 -0
  4. autoglm_gui-0.4.11/AutoGLM_GUI/adb_plus/keyboard_installer.py +380 -0
  5. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/api/agents.py +123 -1
  6. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/api/media.py +39 -44
  7. autoglm_gui-0.4.11/AutoGLM_GUI/config_manager.py +124 -0
  8. autoglm_gui-0.4.11/AutoGLM_GUI/logger.py +85 -0
  9. autoglm_gui-0.4.11/AutoGLM_GUI/platform_utils.py +43 -0
  10. autoglm_gui-0.4.11/AutoGLM_GUI/resources/apks/ADBKeyBoard.LICENSE.txt +339 -0
  11. autoglm_gui-0.4.11/AutoGLM_GUI/resources/apks/ADBKeyBoard.README.txt +1 -0
  12. autoglm_gui-0.4.11/AutoGLM_GUI/resources/apks/ADBKeyboard.apk +0 -0
  13. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/schemas.py +17 -0
  14. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/scrcpy_stream.py +58 -118
  15. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/state.py +2 -1
  16. autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/about-BI6OV6gm.js → autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/about-wSo3UgQ-.js +1 -1
  17. autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/chat-BcY2K0yj.js +25 -0
  18. autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/index-Do7ha9Kf.js → autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/index-B5u1xtK1.js +1 -1
  19. autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/index-CHrYo3Qj.css +1 -0
  20. autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/index-Dn3vR6uV.js → autoglm_gui-0.4.11/AutoGLM_GUI/static/assets/index-D5BALRbT.js +5 -5
  21. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/static/index.html +2 -2
  22. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/PKG-INFO +17 -2
  23. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/README.md +14 -1
  24. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/pyproject.toml +3 -1
  25. autoglm_gui-0.4.8/AutoGLM_GUI/__init__.py +0 -9
  26. autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/chat-C_2Cot0q.js +0 -25
  27. autoglm_gui-0.4.8/AutoGLM_GUI/static/assets/index-DCrxTz-A.css +0 -1
  28. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/.gitignore +0 -0
  29. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/adb_plus/screenshot.py +0 -0
  30. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/adb_plus/touch.py +0 -0
  31. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/api/__init__.py +0 -0
  32. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/api/control.py +0 -0
  33. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/api/devices.py +0 -0
  34. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/config.py +0 -0
  35. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/server.py +0 -0
  36. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/AutoGLM_GUI/version.py +0 -0
  37. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/LICENSE +0 -0
  38. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/__init__.py +0 -0
  39. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/actions/__init__.py +0 -0
  40. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/actions/handler.py +0 -0
  41. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/adb/__init__.py +0 -0
  42. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/adb/connection.py +0 -0
  43. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/adb/device.py +0 -0
  44. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/adb/input.py +0 -0
  45. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/adb/screenshot.py +0 -0
  46. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/agent.py +0 -0
  47. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/config/__init__.py +0 -0
  48. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/config/apps.py +0 -0
  49. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/config/i18n.py +0 -0
  50. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/config/prompts.py +0 -0
  51. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/config/prompts_en.py +0 -0
  52. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/config/prompts_zh.py +0 -0
  53. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/model/__init__.py +0 -0
  54. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/phone_agent/model/client.py +0 -0
  55. {autoglm_gui-0.4.8 → autoglm_gui-0.4.11}/scrcpy-server-v3.3.3 +0 -0
@@ -0,0 +1,52 @@
1
+ """AutoGLM-GUI package metadata."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from functools import wraps
6
+ from importlib import metadata
7
+
8
+ # ============================================================================
9
+ # Fix Windows encoding issue: Force UTF-8 for all subprocess calls
10
+ # ============================================================================
11
+ # On Windows, subprocess defaults to GBK encoding which fails when ADB/scrcpy
12
+ # output UTF-8 characters. This monkey patch ensures all subprocess calls
13
+ # use UTF-8 encoding by default.
14
+
15
+ _original_run = subprocess.run
16
+ _original_popen = subprocess.Popen
17
+
18
+
19
+ @wraps(_original_run)
20
+ def _patched_run(*args, **kwargs):
21
+ """Patched subprocess.run that defaults to UTF-8 encoding on Windows."""
22
+ if sys.platform == "win32":
23
+ # Add encoding='utf-8' if text=True is set but encoding is not specified
24
+ if kwargs.get("text") or kwargs.get("universal_newlines"):
25
+ if "encoding" not in kwargs:
26
+ kwargs["encoding"] = "utf-8"
27
+ return _original_run(*args, **kwargs)
28
+
29
+
30
+ class _PatchedPopen(_original_popen):
31
+ """Patched subprocess.Popen that defaults to UTF-8 encoding on Windows."""
32
+
33
+ def __init__(self, *args, **kwargs):
34
+ if sys.platform == "win32":
35
+ # Add encoding='utf-8' if text=True is set but encoding is not specified
36
+ if kwargs.get("text") or kwargs.get("universal_newlines"):
37
+ if "encoding" not in kwargs:
38
+ kwargs["encoding"] = "utf-8"
39
+ super().__init__(*args, **kwargs)
40
+
41
+
42
+ # Apply the patches globally
43
+ subprocess.run = _patched_run
44
+ subprocess.Popen = _PatchedPopen
45
+
46
+ # ============================================================================
47
+
48
+ # Expose package version at runtime; fall back to "unknown" during editable/dev runs
49
+ try:
50
+ __version__ = metadata.version("autoglm-gui")
51
+ except metadata.PackageNotFoundError:
52
+ __version__ = "unknown"
@@ -76,8 +76,8 @@ def main() -> None:
76
76
  )
77
77
  parser.add_argument(
78
78
  "--base-url",
79
- required=True,
80
- help="Base URL of the model API (required, e.g., http://localhost:8080/v1)",
79
+ required=False,
80
+ help="Base URL of the model API (e.g., http://localhost:8080/v1)",
81
81
  )
82
82
  parser.add_argument(
83
83
  "--model",
@@ -110,11 +110,22 @@ def main() -> None:
110
110
  action="store_true",
111
111
  help="Do not open browser automatically",
112
112
  )
113
-
114
- # If no arguments provided, print help and exit
115
- if len(sys.argv) == 1:
116
- parser.print_help()
117
- sys.exit(1)
113
+ parser.add_argument(
114
+ "--log-level",
115
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
116
+ default="INFO",
117
+ help="Console log level (default: INFO)",
118
+ )
119
+ parser.add_argument(
120
+ "--log-file",
121
+ default="logs/autoglm_{time:YYYY-MM-DD}.log",
122
+ help="Log file path (default: logs/autoglm_{time:YYYY-MM-DD}.log)",
123
+ )
124
+ parser.add_argument(
125
+ "--no-log-file",
126
+ action="store_true",
127
+ help="Disable file logging",
128
+ )
118
129
 
119
130
  args = parser.parse_args()
120
131
 
@@ -131,16 +142,45 @@ def main() -> None:
131
142
 
132
143
  from AutoGLM_GUI import server
133
144
  from AutoGLM_GUI.config import config
145
+ from AutoGLM_GUI.config_manager import load_config_file, merge_configs
146
+ from AutoGLM_GUI.logger import configure_logger
147
+
148
+ # Configure logging system
149
+ configure_logger(
150
+ console_level=args.log_level,
151
+ log_file=None if args.no_log_file else args.log_file,
152
+ )
153
+
154
+ # Load configuration from file
155
+ file_config = load_config_file()
156
+
157
+ # Build CLI config dictionary (only include provided arguments)
158
+ cli_config = {}
159
+ if args.base_url:
160
+ cli_config["base_url"] = args.base_url
161
+ if args.model:
162
+ cli_config["model_name"] = args.model
163
+ if args.apikey:
164
+ cli_config["api_key"] = args.apikey
165
+
166
+ # Merge configurations (CLI > file > defaults)
167
+ merged_config = merge_configs(file_config, cli_config)
134
168
 
135
169
  # Set model configuration via environment variables (survives reload)
136
- os.environ["AUTOGLM_BASE_URL"] = args.base_url
137
- os.environ["AUTOGLM_MODEL_NAME"] = args.model
138
- if args.apikey is not None:
139
- os.environ["AUTOGLM_API_KEY"] = args.apikey
170
+ os.environ["AUTOGLM_BASE_URL"] = merged_config["base_url"]
171
+ os.environ["AUTOGLM_MODEL_NAME"] = merged_config["model_name"]
172
+ os.environ["AUTOGLM_API_KEY"] = merged_config["api_key"]
140
173
 
141
174
  # Refresh config from environment variables
142
175
  config.refresh_from_env()
143
176
 
177
+ # Determine configuration source
178
+ config_source = "default"
179
+ if cli_config:
180
+ config_source = "CLI arguments"
181
+ elif file_config:
182
+ config_source = "config file (~/.config/autoglm/config.json)"
183
+
144
184
  # Display startup banner
145
185
  print()
146
186
  print("=" * 50)
@@ -151,11 +191,19 @@ def main() -> None:
151
191
  print(f" Server: http://{args.host}:{args.port}")
152
192
  print()
153
193
  print(" Model Configuration:")
154
- print(f" Base URL: {args.base_url}")
155
- print(f" Model: {args.model}")
156
- if args.apikey is not None:
157
- print(" API Key: (provided via --apikey)")
194
+ print(f" Source: {config_source}")
195
+ print(f" Base URL: {merged_config['base_url'] or '(not set)'}")
196
+ print(f" Model: {merged_config['model_name']}")
197
+ if merged_config["api_key"] != "EMPTY":
198
+ print(" API Key: (configured)")
158
199
  print()
200
+
201
+ # Warning if base_url is not configured
202
+ if not merged_config["base_url"]:
203
+ print(" ⚠️ WARNING: base_url is not configured!")
204
+ print(" Please configure via frontend or use --base-url")
205
+ print()
206
+
159
207
  print("=" * 50)
160
208
  print(" Press Ctrl+C to stop")
161
209
  print("=" * 50)
@@ -1,9 +1,11 @@
1
1
  """Lightweight ADB helpers with a more robust screenshot implementation."""
2
2
 
3
+ from .keyboard_installer import ADBKeyboardInstaller
3
4
  from .screenshot import Screenshot, capture_screenshot
4
5
  from .touch import touch_down, touch_move, touch_up
5
6
 
6
7
  __all__ = [
8
+ "ADBKeyboardInstaller",
7
9
  "Screenshot",
8
10
  "capture_screenshot",
9
11
  "touch_down",
@@ -0,0 +1,380 @@
1
+ """ADB Keyboard Auto-Installation and Enablement Tool.
2
+
3
+ This module provides automatic installation, configuration, and enabling of ADB Keyboard,
4
+ without requiring users to manually download and install APK.
5
+ """
6
+
7
+ import asyncio
8
+ import urllib.request
9
+ from pathlib import Path
10
+ from typing import Optional, Tuple
11
+
12
+ from AutoGLM_GUI.logger import logger
13
+ from AutoGLM_GUI.platform_utils import run_cmd_silently
14
+
15
+ ADB_KEYBOARD_PACKAGE = "com.android.adbkeyboard"
16
+ ADB_KEYBOARD_IME = "com.android.adbkeyboard/.AdbIME"
17
+ ADB_KEYBOARD_APK_URL = (
18
+ "https://github.com/senzhk/ADBKeyBoard/raw/master/ADBKeyboard.apk"
19
+ )
20
+ ADB_KEYBOARD_APK_FILENAME = "ADBKeyboard.apk"
21
+
22
+ # APK file in user cache directory (fallback)
23
+ USER_CACHE_APK_PATH = Path.home() / ".cache" / "autoglm" / ADB_KEYBOARD_APK_FILENAME
24
+
25
+
26
+ class ADBKeyboardInstaller:
27
+ """ADB Keyboard Auto-Installer."""
28
+
29
+ def __init__(self, device_id: Optional[str] = None):
30
+ """
31
+ Initialize the installer.
32
+
33
+ Args:
34
+ device_id: Optional ADB device ID for multi-device scenarios.
35
+ """
36
+ self.device_id = device_id
37
+ self.adb_prefix = ["adb"]
38
+ if device_id:
39
+ self.adb_prefix.extend(["-s", device_id])
40
+
41
+ logger.debug(
42
+ f"Initialized ADBKeyboardInstaller for device: {device_id or 'default'}"
43
+ )
44
+
45
+ def is_installed(self) -> bool:
46
+ """
47
+ Check if ADB Keyboard is installed (via package name).
48
+
49
+ Returns:
50
+ bool: True if installed, False otherwise.
51
+ """
52
+ try:
53
+ logger.debug(
54
+ f"Checking if ADB Keyboard is installed on device {self.device_id or 'default'}"
55
+ )
56
+ result = asyncio.run(
57
+ run_cmd_silently(self.adb_prefix + ["shell", "pm", "list", "packages"])
58
+ )
59
+ package_list = result.stdout.strip()
60
+ installed = ADB_KEYBOARD_PACKAGE in package_list
61
+ logger.debug(f"ADB Keyboard installed: {installed}")
62
+ return installed
63
+ except Exception as e:
64
+ logger.error(f"Error checking keyboard installation status: {e}")
65
+ return False
66
+
67
+ def is_enabled(self) -> bool:
68
+ """
69
+ Check if ADB Keyboard is enabled (usable).
70
+
71
+ Determined by checking the list of enabled input methods.
72
+
73
+ Returns:
74
+ bool: True if enabled, False otherwise.
75
+ """
76
+ try:
77
+ logger.debug(
78
+ f"Checking if ADB Keyboard is enabled on device {self.device_id or 'default'}"
79
+ )
80
+ # Use ime list -s to check only enabled input methods
81
+ result = asyncio.run(
82
+ run_cmd_silently(self.adb_prefix + ["shell", "ime", "list", "-s"])
83
+ )
84
+ ime_list_enabled = result.stdout.strip()
85
+ enabled = ADB_KEYBOARD_IME in ime_list_enabled
86
+ logger.debug(f"ADB Keyboard enabled: {enabled}")
87
+ return enabled
88
+ except Exception as e:
89
+ logger.error(f"Error checking keyboard enable status: {e}")
90
+ return False
91
+
92
+ def get_apk_path(self) -> Optional[Path]:
93
+ """
94
+ Get APK file path.
95
+
96
+ Prioritizes returning the APK file within the project (bundled in wheel),
97
+ falls back to user cache if not exists.
98
+
99
+ Returns:
100
+ Optional[Path]: APK file path, or None if neither exists.
101
+ """
102
+ # Priority 1: Try bundled resource (packaged in wheel)
103
+ try:
104
+ from importlib.resources import files
105
+
106
+ logger.debug("Searching for bundled APK in wheel package")
107
+ resource = files("AutoGLM_GUI").joinpath(
108
+ "resources/apks", ADB_KEYBOARD_APK_FILENAME
109
+ )
110
+ # Convert to Path
111
+ if hasattr(resource, "read_bytes"):
112
+ # For Python 3.9+, use as_file() context manager
113
+ from importlib.resources import as_file
114
+
115
+ with as_file(resource) as path:
116
+ if path.exists():
117
+ logger.info(f"Found bundled APK: {path}")
118
+ return path
119
+ elif hasattr(resource, "_path"):
120
+ # Fallback for older importlib.resources
121
+ path = Path(str(resource))
122
+ if path.exists():
123
+ logger.info(f"Found bundled APK: {path}")
124
+ return path
125
+ except Exception as e:
126
+ logger.debug(f"Bundled APK not found: {e}")
127
+
128
+ # Priority 2: Try user cache directory
129
+ if USER_CACHE_APK_PATH.exists():
130
+ logger.info(f"Found cached APK: {USER_CACHE_APK_PATH}")
131
+ return USER_CACHE_APK_PATH
132
+
133
+ logger.warning("APK file not found in bundled resources or cache")
134
+ return None
135
+
136
+ def download_apk(self, force: bool = False) -> bool:
137
+ """
138
+ Get or download ADB Keyboard APK.
139
+
140
+ Prioritizes using the APK file within the project, downloads from GitHub if not exists.
141
+
142
+ Args:
143
+ force: Whether to force re-download even if file already exists.
144
+
145
+ Returns:
146
+ bool: True if APK is successfully obtained, False otherwise.
147
+ """
148
+ # Check if APK already exists
149
+ existing_apk = self.get_apk_path()
150
+ if existing_apk and not force:
151
+ logger.debug(f"APK already exists at {existing_apk}, skipping download")
152
+ return True
153
+
154
+ # Download from GitHub
155
+ logger.info(f"Downloading ADB Keyboard APK from {ADB_KEYBOARD_APK_URL}")
156
+
157
+ # Ensure cache directory exists
158
+ USER_CACHE_APK_PATH.parent.mkdir(parents=True, exist_ok=True)
159
+
160
+ try:
161
+ urllib.request.urlretrieve(ADB_KEYBOARD_APK_URL, USER_CACHE_APK_PATH)
162
+
163
+ if USER_CACHE_APK_PATH.exists() and USER_CACHE_APK_PATH.stat().st_size > 0:
164
+ logger.info(f"APK downloaded successfully to {USER_CACHE_APK_PATH}")
165
+ return True
166
+ else:
167
+ logger.error("Downloaded APK is empty or invalid")
168
+ return False
169
+
170
+ except Exception as e:
171
+ logger.error(f"Failed to download APK: {e}")
172
+ # Clean up incomplete file
173
+ if USER_CACHE_APK_PATH.exists():
174
+ USER_CACHE_APK_PATH.unlink()
175
+ return False
176
+
177
+ def install(self) -> Tuple[bool, str]:
178
+ """
179
+ Install ADB Keyboard APK.
180
+
181
+ Returns:
182
+ Tuple[bool, str]: (success, message)
183
+ """
184
+ apk_path = self.get_apk_path()
185
+ if not apk_path or not apk_path.exists():
186
+ error_msg = "APK file not found. Please download first."
187
+ logger.error(error_msg)
188
+ return False, error_msg
189
+
190
+ try:
191
+ logger.info(f"Installing ADB Keyboard from {apk_path}")
192
+ result = asyncio.run(
193
+ run_cmd_silently(self.adb_prefix + ["install", "-r", str(apk_path)])
194
+ )
195
+
196
+ if "Success" in result.stdout or result.returncode == 0:
197
+ success_msg = "ADB Keyboard installed successfully"
198
+ logger.info(success_msg)
199
+ return True, success_msg
200
+ else:
201
+ error_msg = f"Installation failed: {result.stdout} {result.stderr}"
202
+ logger.error(error_msg)
203
+ return False, error_msg
204
+
205
+ except Exception as e:
206
+ error_msg = f"Installation error: {e}"
207
+ logger.exception("Unexpected error during installation")
208
+ return False, error_msg
209
+
210
+ def enable(self) -> Tuple[bool, str]:
211
+ """
212
+ Enable ADB Keyboard (enable only, do not modify default input method).
213
+
214
+ Note: This only enables ADB Keyboard, does not set it as default input method.
215
+ In actual usage, Phone Agent will temporarily switch via detect_and_set_adb_keyboard().
216
+
217
+ Returns:
218
+ Tuple[bool, str]: (success, message)
219
+ """
220
+ try:
221
+ logger.info("Enabling ADB Keyboard IME")
222
+ # Enable keyboard
223
+ result = asyncio.run(
224
+ run_cmd_silently(
225
+ self.adb_prefix + ["shell", "ime", "enable", ADB_KEYBOARD_IME]
226
+ )
227
+ )
228
+
229
+ if result.returncode == 0:
230
+ success_msg = "ADB Keyboard enabled successfully"
231
+ logger.info(success_msg)
232
+ return True, success_msg
233
+ else:
234
+ # Some devices return non-zero but still succeed, verify with is_enabled()
235
+ if self.is_enabled():
236
+ success_msg = "ADB Keyboard enabled (verified)"
237
+ logger.info(success_msg)
238
+ return True, success_msg
239
+ else:
240
+ error_msg = f"Enable failed: {result.stdout} {result.stderr}"
241
+ logger.warning(error_msg)
242
+ return False, error_msg
243
+
244
+ except Exception as e:
245
+ error_msg = f"Enable error: {e}"
246
+ logger.exception("Unexpected error during enable")
247
+ return False, error_msg
248
+
249
+ def auto_setup(self) -> Tuple[bool, str]:
250
+ """
251
+ Automatically complete installation and enablement process.
252
+
253
+ Intelligent handling:
254
+ 1. Installed and enabled - skip, return True
255
+ 2. Installed but not enabled - enable only, return result
256
+ 3. Not installed - install+enable, return result
257
+
258
+ Note: This method does not interact with users, all user prompts should be handled by the caller.
259
+
260
+ Returns:
261
+ Tuple[bool, str]: (success, message)
262
+ """
263
+ logger.debug("Starting auto-setup for ADB Keyboard")
264
+
265
+ # Check current status
266
+ installed = self.is_installed()
267
+ enabled = self.is_enabled()
268
+
269
+ # Status 1: Installed and enabled
270
+ if installed and enabled:
271
+ msg = "ADB Keyboard is ready (already installed and enabled)"
272
+ logger.info(msg)
273
+ return True, msg
274
+
275
+ # Status 2: Installed but not enabled
276
+ if installed and not enabled:
277
+ logger.info("ADB Keyboard is installed but not enabled, enabling now")
278
+ return self.enable()
279
+
280
+ # Status 3: Not installed
281
+ if not installed:
282
+ logger.info("ADB Keyboard is not installed, starting installation")
283
+
284
+ # Step 1: Download APK (if not already available)
285
+ if not self.download_apk():
286
+ error_msg = "Failed to download APK"
287
+ logger.error(error_msg)
288
+ return False, error_msg
289
+
290
+ # Step 2: Install
291
+ success, message = self.install()
292
+ if not success:
293
+ logger.error(f"Installation failed: {message}")
294
+ return False, message
295
+
296
+ # Step 3: Enable
297
+ success, message = self.enable()
298
+ if not success:
299
+ logger.error(f"Enable failed: {message}")
300
+ return False, message
301
+
302
+ # Verify final status
303
+ if self.is_installed() and self.is_enabled():
304
+ success_msg = "ADB Keyboard setup completed successfully"
305
+ logger.info(success_msg)
306
+ return True, success_msg
307
+ else:
308
+ error_msg = "Setup completed but verification failed"
309
+ logger.warning(error_msg)
310
+ return False, error_msg
311
+
312
+ # Default return failure
313
+ error_msg = "Unknown status, setup failed"
314
+ logger.error(error_msg)
315
+ return False, error_msg
316
+
317
+ def get_status(self) -> dict:
318
+ """
319
+ Get detailed status of ADB Keyboard.
320
+
321
+ Returns:
322
+ dict: Dictionary containing installation and enablement status.
323
+ """
324
+ apk_path = self.get_apk_path()
325
+ installed = self.is_installed()
326
+ enabled = self.is_enabled()
327
+
328
+ # Determine current status
329
+ if installed and enabled:
330
+ status = "ready" # Ready
331
+ elif installed and not enabled:
332
+ status = "installed_but_disabled" # Installed but not enabled
333
+ elif not installed:
334
+ status = "not_installed" # Not installed
335
+ else:
336
+ status = "unknown" # Unknown
337
+
338
+ return {
339
+ "installed": installed,
340
+ "enabled": enabled,
341
+ "status": status,
342
+ "status_text": {
343
+ "ready": "Installed and enabled",
344
+ "installed_but_disabled": "Installed but not enabled",
345
+ "not_installed": "Not installed",
346
+ "unknown": "Unknown status",
347
+ }.get(status, "Unknown"),
348
+ "apk_exists": apk_path is not None and apk_path.exists(),
349
+ "apk_path": str(apk_path) if apk_path else "N/A",
350
+ "cache_apk_exists": USER_CACHE_APK_PATH.exists(),
351
+ "cache_apk_path": str(USER_CACHE_APK_PATH),
352
+ }
353
+
354
+
355
+ def auto_setup_adb_keyboard(device_id: Optional[str] = None) -> Tuple[bool, str]:
356
+ """
357
+ Convenience function: One-click auto-install and enable ADB Keyboard.
358
+
359
+ Args:
360
+ device_id: Optional device ID.
361
+
362
+ Returns:
363
+ Tuple[bool, str]: (success, message)
364
+ """
365
+ installer = ADBKeyboardInstaller(device_id)
366
+ return installer.auto_setup()
367
+
368
+
369
+ def check_and_suggest_installation() -> bool:
370
+ """
371
+ Check if ADB Keyboard needs installation.
372
+
373
+ Note: This function does not interact with users, only returns boolean value.
374
+ All user prompts should be handled by the caller.
375
+
376
+ Returns:
377
+ bool: True if not installed, False otherwise.
378
+ """
379
+ installer = ADBKeyboardInstaller()
380
+ return not installer.is_installed()