devicely 2.1.3 → 2.1.5

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 (89) hide show
  1. package/bin/devicely.js +105 -1
  2. package/lib/androidDeviceDetection.js +276 -1
  3. package/lib/appMappings.js +337 -1
  4. package/lib/deviceDetection.js +394 -1
  5. package/lib/devices.js +54 -1
  6. package/lib/doctor.js +94 -1
  7. package/lib/executor.js +104 -1
  8. package/lib/logger.js +35 -1
  9. package/lib/scriptLoader.js +75 -0
  10. package/lib/server.js +3483 -1
  11. package/package.json +3 -12
  12. package/scripts/compile-shell-scripts.js +208 -0
  13. package/scripts/encrypt-shell-simple.js +75 -0
  14. package/scripts/obfuscate-shell.js +160 -0
  15. package/scripts/shell/android_device_control +0 -0
  16. package/scripts/shell/android_device_control.sh +848 -0
  17. package/scripts/shell/apps_presets.conf +271 -0
  18. package/scripts/shell/connect_android_usb +0 -0
  19. package/scripts/shell/connect_android_usb_multi_final +0 -0
  20. package/scripts/shell/connect_android_usb_multi_final.sh +289 -0
  21. package/scripts/shell/connect_android_wireless +0 -0
  22. package/scripts/shell/connect_android_wireless.sh +58 -0
  23. package/scripts/shell/connect_android_wireless_multi_final +0 -0
  24. package/scripts/shell/connect_android_wireless_multi_final.sh +476 -0
  25. package/scripts/shell/connect_ios_usb +0 -0
  26. package/scripts/shell/connect_ios_usb_multi_final +0 -0
  27. package/scripts/shell/connect_ios_usb_multi_final.sh +4225 -0
  28. package/scripts/shell/connect_ios_wireless_multi_final +0 -0
  29. package/scripts/shell/connect_ios_wireless_multi_final.sh +4167 -0
  30. package/scripts/shell/create_production_scripts +0 -0
  31. package/scripts/shell/create_production_scripts.sh +38 -0
  32. package/scripts/shell/devices.conf +24 -0
  33. package/scripts/shell/diagnose_wireless_ios +0 -0
  34. package/scripts/shell/find_element_coordinates +0 -0
  35. package/scripts/shell/find_wda +0 -0
  36. package/scripts/shell/install_uiautomator2 +0 -0
  37. package/scripts/shell/install_uiautomator2.sh +93 -0
  38. package/scripts/shell/ios_device_control +0 -0
  39. package/scripts/shell/ios_device_control.sh +220 -0
  40. package/scripts/shell/organize_project +0 -0
  41. package/scripts/shell/organize_project.sh +59 -0
  42. package/scripts/shell/pre-publish-check +0 -0
  43. package/scripts/shell/pre-publish-check.sh +238 -0
  44. package/scripts/shell/publish +0 -0
  45. package/scripts/shell/publish-to-npm +0 -0
  46. package/scripts/shell/publish-to-npm.sh +366 -0
  47. package/scripts/shell/publish.sh +100 -0
  48. package/scripts/shell/setup +0 -0
  49. package/scripts/shell/setup.sh +121 -0
  50. package/scripts/shell/setup_android +0 -0
  51. package/scripts/shell/start +0 -0
  52. package/scripts/shell/start.sh +59 -0
  53. package/scripts/shell/sync-to-npm-package-final +0 -0
  54. package/scripts/shell/sync-to-npm-package-final.sh +60 -0
  55. package/scripts/shell/test-local-package.sh +95 -0
  56. package/scripts/shell/test_android_locators +0 -0
  57. package/scripts/shell/test_connect +0 -0
  58. package/scripts/shell/test_device_detection +0 -0
  59. package/scripts/shell/test_fixes +0 -0
  60. package/scripts/shell/test_getlocators_fix +0 -0
  61. package/scripts/shell/test_recording_feature +0 -0
  62. package/scripts/shell/verify-shell-protection +0 -0
  63. package/scripts/shell/verify-shell-protection.sh +73 -0
  64. package/scripts/shell/verify_distribution +0 -0
  65. package/lib/package-lock.json +0 -1678
  66. package/lib/package.json +0 -30
  67. package/lib/screenshots/screenshot_ios_iPhone17_20260205_225900.png +0 -0
  68. package/lib/screenshots/screenshot_ios_iPhone17_20260205_225942.png +0 -0
  69. package/lib/screenshots/screenshot_ios_iPhone17_20260205_231101.png +0 -0
  70. package/lib/screenshots/screenshot_ios_iPhone17_20260205_232911.png +0 -0
  71. package/lib/screenshots/screenshot_ios_iPhone17_20260208_095103.png +0 -0
  72. package/lib/screenshots/screenshot_ios_iPhone17_20260208_095720.png +0 -0
  73. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115040.png +0 -0
  74. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115047.png +0 -0
  75. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115118.png +0 -0
  76. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115125.png +0 -0
  77. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_115143.png +0 -0
  78. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_120107.png +0 -0
  79. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_120118.png +0 -0
  80. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_120137.png +0 -0
  81. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_120201.png +0 -0
  82. package/lib/screenshots/screenshot_ios_iPhoneXR17x_20260206_134529.png +0 -0
  83. package/scripts/shell/android_device_control.enc +0 -1
  84. package/scripts/shell/connect_android_usb_multi_final.enc +0 -1
  85. package/scripts/shell/connect_android_wireless.enc +0 -1
  86. package/scripts/shell/connect_android_wireless_multi_final.enc +0 -1
  87. package/scripts/shell/connect_ios_usb_multi_final.enc +0 -1
  88. package/scripts/shell/connect_ios_wireless_multi_final.enc +0 -1
  89. package/scripts/shell/ios_device_control.enc +0 -1
@@ -0,0 +1,4167 @@
1
+ #!/bin/bash
2
+ # iOS Multi-Device Control - ENHANCED VERSION WITH AI
3
+ # Features: Revolutionary AI-powered click detection + Trust-preserving cleanup
4
+ # Usage:
5
+ # ./connect_ios_wireless_multi_final.sh # Interactive mode
6
+ # ./connect_ios_wireless_multi_final.sh "text" # Send text to all
7
+ # ./connect_ios_wireless_multi_final.sh -d device "text" # Send to specific device
8
+ # ./connect_ios_wireless_multi_final.sh "click 100,200" # Click at coordinates
9
+ # ./connect_ios_wireless_multi_final.sh "click 'Button'" # AI-powered click on element
10
+ # ./connect_ios_wireless_multi_final.sh "longpress 'Menu'" # Long press on element
11
+ #
12
+ # Revolutionary Features:
13
+ # 🚀 AI-powered element detection - Pre-caches all screen elements
14
+ # 🧠 Smart fuzzy matching - Finds elements like human would
15
+ # 🎯 Multi-strategy fallback - Professional automation approaches
16
+ # 📸 Screen analysis caching - Instant element lookup after first scan
17
+ # 🔄 Self-refreshing cache - Updates when screen changes detected
18
+ #
19
+ # Commands:
20
+ # click <x,y> or click <text> - AI-powered click detection
21
+ # longpress <x,y> or longpress <text> - AI-powered long press
22
+ # getLocators - Get all locators from current screen
23
+ # refresh - Force refresh screen element cache
24
+ # forcecleanup - Force terminate WDA (may affect device trust)
25
+ #
26
+ # Trust Preservation:
27
+ # Normal 'quit' preserves device trust by keeping WDA processes running
28
+
29
+ # Load the revolutionary AI click system
30
+ SCRIPT_DIR="$(dirname "$0")"
31
+ source "$SCRIPT_DIR/advanced_click_system.sh" 2>/dev/null || echo "⚠️ AI system not loaded, using fallback"
32
+ source "$SCRIPT_DIR/ios_click_fix.sh" 2>/dev/null || echo "⚠️ Non-AI click helper not loaded"
33
+
34
+ WDA_PORT=8100
35
+ WDA_PORT_BASE=8100
36
+ SCRIPT_DIR="$(dirname "$0")"
37
+
38
+ # Function to find config files in multiple locations
39
+ find_config() {
40
+ local filename="$1"
41
+ local search_paths=(
42
+ "$SCRIPT_DIR/$filename"
43
+ "$SCRIPT_DIR/../../config/$filename"
44
+ "$SCRIPT_DIR/../config/$filename"
45
+ "$HOME/.devicely/$filename"
46
+ "/opt/homebrew/lib/node_modules/devicely/config/$filename"
47
+ "$(npm root -g 2>/dev/null)/devicely/config/$filename"
48
+ )
49
+
50
+ for path in "${search_paths[@]}"; do
51
+ if [ -f "$path" ]; then
52
+ echo "$path"
53
+ return 0
54
+ fi
55
+ done
56
+
57
+ return 1
58
+ }
59
+
60
+ # Find config files
61
+ CONFIG_FILE=$(find_config "devices.conf")
62
+ if [ -z "$CONFIG_FILE" ]; then
63
+ if [ -f "$HOME/.devicely/devices.conf" ]; then
64
+ CONFIG_FILE="$HOME/.devicely/devices.conf"
65
+ else
66
+ echo "❌ Required file not found: devices.conf"
67
+ echo " Searched in: script dir, config/, ~/.devicely/, global npm"
68
+ exit 1
69
+ fi
70
+ fi
71
+
72
+ APPS_PRESETS_FILE=$(find_config "apps_presets.conf")
73
+ if [ -z "$APPS_PRESETS_FILE" ]; then
74
+ if [ -f "$HOME/.devicely/apps_presets.conf" ]; then
75
+ APPS_PRESETS_FILE="$HOME/.devicely/apps_presets.conf"
76
+ else
77
+ echo "❌ Required file not found: apps_presets.conf"
78
+ echo " Searched in: script dir, config/, ~/.devicely/, global npm"
79
+ exit 1
80
+ fi
81
+ fi
82
+
83
+ # Track iproxy processes for USB devices
84
+ IPROXY_PIDS_FILE="/tmp/ios_multi_iproxy_pids.$$"
85
+ DEVICE_PORT_MAP_FILE="/tmp/ios_multi_device_ports.$$"
86
+ DEVICE_CONNECTION_TYPE_FILE="/tmp/ios_multi_connection_types.$$"
87
+
88
+ # Command history variables (like original script)
89
+ HISTORY_FILE="/tmp/.ios_multi_history"
90
+ HISTORY_MAX=50
91
+ declare -a TEXT_HISTORY
92
+ HISTORY_INDEX=-1
93
+ CURRENT_INPUT=""
94
+
95
+ # Disable bash history to avoid showing shell commands (like original script)
96
+ set +o history
97
+ unset HISTFILE
98
+
99
+ # Colors
100
+ RED='\033[0;31m'
101
+ GREEN='\033[0;32m'
102
+ YELLOW='\033[1;33m'
103
+ BLUE='\033[0;34m'
104
+ CYAN='\033[0;36m'
105
+ NC='\033[0m'
106
+
107
+ print_color() {
108
+ echo -e "$1$2${NC}"
109
+ }
110
+
111
+ # Global session tracking (use fixed filename so sessions persist across command invocations)
112
+ DEVICE_SESSIONS_FILE="/tmp/ios_multi_sessions"
113
+
114
+ # Parse arguments
115
+ DEVICE_NAME_REQUEST=""
116
+ TEXT=""
117
+ APP_BUNDLE=""
118
+
119
+ if [ "$1" = "-d" ]; then
120
+ DEVICE_NAME_REQUEST="$2"
121
+ TEXT="$3"
122
+ elif [ -n "$1" ]; then
123
+ TEXT="$1"
124
+ fi
125
+
126
+ # Check files
127
+ for file in "$CONFIG_FILE" "$APPS_PRESETS_FILE"; do
128
+ if [ ! -f "$file" ]; then
129
+ echo "❌ Required file not found: $file"
130
+ exit 1
131
+ fi
132
+ done
133
+
134
+ # Get device names from config
135
+ get_all_device_names() {
136
+ local devices=()
137
+ while IFS=',' read -r name rest; do
138
+ [[ "$name" =~ ^# ]] || [[ -z "$name" ]] && continue
139
+ devices+=("$name")
140
+ done < "$CONFIG_FILE"
141
+ printf '%s\n' "${devices[@]}"
142
+ }
143
+
144
+ # Check if device is connected via USB
145
+ is_usb_connected() {
146
+ local udid="$1"
147
+ if command -v idevice_id >/dev/null 2>&1; then
148
+ idevice_id -l 2>/dev/null | grep -q "^${udid}$"
149
+ return $?
150
+ fi
151
+ return 1
152
+ }
153
+
154
+ # Get USB device name from UDID
155
+ get_usb_device_name() {
156
+ local udid="$1"
157
+ if command -v idevicename >/dev/null 2>&1; then
158
+ idevicename -u "$udid" 2>/dev/null
159
+ fi
160
+ }
161
+
162
+ # Get device details (host,udid)
163
+ get_device_details() {
164
+ local device_name="$1"
165
+
166
+ # First check if it's a USB device
167
+ if command -v idevice_id >/dev/null 2>&1; then
168
+ local usb_udids=($(idevice_id -l 2>/dev/null))
169
+ for udid in "${usb_udids[@]}"; do
170
+ local usb_name=$(idevicename -u "$udid" 2>/dev/null || echo "")
171
+ if [ "$usb_name" = "$device_name" ] || [ "$udid" = "$device_name" ]; then
172
+ # For USB devices, return localhost as host
173
+ echo "localhost,$udid"
174
+ return 0
175
+ fi
176
+ done
177
+ fi
178
+
179
+ # If not found in USB, check config file for wireless
180
+ # IMPORTANT: For wireless script, prioritize wireless entries (entries with IP addresses)
181
+ local best_match_ip=""
182
+ local best_match_udid=""
183
+
184
+ while IFS=',' read -r name udid ip platform type rest; do
185
+ [[ "$name" =~ ^# ]] || [[ -z "$name" ]] && continue
186
+
187
+ # Clean up whitespace
188
+ name=$(echo "$name" | xargs)
189
+ udid=$(echo "$udid" | xargs)
190
+ ip=$(echo "$ip" | xargs)
191
+ platform=$(echo "$platform" | xargs)
192
+ type=$(echo "$type" | xargs)
193
+
194
+ if [ "$name" = "$device_name" ]; then
195
+ # Prefer wireless entries (ones with IP addresses)
196
+ if [ -n "$ip" ] && [[ "$type" == "wireless" ]]; then
197
+ # Found wireless entry - return immediately
198
+ echo "$ip,$udid"
199
+ return 0
200
+ elif [ -z "$best_match_ip" ]; then
201
+ # Store first match as fallback
202
+ best_match_ip="$ip"
203
+ best_match_udid="$udid"
204
+ fi
205
+ fi
206
+ done < "$CONFIG_FILE"
207
+
208
+ # If we didn't find a wireless entry, use the first match (if any)
209
+ if [ -n "$best_match_udid" ]; then
210
+ if [ -n "$best_match_ip" ]; then
211
+ echo "$best_match_ip,$best_match_udid"
212
+ else
213
+ echo "$best_match_udid,$best_match_udid"
214
+ fi
215
+ return 0
216
+ fi
217
+
218
+ # Not found
219
+ return 1
220
+ }
221
+
222
+ # Resolve app name to bundle ID
223
+ resolve_app_bundle() {
224
+ local input="$1"
225
+
226
+ # If already a bundle ID, return as-is
227
+ if [[ "$input" == *"."* ]] && [[ "$input" != *" "* ]]; then
228
+ echo "$input"
229
+ return
230
+ fi
231
+
232
+ # Look up in presets
233
+ if [ -f "$APPS_PRESETS_FILE" ]; then
234
+ local input_lower=$(echo "$input" | tr '[:upper:]' '[:lower:]')
235
+ while IFS=',' read -r name bundle rest; do
236
+ [[ "$name" =~ ^# ]] || [[ -z "$name" ]] && continue
237
+ if [ "$(echo "$name" | tr '[:upper:]' '[:lower:]')" = "$input_lower" ]; then
238
+ echo "$bundle"
239
+ return
240
+ fi
241
+ done < "$APPS_PRESETS_FILE"
242
+ fi
243
+
244
+ # Default bundle IDs for common apps
245
+ case "$input_lower" in
246
+ "safari") echo "com.apple.mobilesafari" ;;
247
+ "settings") echo "com.apple.Preferences" ;;
248
+ "messages") echo "com.apple.MobileSMS" ;;
249
+ "camera") echo "com.apple.camera" ;;
250
+ "photos") echo "com.apple.mobileslideshow" ;;
251
+ "notes") echo "com.apple.mobilenotes" ;;
252
+ *) echo "$input" ;;
253
+ esac
254
+ }
255
+
256
+ # Check device connectivity
257
+ check_device_connectivity() {
258
+ local device_name="$1"
259
+ local device_details
260
+ device_details=$(get_device_details "$device_name")
261
+
262
+ if [ $? -ne 0 ]; then
263
+ echo " ❌ Device not found in config"
264
+ return 1
265
+ fi
266
+
267
+ local host=$(echo "$device_details" | cut -d',' -f1)
268
+
269
+ # First check network connectivity with ping
270
+ echo " 🌐 Ping test to $host..."
271
+ if ping -c 1 -W 2000 "$host" >/dev/null 2>&1; then
272
+ echo " ✅ Network reachable"
273
+ else
274
+ echo " ❌ Network unreachable"
275
+ return 1
276
+ fi
277
+
278
+ # Then test WDA connection with optimized timeout
279
+ echo " 🔍 Testing WDA connection..."
280
+ if curl -s --connect-timeout 2 --max-time 3 "http://$host:$WDA_PORT/status" 2>/dev/null | grep -q '"state"'; then
281
+ echo " ✅ WDA ready"
282
+ return 0
283
+ else
284
+ echo " ⚠️ WDA not ready, attempting auto-start..."
285
+
286
+ # Auto-start WDA wirelessly (like original script)
287
+ local udid=$(echo "$device_details" | cut -d',' -f2)
288
+ if [ -n "$udid" ]; then
289
+ start_wda_wirelessly "$device_name" "$host" "$udid"
290
+ else
291
+ return 1
292
+ fi
293
+ fi
294
+ }
295
+
296
+ # Start WDA wirelessly (adapted from original script)
297
+ start_wda_wirelessly() {
298
+ local device_name="$1"
299
+ local host="$2"
300
+ local udid="$3"
301
+
302
+ echo " 🔧 Starting WDA wirelessly for $device_name (UDID: $udid)..."
303
+
304
+ # Check if WebDriverAgent path exists
305
+ local WDA_PATH="$HOME/Downloads/WebDriverAgent-10.2.1"
306
+ if [ ! -d "$WDA_PATH" ]; then
307
+ print_color "$RED" " ❌ WebDriverAgent path not found: $WDA_PATH"
308
+ print_color "$YELLOW" " ℹ️ Please download WebDriverAgent and place it in $WDA_PATH"
309
+ return 1
310
+ fi
311
+
312
+ # Check if xcrun is available
313
+ if ! command -v xcrun >/dev/null 2>&1; then
314
+ print_color "$RED" " ❌ xcrun not available (Xcode not installed)"
315
+ return 1
316
+ fi
317
+
318
+ # Create device-specific log file
319
+ local wda_log="/tmp/wda_output_${device_name}.log"
320
+ : > "$wda_log"
321
+
322
+ print_color "$YELLOW" " → Starting WDA (optimized)..."
323
+
324
+ # Start WDA build/test in background with better error handling
325
+ (
326
+ cd "$WDA_PATH" || exit 1
327
+
328
+ # Check if already built, skip build if possible
329
+ if [ -d "build" ] && [ -f "build/Build/Products/Debug-iphoneos/WebDriverAgentRunner-Runner.app/Info.plist" ]; then
330
+ echo " → Using existing build, launching test..."
331
+ xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination "platform=iOS,id=$udid" test-without-building > "$wda_log" 2>&1 &
332
+ else
333
+ echo " → Building and launching WDA..."
334
+ xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination "platform=iOS,id=$udid" test > "$wda_log" 2>&1 &
335
+ fi
336
+ ) &
337
+
338
+ # Wait for WDA to start (optimized timing)
339
+ echo " ⏳ Waiting for WDA to start (timeout: 60s)..."
340
+ for i in {1..60}; do
341
+ if curl -s --connect-timeout 1 --max-time 2 "http://$host:$WDA_PORT/status" 2>/dev/null | grep -q '"state"'; then
342
+ print_color "$GREEN" " ✅ WDA started in ${i}s for $device_name"
343
+ return 0
344
+ fi
345
+
346
+ # Show progress every 10 seconds
347
+ if [ $((i % 10)) -eq 0 ]; then
348
+ echo " ⏳ Still waiting... (${i}s elapsed)"
349
+ fi
350
+
351
+ sleep 1
352
+ done
353
+
354
+ # Check for test failures in log
355
+ if grep -q "Failing tests:" "$wda_log" 2>/dev/null; then
356
+ print_color "$RED" " ❌ WDA test failure detected for $device_name"
357
+ return 1
358
+ fi
359
+
360
+ # Try to extract IP from log if auto-detection failed
361
+ if ! curl -s --connect-timeout 2 --max-time 3 "http://$host:$WDA_PORT/status" >/dev/null 2>&1; then
362
+ local log_ip=$(grep -oE 'ServerURLHere->http://([0-9]{1,3}(\.[0-9]{1,3}){3}):8100<-ServerURLHere' "$wda_log" 2>/dev/null | head -1 | sed -E 's/.*http:\/\/([0-9\.]+):8100.*/\1/')
363
+ if [ -n "$log_ip" ] && [ "$log_ip" != "$host" ]; then
364
+ print_color "$YELLOW" " → Trying extracted IP: $log_ip"
365
+ if curl -s --connect-timeout 2 --max-time 3 "http://$log_ip:$WDA_PORT/status" 2>/dev/null | grep -q '"state"'; then
366
+ # Update the host in devices.conf or return success with new IP
367
+ print_color "$GREEN" " ✅ WDA started on $log_ip for $device_name"
368
+ return 0
369
+ fi
370
+ fi
371
+ fi
372
+
373
+ print_color "$RED" " ❌ WDA failed to start for $device_name"
374
+ print_color "$YELLOW" " ⚠️ Tips: Ensure device is unlocked, on same WiFi, and WDA built once manually via Xcode"
375
+ return 1
376
+ }
377
+
378
+ # History management functions (adapted from original script)
379
+ load_text_history() {
380
+ # Ensure history file exists and is clean
381
+ if [ ! -f "$HISTORY_FILE" ]; then
382
+ touch "$HISTORY_FILE"
383
+ fi
384
+ }
385
+
386
+ # Add text to history
387
+ add_to_history() {
388
+ local text="$1"
389
+ # Skip empty text, commands, and special keys
390
+ if [ -z "$text" ] || [[ "$text" == /* ]] || [[ "$text" == "{*}" ]] || [[ "$text" == "\\n" ]] || [[ "$text" == "quit" ]] || [[ "$text" == "exit" ]] || [[ "$text" == "help" ]]; then
391
+ return
392
+ fi
393
+
394
+ # Add to history file (remove duplicates and keep last entries)
395
+ grep -v "^$(printf '%s\n' "$text" | sed 's/[[\.*^$()+?{|]/\\&/g')$" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" 2>/dev/null || true
396
+ echo "$text" >> "${HISTORY_FILE}.tmp"
397
+ tail -n "$HISTORY_MAX" "${HISTORY_FILE}.tmp" > "$HISTORY_FILE"
398
+ rm -f "${HISTORY_FILE}.tmp"
399
+ }
400
+
401
+ # Create persistent WDA session (like original script - reuse same session)
402
+ create_persistent_session() {
403
+ local device_name="$1"
404
+ local device_details
405
+ device_details=$(get_device_details "$device_name")
406
+ local host=$(echo "$device_details" | cut -d',' -f1)
407
+
408
+ # Create ONE session per device for the entire interactive session
409
+ # Don't launch any specific app - just create a session to the home screen
410
+ local session=$(curl -s -X POST "http://$host:$WDA_PORT/session" \
411
+ -H 'Content-Type: application/json' \
412
+ -d '{"capabilities":{"alwaysMatch":{}}}' | \
413
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('sessionId','') or d.get('value',{}).get('sessionId',''))" 2>/dev/null)
414
+
415
+ if [ -n "$session" ]; then
416
+ # Use a lock file for atomic writes when running in parallel
417
+ local lock_file="${DEVICE_SESSIONS_FILE}.lock"
418
+ while ! mkdir "$lock_file" 2>/dev/null; do
419
+ sleep 0.01
420
+ done
421
+ echo "$device_name:$session" >> "$DEVICE_SESSIONS_FILE"
422
+ rmdir "$lock_file"
423
+ echo "$session"
424
+ else
425
+ echo ""
426
+ fi
427
+ }
428
+
429
+ # Get existing session for device
430
+ get_device_session() {
431
+ local device_name="$1"
432
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
433
+ grep "^$device_name:" "$DEVICE_SESSIONS_FILE" | cut -d':' -f2 | tail -1
434
+ fi
435
+ }
436
+
437
+ # Helper function to find element using multiple strategies (like Appium)
438
+ find_element_by_text() {
439
+ local session="$1"
440
+ local host="$2"
441
+ local text="$3"
442
+ local device_name="$4"
443
+
444
+ local element_id=""
445
+ local found_strategy=""
446
+
447
+ # Strategy 1: Accessibility ID (most reliable for iOS)
448
+ echo "[$device_name] Trying accessibility ID..."
449
+ local elements_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
450
+ -H "Content-Type: application/json" \
451
+ -d "{\"using\": \"accessibility id\", \"value\": \"$text\"}" 2>/dev/null)
452
+
453
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
454
+ element_id=$(echo "$elements_result" | python3 -c "
455
+ import sys, json
456
+ try:
457
+ data = json.load(sys.stdin)
458
+ elements = data.get('value', [])
459
+ if elements and len(elements) > 0:
460
+ print(elements[0].get('ELEMENT', ''))
461
+ except:
462
+ pass" 2>/dev/null)
463
+ found_strategy="accessibility id"
464
+ fi
465
+
466
+ # Strategy 2: Name attribute
467
+ if [ -z "$element_id" ]; then
468
+ echo "[$device_name] Trying name attribute..."
469
+ elements_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
470
+ -H "Content-Type: application/json" \
471
+ -d "{\"using\": \"name\", \"value\": \"$text\"}" 2>/dev/null)
472
+
473
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
474
+ element_id=$(echo "$elements_result" | python3 -c "
475
+ import sys, json
476
+ try:
477
+ data = json.load(sys.stdin)
478
+ elements = data.get('value', [])
479
+ if elements and len(elements) > 0:
480
+ print(elements[0].get('ELEMENT', ''))
481
+ except:
482
+ pass" 2>/dev/null)
483
+ found_strategy="name"
484
+ fi
485
+ fi
486
+
487
+ # Strategy 3: Predicate string (iOS specific - very powerful)
488
+ if [ -z "$element_id" ]; then
489
+ echo "[$device_name] Trying predicate string..."
490
+ local predicate="name CONTAINS '$text' OR label CONTAINS '$text' OR value CONTAINS '$text'"
491
+ elements_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
492
+ -H "Content-Type: application/json" \
493
+ -d "{\"using\": \"predicate string\", \"value\": \"$predicate\"}" 2>/dev/null)
494
+
495
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
496
+ element_id=$(echo "$elements_result" | python3 -c "
497
+ import sys, json
498
+ try:
499
+ data = json.load(sys.stdin)
500
+ elements = data.get('value', [])
501
+ if elements and len(elements) > 0:
502
+ print(elements[0].get('ELEMENT', ''))
503
+ except:
504
+ pass" 2>/dev/null)
505
+ found_strategy="predicate string"
506
+ fi
507
+ fi
508
+
509
+ # Strategy 4: Class chain for buttons (iOS specific)
510
+ if [ -z "$element_id" ]; then
511
+ echo "[$device_name] Trying class chain for buttons..."
512
+ local class_chain="**/XCUIElementTypeButton[\`name CONTAINS '$text'\`]"
513
+ elements_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
514
+ -H "Content-Type: application/json" \
515
+ -d "{\"using\": \"-ios class chain\", \"value\": \"$class_chain\"}" 2>/dev/null)
516
+
517
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
518
+ element_id=$(echo "$elements_result" | python3 -c "
519
+ import sys, json
520
+ try:
521
+ data = json.load(sys.stdin)
522
+ elements = data.get('value', [])
523
+ if elements and len(elements) > 0:
524
+ print(elements[0].get('ELEMENT', ''))
525
+ except:
526
+ pass" 2>/dev/null)
527
+ found_strategy="class chain (button)"
528
+ fi
529
+ fi
530
+
531
+ # Strategy 5: Class chain for any element (broader search)
532
+ if [ -z "$element_id" ]; then
533
+ echo "[$device_name] Trying class chain for any element..."
534
+ local class_chain="**/*[\`name CONTAINS '$text'\`]"
535
+ elements_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
536
+ -H "Content-Type: application/json" \
537
+ -d "{\"using\": \"-ios class chain\", \"value\": \"$class_chain\"}" 2>/dev/null)
538
+
539
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
540
+ element_id=$(echo "$elements_result" | python3 -c "
541
+ import sys, json
542
+ try:
543
+ data = json.load(sys.stdin)
544
+ elements = data.get('value', [])
545
+ if elements and len(elements) > 0:
546
+ print(elements[0].get('ELEMENT', ''))
547
+ except:
548
+ pass" 2>/dev/null)
549
+ found_strategy="class chain (any)"
550
+ fi
551
+ fi
552
+
553
+ # Output results
554
+ if [ -n "$element_id" ]; then
555
+ echo "$element_id|$found_strategy"
556
+ else
557
+ echo "|"
558
+ fi
559
+ }
560
+
561
+ # Helper function to get element center coordinates with multiple methods
562
+ get_element_center() {
563
+ local session="$1"
564
+ local host="$2"
565
+ local element_id="$3"
566
+
567
+ # Method 1: Try /rect endpoint
568
+ local rect_response=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/element/$element_id/rect" \
569
+ -H 'Content-Type: application/json' 2>/dev/null)
570
+
571
+
572
+ if echo "$rect_response" | grep -q '"x"' && echo "$rect_response" | grep -q '"y"'; then
573
+ local x=$(echo "$rect_response" | python3 -c "
574
+ import sys, json
575
+ try:
576
+ data = json.load(sys.stdin)
577
+ rect = data.get('value', {})
578
+ x = rect.get('x', 0)
579
+ width = rect.get('width', 0)
580
+ center_x = int(x + width/2)
581
+ print(center_x)
582
+ except Exception as e:
583
+ print('0')" 2>/dev/null)
584
+
585
+ local y=$(echo "$rect_response" | python3 -c "
586
+ import sys, json
587
+ try:
588
+ data = json.load(sys.stdin)
589
+ rect = data.get('value', {})
590
+ y = rect.get('y', 0)
591
+ height = rect.get('height', 0)
592
+ center_y = int(y + height/2)
593
+ print(center_y)
594
+ except Exception as e:
595
+ print('0')" 2>/dev/null)
596
+
597
+ if [[ "$x" =~ ^[0-9]+$ ]] && [[ "$y" =~ ^[0-9]+$ ]] && [ "$x" -gt 0 ] && [ "$y" -gt 0 ]; then
598
+ echo "$x,$y"
599
+ return
600
+ fi
601
+ fi
602
+
603
+ # Method 2: Try /location endpoint
604
+ local location_response=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/element/$element_id/location" \
605
+ -H 'Content-Type: application/json' 2>/dev/null)
606
+
607
+
608
+ if echo "$location_response" | grep -q '"x"' && echo "$location_response" | grep -q '"y"'; then
609
+ local x=$(echo "$location_response" | python3 -c "
610
+ import sys, json
611
+ try:
612
+ data = json.load(sys.stdin)
613
+ location = data.get('value', {})
614
+ print(int(location.get('x', 0)))
615
+ except:
616
+ print('0')" 2>/dev/null)
617
+
618
+ local y=$(echo "$location_response" | python3 -c "
619
+ import sys, json
620
+ try:
621
+ data = json.load(sys.stdin)
622
+ location = data.get('value', {})
623
+ print(int(location.get('y', 0)))
624
+ except:
625
+ print('0')" 2>/dev/null)
626
+
627
+ if [[ "$x" =~ ^[0-9]+$ ]] && [[ "$y" =~ ^[0-9]+$ ]] && [ "$x" -gt 0 ] && [ "$y" -gt 0 ]; then
628
+ echo "$x,$y"
629
+ return
630
+ fi
631
+ fi
632
+
633
+ # Method 3: Try WebDriverAgent specific endpoint
634
+ local wda_response=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/wda/element/$element_id/accessibilityContainer" \
635
+ -H 'Content-Type: application/json' 2>/dev/null)
636
+
637
+
638
+ # Method 4: Use element attribute to get frame info
639
+ local attr_response=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/element/$element_id/attribute/frame" \
640
+ -H 'Content-Type: application/json' 2>/dev/null)
641
+
642
+
643
+ # If all methods fail, return 0,0
644
+ echo "0,0"
645
+ }
646
+
647
+ cleanup_wda_session() {
648
+ local device_name="$1"
649
+ local preserve_trust="${2:-true}"
650
+ local session=""
651
+
652
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
653
+ session=$(grep "^$device_name:" "$DEVICE_SESSIONS_FILE" | cut -d':' -f2 | tail -1)
654
+ fi
655
+
656
+ if [ -n "$session" ]; then
657
+ local device_details
658
+ device_details=$(get_device_details "$device_name")
659
+ local host=$(echo "$device_details" | cut -d',' -f1)
660
+
661
+ curl -s -X DELETE "http://$host:$WDA_PORT/session/$session" >/dev/null 2>&1
662
+
663
+ # Remove session from tracking
664
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
665
+ grep -v "^$device_name:" "$DEVICE_SESSIONS_FILE" > "${DEVICE_SESSIONS_FILE}.tmp" 2>/dev/null || true
666
+ mv "${DEVICE_SESSIONS_FILE}.tmp" "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
667
+ fi
668
+
669
+ if [ "$preserve_trust" = "true" ] && [ "${preserve_trust}" != "false" ]; then
670
+ print_color "$GREEN" "✅ Session closed for $device_name (WebDriverAgent preserved)"
671
+ fi
672
+ fi
673
+ }
674
+
675
+ # Execute WDA command directly
676
+ execute_wda_command() {
677
+ local device_name="$1"
678
+ local command="$2"
679
+ local log_prefix="[${device_name}]"
680
+ local device_details
681
+ device_details=$(get_device_details "$device_name")
682
+ local host=$(echo "$device_details" | cut -d',' -f1)
683
+
684
+ # Parse command
685
+ local cmd_type=$(echo "$command" | awk '{print $1}')
686
+ local cmd_args=$(echo "$command" | sed "s/^$cmd_type *//" | xargs)
687
+
688
+ # Show what we parsed for debugging
689
+ echo "${log_prefix} Processing command: '$cmd_type' with args: '$cmd_args'"
690
+
691
+ # Check the actual values with explicit debugging
692
+
693
+ case "$cmd_type" in
694
+ "launch"|"open")
695
+ echo "${log_prefix} CASE: Launch/open matched"
696
+ local bundle_id=$(resolve_app_bundle "$cmd_args")
697
+ echo "${log_prefix} Launching: $cmd_args → $bundle_id"
698
+
699
+ # DON'T clean up existing session - just activate the app
700
+ # This prevents the app from closing
701
+
702
+ # Try to activate app with existing session first
703
+ local session=$(get_device_session "$device_name")
704
+
705
+ if [ -n "$session" ]; then
706
+ # Use existing session to activate app
707
+ local result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/apps/activate" \
708
+ -H 'Content-Type: application/json' \
709
+ -d "{\"bundleId\":\"$bundle_id\"}" 2>/dev/null)
710
+
711
+ if echo "$result" | grep -q '"value"'; then
712
+ echo "${log_prefix} ✅ App launched (existing session)"
713
+ else
714
+ # If activation fails, try global endpoint
715
+ local result2=$(curl -s -X POST "http://$host:$WDA_PORT/wda/apps/activate" \
716
+ -H 'Content-Type: application/json' \
717
+ -d "{\"bundleId\":\"$bundle_id\"}" 2>/dev/null)
718
+
719
+ if echo "$result2" | grep -q '"value"'; then
720
+ echo "${log_prefix} ✅ App launched (global)"
721
+ else
722
+ echo "${log_prefix} ❌ Launch failed"
723
+ fi
724
+ fi
725
+ else
726
+ # No session exists, create one with the app
727
+ local new_session=$(curl -s -X POST "http://$host:$WDA_PORT/session" \
728
+ -H 'Content-Type: application/json' \
729
+ -d "{\"capabilities\":{\"alwaysMatch\":{\"bundleId\":\"$bundle_id\"}}}" | \
730
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('sessionId','') or d.get('value',{}).get('sessionId',''))" 2>/dev/null)
731
+
732
+ if [ -n "$new_session" ]; then
733
+ # Store new session
734
+ echo "$device_name:$new_session" >> "$DEVICE_SESSIONS_FILE"
735
+ echo "${log_prefix} ✅ App launched (new session)"
736
+ else
737
+ echo "${log_prefix} ❌ Launch failed"
738
+ fi
739
+ fi
740
+ ;;
741
+
742
+ "kill"|"close"|"terminate")
743
+ local bundle_id=$(resolve_app_bundle "$cmd_args")
744
+ echo "${log_prefix} Terminating: $cmd_args → $bundle_id"
745
+
746
+ # Method 1: Try WDA terminate endpoint
747
+ local session=$(get_device_session "$device_name")
748
+ local success=false
749
+
750
+ if [ -n "$session" ]; then
751
+ echo "${log_prefix} → Trying WDA terminate endpoint..."
752
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/wda/apps/terminate" \
753
+ -H 'Content-Type: application/json' \
754
+ -d "{\"bundleId\":\"$bundle_id\"}")
755
+
756
+ # Debug: Show response for troubleshooting
757
+ # echo "${log_prefix} DEBUG: Response: $response"
758
+
759
+ if echo "$response" | grep -q '"value".*true' && ! echo "$response" | grep -q '"error"'; then
760
+ echo "${log_prefix} ✅ App terminated via WDA"
761
+ success=true
762
+ elif echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
763
+ echo "${log_prefix} ✅ App terminated via WDA"
764
+ success=true
765
+ else
766
+ echo "${log_prefix} → WDA terminate failed, trying alternatives..."
767
+ fi
768
+ fi
769
+
770
+ # Method 2: Try alternative WDA endpoint if first method failed
771
+ if [ "$success" = false ] && [ -n "$session" ]; then
772
+ echo "${log_prefix} → Trying session-based terminate method..."
773
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/apps/terminate" \
774
+ -H 'Content-Type: application/json' \
775
+ -d "{\"bundleId\":\"$bundle_id\"}")
776
+
777
+ if echo "$response" | grep -q '"value".*true' || (echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'); then
778
+ echo "${log_prefix} ✅ App terminated via session API"
779
+ success=true
780
+ else
781
+ echo "${log_prefix} → Session terminate also failed..."
782
+ fi
783
+ fi
784
+
785
+ # Method 3: Try using xcrun devicectl (iOS 17+)
786
+ if [ "$success" = false ]; then
787
+ echo "${log_prefix} → Trying xcrun devicectl..."
788
+ local device_details
789
+ device_details=$(get_device_details "$device_name")
790
+ local udid=$(echo "$device_details" | cut -d',' -f2)
791
+
792
+ if [ -n "$udid" ] && command -v xcrun >/dev/null 2>&1; then
793
+ # Try process kill first
794
+ local kill_result=$(xcrun devicectl device process kill --device "$udid" "$bundle_id" 2>&1)
795
+ if [ $? -eq 0 ]; then
796
+ echo "${log_prefix} ✅ App terminated via xcrun devicectl (process kill)"
797
+ success=true
798
+ else
799
+ # Try app terminate
800
+ local terminate_result=$(xcrun devicectl device apps terminate --device "$udid" "$bundle_id" 2>&1)
801
+ if [ $? -eq 0 ]; then
802
+ echo "${log_prefix} ✅ App terminated via xcrun devicectl (app terminate)"
803
+ success=true
804
+ fi
805
+ fi
806
+ fi
807
+ fi
808
+
809
+ # Method 4: Try launching home screen to background the app
810
+ if [ "$success" = false ]; then
811
+ echo "${log_prefix} → Backgrounding app (launching home screen)..."
812
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/wda/homescreen" \
813
+ -H 'Content-Type: application/json' -d '{}')
814
+
815
+ if ! echo "$response" | grep -q '"error"'; then
816
+ echo "${log_prefix} ✅ App backgrounded (sent to home screen)"
817
+ success=true
818
+ fi
819
+ fi
820
+
821
+ if [ "$success" = false ]; then
822
+ echo "${log_prefix} ❌ All terminate methods failed"
823
+ echo "${log_prefix} 💡 Try manually closing the app or use 'home' command"
824
+ fi
825
+ ;;
826
+
827
+ "home")
828
+ echo "${log_prefix} Going to home screen"
829
+ # Use the same endpoint as original script (no session ID needed)
830
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/wda/homescreen" \
831
+ -H 'Content-Type: application/json' -d '{}')
832
+
833
+ if ! echo "$response" | grep -q '"error"'; then
834
+ echo "${log_prefix} ✅ Went to home"
835
+ else
836
+ echo "${log_prefix} ❌ Home failed"
837
+ fi
838
+ ;;
839
+
840
+ "url")
841
+ local url="$cmd_args"
842
+ if ! echo "$url" | grep -qE '^[a-z]+://'; then
843
+ url="https://$url"
844
+ fi
845
+ echo "${log_prefix} Opening URL: $url"
846
+
847
+ # Use existing session - first activate Safari, then navigate
848
+ local session=$(get_device_session "$device_name")
849
+ if [ -n "$session" ]; then
850
+ # Activate Safari first
851
+ curl -s -X POST "http://$host:$WDA_PORT/wda/apps/activate" \
852
+ -H 'Content-Type: application/json' \
853
+ -d '{"bundleId":"com.apple.mobilesafari"}' >/dev/null 2>&1
854
+ sleep 1
855
+ # Navigate to URL
856
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/url" \
857
+ -H 'Content-Type: application/json' \
858
+ -d "{\"url\":\"$url\"}" >/dev/null 2>&1
859
+ echo "${log_prefix} ✅ URL opened in Safari"
860
+ else
861
+ echo "${log_prefix} ❌ URL open failed"
862
+ fi
863
+ ;;
864
+
865
+ "screenshot")
866
+ echo "${log_prefix} Taking screenshot"
867
+ local session=$(get_device_session "$device_name")
868
+ if [ -n "$session" ]; then
869
+ # Save to webapp screenshots directory
870
+ local screenshots_dir="$SCRIPT_DIR/webapp/backend/screenshots"
871
+ mkdir -p "$screenshots_dir"
872
+ local out_file="${screenshots_dir}/screenshot_ios_${device_name}_$(date +%Y%m%d_%H%M%S).png"
873
+ local response=$(curl -s -X GET "http://$host:$WDA_PORT/screenshot")
874
+
875
+ if echo "$response" | python3 -c "
876
+ import sys,base64,json
877
+ try:
878
+ j=json.load(sys.stdin)
879
+ img_b64=j.get('value','')
880
+ if img_b64:
881
+ with open('$out_file','wb') as f:
882
+ f.write(base64.b64decode(img_b64))
883
+ print('✅ Screenshot saved: $out_file')
884
+ else:
885
+ print('❌ Screenshot failed')
886
+ except:
887
+ print('❌ Screenshot error')
888
+ " 2>/dev/null; then
889
+ echo "${log_prefix} Screenshot completed"
890
+ fi
891
+ else
892
+ echo "${log_prefix} ❌ Screenshot failed"
893
+ fi
894
+ ;;
895
+
896
+ "lock")
897
+ echo "${log_prefix} Locking device"
898
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/wda/lock" \
899
+ -H 'Content-Type: application/json' -d '{}')
900
+
901
+ if ! echo "$response" | grep -q '"error"'; then
902
+ echo "${log_prefix} ✅ Device locked"
903
+ else
904
+ echo "${log_prefix} ❌ Lock failed"
905
+ fi
906
+ ;;
907
+
908
+ "unlock")
909
+ echo "${log_prefix} Unlocking device"
910
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/wda/unlock" \
911
+ -H 'Content-Type: application/json' -d '{}')
912
+
913
+ if ! echo "$response" | grep -q '"error"'; then
914
+ echo "${log_prefix} ✅ Device unlocked"
915
+ else
916
+ echo "${log_prefix} ❌ Unlock failed"
917
+ fi
918
+ ;;
919
+
920
+ "back"|"goback")
921
+ echo "${log_prefix} Going back"
922
+ # iOS doesn't have a universal back button like Android
923
+ # Use swipe from left edge gesture which is the iOS equivalent
924
+ local session=$(get_device_session "$device_name")
925
+ if [ -n "$session" ]; then
926
+ # Swipe from left edge (back gesture)
927
+ local swipe_payload='{
928
+ "actions": [
929
+ {
930
+ "type": "pointer",
931
+ "id": "finger1",
932
+ "parameters": {"pointerType": "touch"},
933
+ "actions": [
934
+ {"type": "pointerMove", "duration": 0, "x": 10, "y": 400},
935
+ {"type": "pointerDown", "button": 0},
936
+ {"type": "pointerMove", "duration": 300, "x": 200, "y": 400},
937
+ {"type": "pointerUp", "button": 0}
938
+ ]
939
+ }
940
+ ]
941
+ }'
942
+
943
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
944
+ -H 'Content-Type: application/json' \
945
+ -d "$swipe_payload" >/dev/null 2>&1
946
+
947
+ echo "${log_prefix} ✅ Back gesture performed"
948
+ else
949
+ echo "${log_prefix} ❌ Back failed"
950
+ fi
951
+ ;;
952
+
953
+ "install")
954
+ local app_path="$cmd_args"
955
+ echo "${log_prefix} Installing app: $app_path"
956
+
957
+ # Check if file exists
958
+ if [ ! -f "$app_path" ]; then
959
+ echo "${log_prefix} ❌ App file not found: $app_path"
960
+ return 1
961
+ fi
962
+
963
+ # Check if it's an IPA file
964
+ if [[ ! "$app_path" =~ \.ipa$ ]]; then
965
+ echo "${log_prefix} ❌ File must be a .ipa file: $app_path"
966
+ return 1
967
+ fi
968
+
969
+ # Get device details for wireless installation
970
+ local device_details
971
+ device_details=$(get_device_details "$device_name")
972
+ local udid=$(echo "$device_details" | cut -d',' -f2)
973
+
974
+ if [ -n "$udid" ]; then
975
+ echo "${log_prefix} 📦 Installing app via wireless-compatible tools..."
976
+
977
+ # For wireless devices, prioritize xcrun devicectl
978
+ if command -v xcrun >/dev/null 2>&1; then
979
+ # Use xcrun devicectl (works with wireless devices on iOS 17+)
980
+ echo "${log_prefix} 🔄 Using xcrun devicectl for wireless installation..."
981
+ local install_result=$(xcrun devicectl device install app --device "$udid" "$app_path" 2>&1)
982
+ if [ $? -eq 0 ]; then
983
+ echo "${log_prefix} ✅ App installed successfully via xcrun devicectl"
984
+ else
985
+ echo "${log_prefix} ❌ Installation failed via xcrun devicectl: $install_result"
986
+ echo "${log_prefix} 💡 For wireless devices, ensure:"
987
+ echo "${log_prefix} • Device is paired and trusted in Xcode"
988
+ echo "${log_prefix} • iOS 17+ for best wireless support"
989
+ fi
990
+ elif command -v ios-deploy >/dev/null 2>&1; then
991
+ # Try ios-deploy with wireless flag
992
+ echo "${log_prefix} 🔄 Trying ios-deploy with wireless support..."
993
+ local install_result=$(ios-deploy --id "$udid" --bundle "$app_path" 2>&1)
994
+ if [ $? -eq 0 ]; then
995
+ echo "${log_prefix} ✅ App installed successfully via ios-deploy"
996
+ else
997
+ echo "${log_prefix} ❌ Installation failed via ios-deploy: $install_result"
998
+ fi
999
+ elif command -v ideviceinstaller >/dev/null 2>&1; then
1000
+ # ideviceinstaller may not work with wireless, but try anyway
1001
+ echo "${log_prefix} 🔄 Trying ideviceinstaller (may require USB)..."
1002
+ local install_result=$(ideviceinstaller -u "$udid" install "$app_path" 2>&1)
1003
+ if [ $? -eq 0 ]; then
1004
+ echo "${log_prefix} ✅ App installed successfully via ideviceinstaller"
1005
+ else
1006
+ echo "${log_prefix} ❌ Installation failed via ideviceinstaller (wireless not supported): $install_result"
1007
+ fi
1008
+ else
1009
+ echo "${log_prefix} ❌ No wireless-compatible installation tools found"
1010
+ echo "${log_prefix} 💡 For wireless installation:"
1011
+ echo "${log_prefix} • Install via Xcode → Window → Devices and Simulators"
1012
+ echo "${log_prefix} • Or use: xcrun devicectl device install app --device $udid $app_path"
1013
+ fi
1014
+ else
1015
+ echo "${log_prefix} ❌ Device UDID not available"
1016
+ fi
1017
+ ;;
1018
+
1019
+ "uninstall")
1020
+ local bundle_id="$cmd_args"
1021
+ echo "${log_prefix} Uninstalling app: $bundle_id"
1022
+
1023
+ # Get device UDID for wireless uninstallation
1024
+ local device_details
1025
+ device_details=$(get_device_details "$device_name")
1026
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1027
+
1028
+ if [ -n "$udid" ]; then
1029
+ echo "${log_prefix} 🗑️ Uninstalling app via wireless-compatible tools..."
1030
+
1031
+ # For wireless devices, prioritize xcrun devicectl
1032
+ if command -v xcrun >/dev/null 2>&1; then
1033
+ # Use xcrun devicectl (works with wireless devices on iOS 17+)
1034
+ echo "${log_prefix} 🔄 Using xcrun devicectl for wireless uninstallation..."
1035
+ local uninstall_result=$(xcrun devicectl device uninstall app --device "$udid" "$bundle_id" 2>&1)
1036
+ if [ $? -eq 0 ]; then
1037
+ echo "${log_prefix} ✅ App uninstalled successfully via xcrun devicectl"
1038
+ else
1039
+ echo "${log_prefix} ❌ Uninstallation failed via xcrun devicectl: $uninstall_result"
1040
+ echo "${log_prefix} 💡 For wireless devices, ensure:"
1041
+ echo "${log_prefix} • Device is paired and trusted in Xcode"
1042
+ echo "${log_prefix} • App bundle ID is correct: $bundle_id"
1043
+ fi
1044
+ elif command -v ideviceinstaller >/dev/null 2>&1; then
1045
+ # ideviceinstaller may not work with wireless, but try anyway
1046
+ echo "${log_prefix} 🔄 Trying ideviceinstaller (may require USB)..."
1047
+ local uninstall_result=$(ideviceinstaller -u "$udid" uninstall "$bundle_id" 2>&1)
1048
+ if [ $? -eq 0 ]; then
1049
+ echo "${log_prefix} ✅ App uninstalled successfully via ideviceinstaller"
1050
+ else
1051
+ echo "${log_prefix} ❌ Uninstallation failed via ideviceinstaller (wireless not supported): $uninstall_result"
1052
+ fi
1053
+ else
1054
+ echo "${log_prefix} ❌ No wireless-compatible uninstallation tools found"
1055
+ echo "${log_prefix} 💡 For wireless uninstallation:"
1056
+ echo "${log_prefix} • Uninstall manually on device (long-press app icon)"
1057
+ echo "${log_prefix} • Or use Xcode → Window → Devices and Simulators"
1058
+ echo "${log_prefix} • Or use: xcrun devicectl device uninstall app --device $udid $bundle_id"
1059
+ fi
1060
+ else
1061
+ echo "${log_prefix} ❌ Device UDID not available"
1062
+ fi
1063
+ ;;
1064
+
1065
+ "listapps"|"apps")
1066
+ local search_term="$cmd_args"
1067
+ if [ -n "$search_term" ]; then
1068
+ echo "${log_prefix} 🔍 Searching installed apps for: '$search_term'"
1069
+ echo "${log_prefix} 💡 Searching by app name, bundle ID, and display name..."
1070
+ else
1071
+ echo "${log_prefix} Listing all installed apps..."
1072
+ fi
1073
+
1074
+ # For wireless devices, use WebDriverAgent or xcrun devicectl
1075
+ local device_details
1076
+ device_details=$(get_device_details "$device_name")
1077
+ local host=$(echo "$device_details" | cut -d',' -f1)
1078
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1079
+
1080
+ echo "${log_prefix} 📱 Using wireless device tools for app listing..."
1081
+
1082
+ # Method 1: Try xcrun devicectl for wireless devices (iOS 17+)
1083
+ if command -v xcrun >/dev/null 2>&1 && [ -n "$udid" ]; then
1084
+ echo "${log_prefix} 🔄 Trying xcrun devicectl (wireless-compatible)..."
1085
+
1086
+ if [ -n "$search_term" ]; then
1087
+ # First try exact bundle ID match
1088
+ local list_result=$(xcrun devicectl device info apps --device "$udid" --bundle-id "$search_term" 2>&1)
1089
+ local found_exact=false
1090
+ if [ $? -eq 0 ] && [ -n "$list_result" ] && ! echo "$list_result" | grep -q "No matching apps found"; then
1091
+ echo "${log_prefix} ✅ Found exact bundle ID match: $search_term"
1092
+ echo "$list_result" | while read line; do
1093
+ echo "${log_prefix} $line"
1094
+ done
1095
+ found_exact=true
1096
+ fi
1097
+
1098
+ # Always also search by app name/partial match for better user experience
1099
+ local list_result=$(xcrun devicectl device info apps --device "$udid" --include-all-apps 2>&1)
1100
+ if [ $? -eq 0 ]; then
1101
+ # Enhanced search: look for matches in app name, display name, and bundle ID
1102
+ local filtered_apps=$(echo "$list_result" | grep -i "$search_term" 2>/dev/null)
1103
+ if [ -n "$filtered_apps" ]; then
1104
+ if [ "$found_exact" = false ]; then
1105
+ echo "${log_prefix} ✅ Found matching apps:"
1106
+ else
1107
+ echo "${log_prefix} ✅ Additional matches found:"
1108
+ fi
1109
+ echo "$filtered_apps" | while read line; do
1110
+ echo "${log_prefix} $line"
1111
+ done
1112
+ return
1113
+ elif [ "$found_exact" = false ]; then
1114
+ echo "${log_prefix} ❌ No apps found matching: '$search_term'"
1115
+ fi
1116
+ fi
1117
+ else
1118
+ # List all apps
1119
+ local list_result=$(xcrun devicectl device info apps --device "$udid" --include-all-apps 2>&1)
1120
+ if [ $? -eq 0 ]; then
1121
+ echo "${log_prefix} ✅ App list retrieved via xcrun devicectl"
1122
+ echo "${log_prefix} ✅ Installed apps:"
1123
+ echo "$list_result" | head -20 | while read line; do
1124
+ echo "${log_prefix} $line"
1125
+ done
1126
+ local app_count=$(echo "$list_result" | wc -l)
1127
+ if [ "$app_count" -gt 20 ]; then
1128
+ echo "${log_prefix} ... and $((app_count - 20)) more apps"
1129
+ fi
1130
+ return
1131
+ else
1132
+ echo "${log_prefix} ❌ xcrun devicectl failed: $list_result"
1133
+ fi
1134
+ fi
1135
+ fi
1136
+
1137
+ # Method 2: Use WebDriverAgent to get basic app info
1138
+ local session=$(get_device_session "$device_name")
1139
+ if [ -n "$session" ] && [ -n "$host" ]; then
1140
+ echo "${log_prefix} 🔄 Trying WebDriverAgent for basic app detection..."
1141
+
1142
+ # Test a few common apps to demonstrate the approach
1143
+ local test_apps=(
1144
+ "com.apple.mobilesafari"
1145
+ "com.apple.mobilemail"
1146
+ "com.apple.MobileSMS"
1147
+ "com.apple.camera"
1148
+ "com.apple.mobilenotes"
1149
+ "com.apple.Preferences"
1150
+ "com.mobileiron.enterprise.anyware.ios"
1151
+ "com.facebook.Facebook"
1152
+ "com.google.chrome.ios"
1153
+ "com.microsoft.Office.Outlook"
1154
+ )
1155
+
1156
+ echo "${log_prefix} 📱 Checking common/enterprise apps via WebDriverAgent..."
1157
+ local found_apps=()
1158
+
1159
+ for bundle_id in "${test_apps[@]}"; do
1160
+ local app_state_response=$(curl -s -X GET "http://$host:$WDA_PORT/wda/apps/state" \
1161
+ -H 'Content-Type: application/json' \
1162
+ -d "{\"bundleId\":\"$bundle_id\"}" 2>/dev/null)
1163
+
1164
+ if echo "$app_state_response" | grep -q '"value"' && ! echo "$app_state_response" | grep -q '"error"'; then
1165
+ local state_value=$(echo "$app_state_response" | python3 -c "
1166
+ import sys, json
1167
+ try:
1168
+ data = json.load(sys.stdin)
1169
+ state = data.get('value', 0)
1170
+ if state > 0: # App is installed if state > 0
1171
+ print('installed')
1172
+ else:
1173
+ print('not_installed')
1174
+ except:
1175
+ print('error')" 2>/dev/null)
1176
+
1177
+ if [ "$state_value" = "installed" ]; then
1178
+ found_apps+=("$bundle_id")
1179
+ fi
1180
+ fi
1181
+ done
1182
+
1183
+ if [ ${#found_apps[@]} -gt 0 ]; then
1184
+ echo "${log_prefix} ✅ Found installed apps via WebDriverAgent:"
1185
+ local matches=0
1186
+ for app in "${found_apps[@]}"; do
1187
+ if [ -z "$search_term" ] || echo "$app" | grep -qi "$search_term"; then
1188
+ echo "${log_prefix} $app - installed"
1189
+ ((matches++))
1190
+ fi
1191
+ done
1192
+
1193
+ if [ -n "$search_term" ] && [ $matches -eq 0 ]; then
1194
+ echo "${log_prefix} ❌ No apps found matching: '$search_term'"
1195
+ elif [ -n "$search_term" ]; then
1196
+ echo "${log_prefix} 🎯 Found $matches app(s) matching: '$search_term'"
1197
+ fi
1198
+ else
1199
+ echo "${log_prefix} ❌ No apps detected via WebDriverAgent"
1200
+ fi
1201
+ else
1202
+ echo "${log_prefix} ❌ No WebDriverAgent session available"
1203
+ fi
1204
+
1205
+ # Method 3: Suggest manual verification
1206
+ echo "${log_prefix} 💡 For complete app listing on wireless devices:"
1207
+ echo "${log_prefix} • Use Xcode → Window → Devices and Simulators"
1208
+ echo "${log_prefix} • Or check specific apps with: appinfo <bundle_id>"
1209
+ ;;
1210
+
1211
+ "debug"|"test")
1212
+ echo "${log_prefix} 🔍 Running debug tests..."
1213
+
1214
+ # Get device UDID
1215
+ local device_details
1216
+ device_details=$(get_device_details "$device_name")
1217
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1218
+
1219
+ echo "${log_prefix} Device: $device_name"
1220
+ echo "${log_prefix} UDID: $udid"
1221
+
1222
+ if [ -n "$udid" ]; then
1223
+ echo "${log_prefix} 🧪 Testing idevice tools..."
1224
+
1225
+ # Test 1: Check if device is detected
1226
+ echo "${log_prefix} Test 1: Device detection"
1227
+ if command -v idevice_id >/dev/null 2>&1; then
1228
+ local detected_devices=$(idevice_id -l 2>/dev/null)
1229
+ if echo "$detected_devices" | grep -q "$udid"; then
1230
+ echo "${log_prefix} ✅ Device detected by idevice_id"
1231
+ else
1232
+ echo "${log_prefix} ❌ Device NOT detected by idevice_id"
1233
+ echo "${log_prefix} Available devices: $detected_devices"
1234
+ fi
1235
+ else
1236
+ echo "${log_prefix} ❌ idevice_id not found"
1237
+ fi
1238
+
1239
+ # Test 2: Raw ideviceinstaller command
1240
+ echo "${log_prefix} Test 2: Raw ideviceinstaller"
1241
+ local raw_command="ideviceinstaller -u '$udid' list --user"
1242
+ echo "${log_prefix} Command: $raw_command"
1243
+ local raw_result=$(eval $raw_command 2>&1)
1244
+ local raw_exit=$?
1245
+ echo "${log_prefix} Exit code: $raw_exit"
1246
+ if [ $raw_exit -eq 0 ]; then
1247
+ local app_count=$(echo "$raw_result" | wc -l)
1248
+ echo "${log_prefix} ✅ Success! Found $app_count apps"
1249
+ echo "${log_prefix} Sample apps:"
1250
+ echo "$raw_result" | head -3 | while read line; do
1251
+ echo "${log_prefix} $line"
1252
+ done
1253
+ else
1254
+ echo "${log_prefix} ❌ Failed: $raw_result"
1255
+ fi
1256
+
1257
+ # Test 3: Your specific app search
1258
+ echo "${log_prefix} Test 3: MobileIron app search"
1259
+ local mobileiron_search=$(eval $raw_command 2>/dev/null | grep -i mobileiron)
1260
+ if [ -n "$mobileiron_search" ]; then
1261
+ echo "${log_prefix} ✅ Found MobileIron apps:"
1262
+ echo "$mobileiron_search" | while read line; do
1263
+ echo "${log_prefix} $line"
1264
+ done
1265
+ else
1266
+ echo "${log_prefix} ❌ No MobileIron apps found"
1267
+ fi
1268
+
1269
+ else
1270
+ echo "${log_prefix} ❌ No UDID available for testing"
1271
+ fi
1272
+ ;;
1273
+
1274
+ "appinfo"|"version")
1275
+ local bundle_id="$cmd_args"
1276
+ echo "${log_prefix} Getting app info: $bundle_id"
1277
+
1278
+ # Get device UDID for app info
1279
+ local device_details
1280
+ device_details=$(get_device_details "$device_name")
1281
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1282
+
1283
+ if [ -n "$udid" ]; then
1284
+ echo "${log_prefix} 📱 Checking app info via iOS tools..."
1285
+
1286
+ # Try different methods to get app info
1287
+ if command -v ideviceinstaller >/dev/null 2>&1; then
1288
+ # Method 1: Search by exact bundle ID
1289
+ echo "${log_prefix} 🔍 Searching for exact bundle ID: $bundle_id"
1290
+ local app_info=$(ideviceinstaller -u "$udid" list --bundle-identifier "$bundle_id" 2>/dev/null)
1291
+
1292
+ if [ -n "$app_info" ]; then
1293
+ echo "${log_prefix} ✅ App found (exact match):"
1294
+ echo "${log_prefix} $app_info"
1295
+ else
1296
+ # Method 2: List all apps and grep for partial matches
1297
+ echo "${log_prefix} 🔍 Searching all installed apps for pattern..."
1298
+ local all_apps=$(ideviceinstaller -u "$udid" list --all 2>/dev/null)
1299
+ local found_apps=$(echo "$all_apps" | grep -i "$bundle_id" 2>/dev/null)
1300
+
1301
+ if [ -n "$found_apps" ]; then
1302
+ echo "${log_prefix} ✅ Found matching apps:"
1303
+ echo "$found_apps" | while read line; do
1304
+ echo "${log_prefix} $line"
1305
+ done
1306
+ else
1307
+ # Method 3: Search by partial bundle ID (useful for enterprise apps)
1308
+ local partial_search=$(echo "$bundle_id" | sed 's/.*\.//') # Get last part after dot
1309
+ local partial_apps=$(echo "$all_apps" | grep -i "$partial_search" 2>/dev/null)
1310
+
1311
+ if [ -n "$partial_apps" ]; then
1312
+ echo "${log_prefix} ✅ Found apps with similar names:"
1313
+ echo "$partial_apps" | while read line; do
1314
+ echo "${log_prefix} $line"
1315
+ done
1316
+ else
1317
+ echo "${log_prefix} ❌ App not found: $bundle_id"
1318
+ fi
1319
+ fi
1320
+ fi
1321
+ elif command -v xcrun >/dev/null 2>&1; then
1322
+ # Use xcrun devicectl to get app info (iOS 17+)
1323
+ echo "${log_prefix} 🔄 Checking via xcrun devicectl..."
1324
+ local app_info=$(xcrun devicectl device info apps --device "$udid" --bundle-id "$bundle_id" 2>&1)
1325
+
1326
+ if [ $? -eq 0 ] && [ -n "$app_info" ] && ! echo "$app_info" | grep -q "No matching apps found"; then
1327
+ echo "${log_prefix} ✅ App info:"
1328
+ echo "$app_info" | while read line; do
1329
+ echo "${log_prefix} $line"
1330
+ done
1331
+ else
1332
+ echo "${log_prefix} ❌ App not found or info unavailable: $bundle_id"
1333
+ echo "${log_prefix} Response: $app_info"
1334
+ fi
1335
+ else
1336
+ echo "${log_prefix} ❌ No iOS app info tools found"
1337
+ echo "${log_prefix} Please install ideviceinstaller or use Xcode"
1338
+ fi
1339
+
1340
+ # Alternative: Try to get app info through WebDriverAgent if available
1341
+ local session=$(get_device_session "$device_name")
1342
+ if [ -n "$session" ]; then
1343
+ echo "${log_prefix} 🔍 Checking if app is installed via WDA..."
1344
+ local wda_response=$(curl -s -X GET "http://$host:$WDA_PORT/wda/apps/state" \
1345
+ -H 'Content-Type: application/json' \
1346
+ -d "{\"bundleId\":\"$bundle_id\"}" 2>/dev/null)
1347
+
1348
+ if echo "$wda_response" | grep -q '"value"' && ! echo "$wda_response" | grep -q '"error"'; then
1349
+ local app_state=$(echo "$wda_response" | python3 -c "
1350
+ import sys, json
1351
+ try:
1352
+ data = json.load(sys.stdin)
1353
+ state = data.get('value', 0)
1354
+ states = {0: 'Not installed', 1: 'Not running', 2: 'Running in background', 3: 'Running in foreground', 4: 'Running but suspended'}
1355
+ print(f'App State: {states.get(state, f\"Unknown ({state})\")}')" 2>/dev/null)
1356
+
1357
+ if [ -n "$app_state" ]; then
1358
+ echo "${log_prefix} ✅ $app_state"
1359
+ fi
1360
+ fi
1361
+ fi
1362
+ else
1363
+ echo "${log_prefix} ❌ Device UDID not available"
1364
+ fi
1365
+ ;;
1366
+
1367
+ "darkmode"|"lightmode"|"theme"|"appearance")
1368
+ if [ "$cmd_type" = "appearance" ] && [ -z "$cmd_args" ]; then
1369
+ # Just check current appearance
1370
+ echo "${log_prefix} Checking current appearance..."
1371
+ check_appearance_mode "$device_name"
1372
+ else
1373
+ local mode=""
1374
+ case "$cmd_type" in
1375
+ "darkmode") mode="dark" ;;
1376
+ "lightmode") mode="light" ;;
1377
+ "theme") mode="$cmd_args" ;;
1378
+ "appearance") mode="$cmd_args" ;;
1379
+ esac
1380
+ if [ -z "$mode" ]; then
1381
+ mode="toggle"
1382
+ fi
1383
+ echo "${log_prefix} Setting theme to: $mode"
1384
+
1385
+ # Try multiple methods for theme switching
1386
+ if ! toggle_dark_mode "$device_name" "$mode"; then
1387
+ # Fallback: Try direct URL scheme approach
1388
+ echo "${log_prefix} Trying alternative method..."
1389
+ local session=$(get_device_session "$device_name")
1390
+ if [ -n "$session" ]; then
1391
+ case "$mode" in
1392
+ "dark"|"light")
1393
+ # Use Settings URL scheme
1394
+ local url_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/url" \
1395
+ -H "Content-Type: application/json" \
1396
+ -d '{"url": "prefs:root=DISPLAY"}' 2>/dev/null)
1397
+
1398
+ if echo "$url_result" | grep -q '"sessionId"'; then
1399
+ echo "${log_prefix} ✅ Opened Display settings - manual selection may be needed"
1400
+ sleep 3
1401
+ execute_wda_command "$device_name" "home" >/dev/null
1402
+ else
1403
+ echo "${log_prefix} 💡 Please manually go to Settings > Display & Brightness > Appearance"
1404
+ fi
1405
+ ;;
1406
+ esac
1407
+ fi
1408
+ fi
1409
+ fi
1410
+ ;;
1411
+
1412
+ "rotate"|"rotation")
1413
+ local orientation="$cmd_args"
1414
+ if [ -z "$orientation" ]; then
1415
+ orientation="toggle"
1416
+ fi
1417
+ echo "${log_prefix} Rotating screen to: $orientation"
1418
+ rotate_screen "$device_name" "$orientation"
1419
+ ;;
1420
+
1421
+ "rotationlock"|"lockrotation")
1422
+ local lock_state="$cmd_args"
1423
+ if [ -z "$lock_state" ]; then
1424
+ lock_state="toggle"
1425
+ fi
1426
+ echo "${log_prefix} Setting rotation lock: $lock_state"
1427
+ toggle_rotation_lock "$device_name" "$lock_state"
1428
+ ;;
1429
+
1430
+ "restart"|"reboot")
1431
+ echo "${log_prefix} 🔄 Attempting device restart..."
1432
+
1433
+ # Get device UDID for wireless restart
1434
+ local device_details
1435
+ device_details=$(get_device_details "$device_name")
1436
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1437
+
1438
+ if [ -n "$udid" ]; then
1439
+ echo "${log_prefix} 🔄 Restarting device via wireless-compatible tools..."
1440
+
1441
+ # Method 1: Try xcrun devicectl (works with wireless devices on iOS 17+)
1442
+ if command -v xcrun >/dev/null 2>&1; then
1443
+ echo "${log_prefix} 🔄 Using xcrun devicectl for wireless restart..."
1444
+
1445
+ # First try with hardware UDID
1446
+ local restart_result=$(xcrun devicectl device reboot --device "$udid" --timeout 10 2>&1)
1447
+ if [ $? -eq 0 ]; then
1448
+ echo "${log_prefix} ✅ Restart command sent via xcrun devicectl (UDID)"
1449
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1450
+ else
1451
+ # Try to find device by name in devicectl list and use that identifier
1452
+ echo "${log_prefix} 🔍 Searching for device in CoreDevice list..."
1453
+ local coredevice_info=$(xcrun devicectl list devices --timeout 5 2>/dev/null | grep -v "Name\|--" | head -20)
1454
+
1455
+ # Look for connected devices first, then any device
1456
+ local target_device=""
1457
+ if [ -n "$coredevice_info" ]; then
1458
+ # First try to find a connected device
1459
+ target_device=$(echo "$coredevice_info" | grep "connected" | head -1 | awk '{print $1}')
1460
+
1461
+ # If no connected device, try any available device
1462
+ if [ -z "$target_device" ]; then
1463
+ target_device=$(echo "$coredevice_info" | head -1 | awk '{print $1}')
1464
+ fi
1465
+ fi
1466
+
1467
+ if [ -n "$target_device" ]; then
1468
+ echo "${log_prefix} 🎯 Found target device: $target_device"
1469
+ local retry_result=$(xcrun devicectl device reboot --device "$target_device" --timeout 10 2>&1)
1470
+ if [ $? -eq 0 ]; then
1471
+ echo "${log_prefix} ✅ Restart command sent via xcrun devicectl (device name)"
1472
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1473
+ else
1474
+ echo "${log_prefix} ❌ Restart failed with both UDID and device name"
1475
+ echo "${log_prefix} 💡 Error: $retry_result"
1476
+
1477
+ # Final fallback to USB method
1478
+ echo "${log_prefix} 🔄 Trying USB fallback..."
1479
+ local device_control_script="$SCRIPT_DIR/ios_device_control.sh"
1480
+ if [ -f "$device_control_script" ] && "$device_control_script" restart "$device_name" 2>&1 | grep -q "✅"; then
1481
+ echo "${log_prefix} ✅ Restart command sent via USB fallback"
1482
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1483
+ else
1484
+ echo "${log_prefix} ❌ All restart methods failed"
1485
+ echo "${log_prefix} 💡 Try connecting device via USB cable"
1486
+ fi
1487
+ fi
1488
+ else
1489
+ echo "${log_prefix} ❌ No devices found in CoreDevice list"
1490
+ echo "${log_prefix} 💡 Ensure device is paired and trusted in Xcode"
1491
+ fi
1492
+ fi
1493
+ elif command -v idevicediagnostics >/dev/null 2>&1; then
1494
+ # Fallback to USB-based restart
1495
+ echo "${log_prefix} 🔄 xcrun not available, trying USB restart..."
1496
+ if idevicediagnostics -u "$udid" restart 2>/dev/null; then
1497
+ echo "${log_prefix} ✅ Restart command sent via USB"
1498
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1499
+ else
1500
+ echo "${log_prefix} ❌ Restart failed - device must be USB connected"
1501
+ echo "${log_prefix} 💡 For wireless devices, ensure iOS 17+ and Xcode pairing"
1502
+ fi
1503
+ else
1504
+ echo "${log_prefix} ❌ No restart tools available"
1505
+ echo "${log_prefix} 💡 Install Xcode command line tools or libimobiledevice"
1506
+ fi
1507
+ else
1508
+ echo "${log_prefix} ❌ Device UDID not available"
1509
+ fi
1510
+ ;;
1511
+
1512
+ "shutdown"|"poweroff")
1513
+ echo "${log_prefix} 🔌 Attempting device shutdown..."
1514
+ echo "${log_prefix} ⚠️ WARNING: Wireless shutdown not supported - will attempt restart instead!"
1515
+
1516
+ # Get device UDID for wireless operations
1517
+ local device_details
1518
+ device_details=$(get_device_details "$device_name")
1519
+ local udid=$(echo "$device_details" | cut -d',' -f2)
1520
+
1521
+ if [ -n "$udid" ]; then
1522
+ echo "${log_prefix} 🔄 Note: xcrun devicectl doesn't support shutdown, using restart..."
1523
+
1524
+ # Method 1: Try xcrun devicectl reboot (closest alternative for wireless)
1525
+ if command -v xcrun >/dev/null 2>&1; then
1526
+ echo "${log_prefix} 🔄 Using xcrun devicectl for wireless restart (shutdown not supported)..."
1527
+
1528
+ # First try with hardware UDID
1529
+ local restart_result=$(xcrun devicectl device reboot --device "$udid" --timeout 10 2>&1)
1530
+ if [ $? -eq 0 ]; then
1531
+ echo "${log_prefix} ⚠️ Device will restart instead of shutdown (wireless limitation)"
1532
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1533
+ else
1534
+ # Try to find device by name in devicectl list
1535
+ echo "${log_prefix} 🔍 Searching for device in CoreDevice list..."
1536
+ local coredevice_info=$(xcrun devicectl list devices --timeout 5 2>/dev/null | grep -v "Name\|--" | head -20)
1537
+
1538
+ # Look for connected devices first
1539
+ local target_device=""
1540
+ if [ -n "$coredevice_info" ]; then
1541
+ target_device=$(echo "$coredevice_info" | grep "connected" | head -1 | awk '{print $1}')
1542
+ if [ -z "$target_device" ]; then
1543
+ target_device=$(echo "$coredevice_info" | head -1 | awk '{print $1}')
1544
+ fi
1545
+ fi
1546
+
1547
+ if [ -n "$target_device" ]; then
1548
+ echo "${log_prefix} 🎯 Found target device: $target_device"
1549
+ local retry_result=$(xcrun devicectl device reboot --device "$target_device" --timeout 10 2>&1)
1550
+ if [ $? -eq 0 ]; then
1551
+ echo "${log_prefix} ⚠️ Device will restart instead of shutdown (wireless limitation)"
1552
+ echo "${log_prefix} ⏳ Device will reboot in a few seconds..."
1553
+ else
1554
+ echo "${log_prefix} ❌ Wireless restart failed, trying USB shutdown..."
1555
+
1556
+ # Final fallback to USB shutdown
1557
+ local device_control_script="$SCRIPT_DIR/ios_device_control.sh"
1558
+ if [ -f "$device_control_script" ]; then
1559
+ # Auto-confirm for batch operations
1560
+ if echo "y" | "$device_control_script" shutdown "$device_name" 2>&1 | grep -q "✅"; then
1561
+ echo "${log_prefix} ✅ Shutdown command sent via USB fallback"
1562
+ echo "${log_prefix} ⏳ Device will power off in a few seconds..."
1563
+ else
1564
+ echo "${log_prefix} ❌ Both wireless and USB methods failed"
1565
+ echo "${log_prefix} 💡 Try connecting via USB or manually power off device"
1566
+ fi
1567
+ fi
1568
+ fi
1569
+ else
1570
+ echo "${log_prefix} ❌ No devices found for wireless operation"
1571
+ echo "${log_prefix} 💡 Ensure device is paired and trusted in Xcode"
1572
+ fi
1573
+ fi
1574
+ elif command -v idevicediagnostics >/dev/null 2>&1; then
1575
+ # Fallback to USB-based shutdown
1576
+ echo "${log_prefix} 🔄 xcrun not available, trying USB shutdown..."
1577
+ if idevicediagnostics -u "$udid" shutdown 2>/dev/null; then
1578
+ echo "${log_prefix} ✅ Shutdown command sent via USB"
1579
+ echo "${log_prefix} ⏳ Device will power off in a few seconds..."
1580
+ else
1581
+ echo "${log_prefix} ❌ Shutdown failed - device must be USB connected"
1582
+ echo "${log_prefix} 💡 For wireless devices, only restart is supported"
1583
+ fi
1584
+ else
1585
+ echo "${log_prefix} ❌ No device control tools available"
1586
+ echo "${log_prefix} 💡 Install Xcode command line tools or libimobiledevice"
1587
+ fi
1588
+ else
1589
+ echo "${log_prefix} ❌ Device UDID not available"
1590
+ fi
1591
+ ;;
1592
+
1593
+ "back"|"goback"|"navigate back"|"←")
1594
+ echo "${log_prefix} CASE: Back navigation matched!"
1595
+ local back_context="$cmd_args"
1596
+
1597
+ local session=$(get_device_session "$device_name")
1598
+ if [ -n "$session" ]; then
1599
+ echo "${log_prefix} 🔙 Executing smart back navigation..."
1600
+
1601
+ # Method 1: Try back button click first (most reliable)
1602
+ echo "${log_prefix} Trying Method 1: Back button click..."
1603
+ local back_click_payload='{
1604
+ "actions": [
1605
+ {
1606
+ "type": "pointer",
1607
+ "id": "finger1",
1608
+ "parameters": {"pointerType": "touch"},
1609
+ "actions": [
1610
+ {"type": "pointerMove", "duration": 0, "x": 40, "y": 110},
1611
+ {"type": "pointerDown", "button": 0},
1612
+ {"type": "pause", "duration": 150},
1613
+ {"type": "pointerUp", "button": 0}
1614
+ ]
1615
+ }
1616
+ ]
1617
+ }'
1618
+
1619
+ local click_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
1620
+ -H 'Content-Type: application/json' \
1621
+ -d "$back_click_payload" 2>/dev/null)
1622
+
1623
+ if echo "$click_response" | grep -q '"sessionId"' && ! echo "$click_response" | grep -q '"error"'; then
1624
+ echo "${log_prefix} ✅ Back navigation via button click"
1625
+ else
1626
+ # Method 2: Try improved iOS native back gesture (proper edge swipe)
1627
+ echo "${log_prefix} Trying Method 2: iOS back gesture (proper edge swipe)..."
1628
+ local swipe_payload='{
1629
+ "actions": [
1630
+ {
1631
+ "type": "pointer",
1632
+ "id": "finger1",
1633
+ "parameters": {"pointerType": "touch"},
1634
+ "actions": [
1635
+ {"type": "pointerMove", "duration": 0, "x": 0, "y": 400},
1636
+ {"type": "pointerDown", "button": 0},
1637
+ {"type": "pointerMove", "duration": 800, "x": 250, "y": 400},
1638
+ {"type": "pointerUp", "button": 0}
1639
+ ]
1640
+ }
1641
+ ]
1642
+ }'
1643
+
1644
+ local swipe_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
1645
+ -H 'Content-Type: application/json' \
1646
+ -d "$swipe_payload" 2>/dev/null)
1647
+
1648
+ if echo "$swipe_response" | grep -q '"sessionId"' && ! echo "$swipe_response" | grep -q '"error"'; then
1649
+ echo "${log_prefix} ✅ Back navigation via edge swipe"
1650
+ else
1651
+ # Method 3: Try WebDriverAgent drag method as fallback
1652
+ echo "${log_prefix} Trying Method 3: WDA drag method..."
1653
+ local drag_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/dragfromtoforduration" \
1654
+ -H 'Content-Type: application/json' \
1655
+ -d '{"fromX": 0, "fromY": 400, "toX": 250, "toY": 400, "duration": 0.8}' 2>/dev/null)
1656
+
1657
+ if echo "$drag_response" | grep -q '"sessionId"' && ! echo "$drag_response" | grep -q '"error"'; then
1658
+ echo "${log_prefix} ✅ Back navigation via drag method"
1659
+ else
1660
+ # Method 4: Try context-specific back navigation
1661
+ echo "${log_prefix} Trying Method 4: Context-specific navigation..."
1662
+ case "${back_context,,}" in
1663
+ "settings"|"preferences")
1664
+ # Settings app back - try Settings-specific position
1665
+ local settings_back='{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":35,"y":94},{"type":"pointerDown","button":0},{"type":"pause","duration":100},{"type":"pointerUp","button":0}]}]}'
1666
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" -H 'Content-Type: application/json' -d "$settings_back" >/dev/null
1667
+ echo "${log_prefix} ✅ Settings back navigation"
1668
+ ;;
1669
+ "safari"|"browser"|"web")
1670
+ # Safari back navigation
1671
+ local safari_back='{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":44,"y":94},{"type":"pointerDown","button":0},{"type":"pause","duration":100},{"type":"pointerUp","button":0}]}]}'
1672
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" -H 'Content-Type: application/json' -d "$safari_back" >/dev/null
1673
+ echo "${log_prefix} ✅ Safari back navigation"
1674
+ ;;
1675
+ *)
1676
+ # Method 5: Hardware home button simulation (universal fallback)
1677
+ echo "${log_prefix} Trying Method 5: Home button fallback..."
1678
+ local home_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/homescreen" \
1679
+ -H 'Content-Type: application/json' -d '{}' 2>/dev/null)
1680
+
1681
+ if echo "$home_response" | grep -q '"sessionId"' && ! echo "$home_response" | grep -q '"error"'; then
1682
+ echo "${log_prefix} ✅ Navigated to home screen"
1683
+ else
1684
+ echo "${log_prefix} ❌ All back navigation methods failed"
1685
+ fi
1686
+ ;;
1687
+ esac
1688
+ fi
1689
+ fi
1690
+ fi
1691
+ else
1692
+ echo "${log_prefix} ❌ No session available"
1693
+ fi
1694
+ ;;
1695
+
1696
+ "click"|"tap")
1697
+ echo "${log_prefix} CASE: Accessibility-first Click Activated"
1698
+ local coords="$cmd_args"
1699
+
1700
+ if [[ "$coords" =~ ^[0-9]+,[0-9]+$ ]]; then
1701
+ # Direct coordinates - use precise W3C Actions API
1702
+ local x=$(echo "$coords" | cut -d',' -f1)
1703
+ local y=$(echo "$coords" | cut -d',' -f2)
1704
+ echo "${log_prefix} 🎯 Precise coordinate click: ($x, $y)"
1705
+
1706
+ local session=$(get_device_session "$device_name")
1707
+ if [ -n "$session" ]; then
1708
+ local actions_payload='{
1709
+ "actions": [
1710
+ {
1711
+ "type": "pointer",
1712
+ "id": "finger1",
1713
+ "parameters": {"pointerType": "touch"},
1714
+ "actions": [
1715
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
1716
+ {"type": "pointerDown", "button": 0},
1717
+ {"type": "pause", "duration": 100},
1718
+ {"type": "pointerUp", "button": 0}
1719
+ ]
1720
+ }
1721
+ ]
1722
+ }'
1723
+
1724
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
1725
+ -H 'Content-Type: application/json' \
1726
+ -d "$actions_payload" 2>/dev/null)
1727
+
1728
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
1729
+ echo "${log_prefix} ✅ Coordinate click successful at ($x, $y)"
1730
+ else
1731
+ echo "${log_prefix} ❌ Coordinate click failed"
1732
+ echo "${log_prefix} Response: $response"
1733
+ fi
1734
+ else
1735
+ echo "${log_prefix} ❌ No session available"
1736
+ fi
1737
+ else
1738
+ # Non-AI click: Inline accessibility-first implementation
1739
+ local text="$coords"
1740
+ echo "${log_prefix} 🔎 Accessibility search: '$text'"
1741
+ local session=$(get_device_session "$device_name")
1742
+ if [ -n "$session" ]; then
1743
+ # Method 1: Try accessibility ID
1744
+ local acc_elements=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
1745
+ -H 'Content-Type: application/json' \
1746
+ -d '{"using":"accessibility id","value":"'"$text"'"}' 2>/dev/null)
1747
+
1748
+ local element_id=$(echo "$acc_elements" | python3 -c "
1749
+ import sys, json
1750
+ try:
1751
+ data = json.load(sys.stdin)
1752
+ elements = data.get('value', [])
1753
+ if elements and 'ELEMENT' in elements[0]:
1754
+ print(elements[0]['ELEMENT'])
1755
+ except: pass
1756
+ " 2>/dev/null)
1757
+
1758
+ if [ -z "$element_id" ]; then
1759
+ # Method 2: Try name attribute
1760
+ local name_elements=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
1761
+ -H 'Content-Type: application/json' \
1762
+ -d '{"using":"name","value":"'"$text"'"}' 2>/dev/null)
1763
+ element_id=$(echo "$name_elements" | python3 -c "
1764
+ import sys, json
1765
+ try:
1766
+ data = json.load(sys.stdin)
1767
+ elements = data.get('value', [])
1768
+ if elements and 'ELEMENT' in elements[0]:
1769
+ print(elements[0]['ELEMENT'])
1770
+ except: pass
1771
+ " 2>/dev/null)
1772
+ fi
1773
+
1774
+ # Method 3: Try predicate string (searches across all apps including system)
1775
+ if [ -z "$element_id" ]; then
1776
+ echo "${log_prefix} 🔍 Searching with predicate string..."
1777
+ # Try multiple predicate variations for better matching
1778
+ for pred_query in \
1779
+ "type == 'XCUIElementTypeButton' AND (name CONTAINS[cd] '$text' OR label CONTAINS[cd] '$text')" \
1780
+ "name CONTAINS[cd] '$text' OR label CONTAINS[cd] '$text'" \
1781
+ "type == 'XCUIElementTypeButton' AND (name == '$text' OR label == '$text')" \
1782
+ "name == '$text' OR label == '$text'"; do
1783
+
1784
+ local pred_elements=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
1785
+ -H 'Content-Type: application/json' \
1786
+ -d '{"using":"predicate string","value":"'"$pred_query"'"}' 2>/dev/null)
1787
+ element_id=$(echo "$pred_elements" | python3 -c "
1788
+ import sys, json
1789
+ try:
1790
+ data = json.load(sys.stdin)
1791
+ elements = data.get('value', [])
1792
+ if elements and 'ELEMENT' in elements[0]:
1793
+ print(elements[0]['ELEMENT'])
1794
+ except: pass
1795
+ " 2>/dev/null)
1796
+ if [ -n "$element_id" ]; then
1797
+ echo "${log_prefix} ✅ Found via predicate (system UI)"
1798
+ break
1799
+ fi
1800
+ done
1801
+ fi
1802
+
1803
+ # Method 4: Try class chain with partial matching
1804
+ if [ -z "$element_id" ]; then
1805
+ echo "${log_prefix} 🔍 Searching with class chain..."
1806
+ for chain_pattern in \
1807
+ "**/XCUIElementTypeButton[\`name CONTAINS '$text' OR label CONTAINS '$text'\`]" \
1808
+ "**/XCUIElementTypeButton[\`name == '$text' OR label == '$text'\`]" \
1809
+ "**/XCUIElementTypeButton[\`name BEGINSWITH '$text' OR label BEGINSWITH '$text'\`]" \
1810
+ "**/*[\`name CONTAINS '$text' OR label CONTAINS '$text'\`]"; do
1811
+
1812
+ local chain_elements=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
1813
+ -H 'Content-Type: application/json' \
1814
+ -d '{"using":"class chain","value":"'"$chain_pattern"'"}' 2>/dev/null)
1815
+ element_id=$(echo "$chain_elements" | python3 -c "
1816
+ import sys, json
1817
+ try:
1818
+ data = json.load(sys.stdin)
1819
+ elements = data.get('value', [])
1820
+ if elements and 'ELEMENT' in elements[0]:
1821
+ print(elements[0]['ELEMENT'])
1822
+ except: pass
1823
+ " 2>/dev/null)
1824
+ if [ -n "$element_id" ]; then
1825
+ echo "${log_prefix} ✅ Found via class chain (pattern: $chain_pattern)"
1826
+ break
1827
+ fi
1828
+ done
1829
+ fi
1830
+
1831
+ # Method 5: Try getting full page source and searching with partial text
1832
+ if [ -z "$element_id" ]; then
1833
+ echo "${log_prefix} 🔍 Searching in full page source..."
1834
+ local page_source=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/source" 2>/dev/null)
1835
+
1836
+ # Check if element exists in page source (case insensitive partial match)
1837
+ if echo "$page_source" | grep -iq "$text"; then
1838
+ echo "${log_prefix} 📄 Element found in page source (partial match), trying XPath..."
1839
+
1840
+ # Try multiple XPath strategies with partial text matching
1841
+ for xpath in \
1842
+ "//XCUIElementTypeButton[@name='$text']" \
1843
+ "//XCUIElementTypeButton[@label='$text']" \
1844
+ "//XCUIElementTypeButton[contains(@name,'$text')]" \
1845
+ "//XCUIElementTypeButton[contains(@label,'$text')]" \
1846
+ "//XCUIElementTypeStaticText[@name='$text']/parent::XCUIElementTypeButton" \
1847
+ "//XCUIElementTypeStaticText[contains(@name,'$text')]/parent::XCUIElementTypeButton" \
1848
+ "//*[@name='$text']" \
1849
+ "//*[@label='$text']" \
1850
+ "//*[contains(@name,'$text')]" \
1851
+ "//*[contains(@label,'$text')]"; do
1852
+
1853
+ local xpath_elements=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
1854
+ -H 'Content-Type: application/json' \
1855
+ -d '{"using":"xpath","value":"'"$xpath"'"}' 2>/dev/null)
1856
+ element_id=$(echo "$xpath_elements" | python3 -c "
1857
+ import sys, json
1858
+ try:
1859
+ data = json.load(sys.stdin)
1860
+ elements = data.get('value', [])
1861
+ if elements and 'ELEMENT' in elements[0]:
1862
+ print(elements[0]['ELEMENT'])
1863
+ except: pass
1864
+ " 2>/dev/null)
1865
+ if [ -n "$element_id" ]; then
1866
+ echo "${log_prefix} ✅ Found via XPath: $xpath"
1867
+ break
1868
+ fi
1869
+ done
1870
+ else
1871
+ echo "${log_prefix} 📄 Text '$text' not found in page source - element may not be visible"
1872
+ fi
1873
+ fi
1874
+
1875
+ if [ -n "$element_id" ]; then
1876
+ echo "${log_prefix} ✅ Found element via accessibility"
1877
+ # Get element rect for coordinates
1878
+ local rect_response=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/element/$element_id/rect" 2>/dev/null)
1879
+ local elem_x=$(echo "$rect_response" | python3 -c "
1880
+ import sys, json
1881
+ try:
1882
+ data = json.load(sys.stdin)
1883
+ value = data.get('value', {})
1884
+ print(int(value.get('x', 0) + value.get('width', 0) / 2))
1885
+ except: print(0)
1886
+ " 2>/dev/null)
1887
+ local elem_y=$(echo "$rect_response" | python3 -c "
1888
+ import sys, json
1889
+ try:
1890
+ data = json.load(sys.stdin)
1891
+ value = data.get('value', {})
1892
+ print(int(value.get('y', 0) + value.get('height', 0) / 2))
1893
+ except: print(0)
1894
+ " 2>/dev/null)
1895
+
1896
+ if [ "$elem_x" -gt 0 ] && [ "$elem_y" -gt 0 ]; then
1897
+ echo "${log_prefix} 🎯 Element center: ($elem_x,$elem_y)"
1898
+ local elem_actions='{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":'$elem_x',"y":'$elem_y'},{"type":"pointerDown","button":0},{"type":"pause","duration":150},{"type":"pointerUp","button":0}]}]}'
1899
+ local elem_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
1900
+ -H 'Content-Type: application/json' -d "$elem_actions" 2>/dev/null)
1901
+
1902
+ if echo "$elem_response" | grep -q '"sessionId"' && ! echo "$elem_response" | grep -q '"error"'; then
1903
+ echo "${log_prefix} ✅ Clicked '$text' at ($elem_x,$elem_y)"
1904
+ else
1905
+ echo "${log_prefix} ❌ Click failed"
1906
+ fi
1907
+ else
1908
+ # Fallback: Direct element click
1909
+ local click_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$element_id/click" \
1910
+ -H 'Content-Type: application/json' -d '{}' 2>/dev/null)
1911
+ if echo "$click_response" | grep -q '"sessionId"' && ! echo "$click_response" | grep -q '"error"'; then
1912
+ echo "${log_prefix} ✅ Clicked '$text' via element click"
1913
+ else
1914
+ echo "${log_prefix} ❌ Click failed"
1915
+ fi
1916
+ fi
1917
+ else
1918
+ echo "${log_prefix} ❌ Element '$text' not found"
1919
+ fi
1920
+ else
1921
+ echo "${log_prefix} ❌ No session available"
1922
+ fi
1923
+ fi
1924
+ ;;
1925
+
1926
+ "refresh"|"refresh_screen"|"reload_cache")
1927
+ echo "${log_prefix} 🔄 REFRESHING SCREEN CACHE..."
1928
+ refresh_screen_cache "$device_name"
1929
+ echo "${log_prefix} ✅ Screen cache refreshed. Next click will re-analyze screen."
1930
+ ;;
1931
+
1932
+ "longpress"|"longclick")
1933
+ local coords="$cmd_args"
1934
+ if [[ "$coords" =~ ^[0-9]+,[0-9]+$ ]]; then
1935
+ local x=$(echo "$coords" | cut -d',' -f1)
1936
+ local y=$(echo "$coords" | cut -d',' -f2)
1937
+ echo "${log_prefix} 🎯 Precise long press at ($x, $y)"
1938
+ local session=$(get_device_session "$device_name")
1939
+ if [ -n "$session" ]; then
1940
+ local actions_payload='{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":'$x',"y":'$y'},{"type":"pointerDown","button":0},{"type":"pause","duration":800},{"type":"pointerUp","button":0}]}]}'
1941
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" -H 'Content-Type: application/json' -d "$actions_payload" 2>/dev/null)
1942
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
1943
+ echo "${log_prefix} ✅ Long press successful at ($x, $y)"
1944
+ else
1945
+ echo "${log_prefix} ❌ Long press failed"
1946
+ echo "${log_prefix} Response: $response"
1947
+ fi
1948
+ else
1949
+ echo "${log_prefix} ❌ No session available"
1950
+ fi
1951
+ else
1952
+ # Search for element by text - GENIUS SCALABLE APPROACH!
1953
+ local text="$coords"
1954
+ echo "${log_prefix} Searching for element: '$text'"
1955
+
1956
+ local session=$(get_device_session "$device_name")
1957
+ if [ -n "$session" ]; then
1958
+ # Step 1: Try dynamic element detection first (like long press does)
1959
+ echo "${log_prefix} 🔍 Attempting dynamic element detection..."
1960
+ local search_result=$(find_element_by_text "$session" "$host" "$text" "$device_name")
1961
+ local element_id=$(echo "$search_result" | cut -d'|' -f1)
1962
+ local found_strategy=$(echo "$search_result" | cut -d'|' -f2)
1963
+
1964
+ if [ -n "$element_id" ]; then
1965
+ echo "${log_prefix} ✅ Found element dynamically using: $found_strategy"
1966
+ echo "${log_prefix} Element ID: $element_id"
1967
+
1968
+ # Get element center coordinates
1969
+ local coordinates=$(get_element_center "$session" "$host" "$element_id")
1970
+ local x=$(echo "$coordinates" | cut -d',' -f1)
1971
+ local y=$(echo "$coordinates" | cut -d',' -f2)
1972
+
1973
+ if [[ "$x" =~ ^[0-9]+$ ]] && [[ "$y" =~ ^[0-9]+$ ]] && [ "$x" -gt 0 ] && [ "$y" -gt 0 ]; then
1974
+ echo "${log_prefix} 🎯 Dynamic element center: ($x, $y)"
1975
+
1976
+ # Use W3C Actions API for precise click
1977
+ echo "${log_prefix} Trying Method 1: W3C Actions API (dynamic)..."
1978
+ local actions_payload='{
1979
+ "actions": [
1980
+ {
1981
+ "type": "pointer",
1982
+ "id": "finger1",
1983
+ "parameters": {"pointerType": "touch"},
1984
+ "actions": [
1985
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
1986
+ {"type": "pointerDown", "button": 0},
1987
+ {"type": "pause", "duration": 100},
1988
+ {"type": "pointerUp", "button": 0}
1989
+ ]
1990
+ }
1991
+ ]
1992
+ }'
1993
+
1994
+ local tap_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
1995
+ -H 'Content-Type: application/json' \
1996
+ -d "$actions_payload" 2>/dev/null)
1997
+
1998
+
1999
+ if echo "$tap_response" | grep -q '"sessionId"' && ! echo "$tap_response" | grep -q '"error"'; then
2000
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - Dynamic Detection"
2001
+ echo "${log_prefix} Completed"
2002
+ return
2003
+ fi
2004
+ fi
2005
+ fi
2006
+
2007
+ # Step 2: Fallback to device-specific coordinate mapping
2008
+ echo "${log_prefix} 🔄 Falling back to coordinate mapping..."
2009
+ local text_lower=$(echo "$text" | tr '[:upper:]' '[:lower:]')
2010
+ local x y found=false
2011
+
2012
+ # Get device screen dimensions for coordinate adaptation
2013
+ local screen_width screen_height
2014
+ case "$device_name" in
2015
+ "iphone16p")
2016
+ screen_width=430; screen_height=932
2017
+ ;;
2018
+ "iphone17")
2019
+ screen_width=402; screen_height=874
2020
+ ;;
2021
+ "ipad")
2022
+ screen_width=834; screen_height=1210
2023
+ ;;
2024
+ *)
2025
+ screen_width=400; screen_height=800 # fallback
2026
+ ;;
2027
+ esac
2028
+
2029
+ # Common app icons (home screen)
2030
+ case "$text_lower" in
2031
+ "settings")
2032
+ x=200; y=300; found=true
2033
+ ;;
2034
+ "safari")
2035
+ x=75; y=300; found=true
2036
+ ;;
2037
+ "camera")
2038
+ x=325; y=300; found=true
2039
+ ;;
2040
+ "photos")
2041
+ x=75; y=425; found=true
2042
+ ;;
2043
+ "messages")
2044
+ x=200; y=425; found=true
2045
+ ;;
2046
+ "mail")
2047
+ x=325; y=425; found=true
2048
+ ;;
2049
+ "phone")
2050
+ x=75; y=550; found=true
2051
+ ;;
2052
+ "facetime")
2053
+ x=200; y=550; found=true
2054
+ ;;
2055
+ # Settings app elements
2056
+ "general")
2057
+ x=200; y=200; found=true
2058
+ ;;
2059
+ "wi-fi"|"wifi")
2060
+ x=200; y=150; found=true
2061
+ ;;
2062
+ "bluetooth")
2063
+ x=200; y=200; found=true
2064
+ ;;
2065
+ "cellular")
2066
+ x=200; y=250; found=true
2067
+ ;;
2068
+ "personal hotspot"|"hotspot")
2069
+ x=200; y=300; found=true
2070
+ ;;
2071
+ "privacy & security"|"privacy")
2072
+ x=200; y=400; found=true
2073
+ ;;
2074
+ # Navigation elements - COMPREHENSIVE BACK SYSTEM (device-aware)
2075
+ "settings back"|"settings <"|"< settings")
2076
+ # Back button in Settings app
2077
+ x=$(echo "$screen_width * 0.0875" | bc); y=$(echo "$screen_height * 0.117" | bc); found=true
2078
+ ;;
2079
+ "safari back"|"safari <"|"< safari")
2080
+ # Back button in Safari
2081
+ x=$(echo "$screen_width * 0.11" | bc); y=$(echo "$screen_height * 0.117" | bc); found=true
2082
+ ;;
2083
+ "app back"|"navigation back")
2084
+ # Generic app back button
2085
+ x=$(echo "$screen_width * 0.08" | bc); y=$(echo "$screen_height * 0.1225" | bc); found=true
2086
+ ;;
2087
+ "cancel"|"cancel button")
2088
+ # Top-left area
2089
+ x=$(echo "$screen_width * 0.125" | bc); y=$(echo "$screen_width * 0.125" | bc); found=true
2090
+ ;;
2091
+ "next"|"next button")
2092
+ # Top-right
2093
+ x=$(echo "$screen_width * 0.875" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2094
+ ;;
2095
+ "edit"|"edit button")
2096
+ # Top-right
2097
+ x=$(echo "$screen_width * 0.875" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2098
+ ;;
2099
+ "save"|"save button")
2100
+ x=350; y=100; found=true
2101
+ ;;
2102
+ # Tab Bar Navigation (bottom of screen)
2103
+ "tab back"|"previous tab")
2104
+ x=100; y=680; found=true
2105
+ ;;
2106
+ "home tab"|"house"|"🏠")
2107
+ x=200; y=680; found=true
2108
+ ;;
2109
+ "search tab"|"🔍"|"magnifying glass")
2110
+ x=300; y=680; found=true
2111
+ ;;
2112
+ # Browser Navigation
2113
+ "browser back"|"web back")
2114
+ x=44; y=650; found=true
2115
+ ;;
2116
+ "browser forward"|"web forward")
2117
+ x=100; y=650; found=true
2118
+ ;;
2119
+ "refresh"|"reload"|"↻")
2120
+ x=200; y=650; found=true
2121
+ ;;
2122
+ # Control Center elements
2123
+ "wifi toggle"|"wifi button")
2124
+ x=100; y=200; found=true
2125
+ ;;
2126
+ "bluetooth toggle"|"bluetooth button")
2127
+ x=200; y=200; found=true
2128
+ ;;
2129
+ "airplane mode")
2130
+ x=50; y=200; found=true
2131
+ ;;
2132
+ # Common UI Elements - Device-aware coordinates
2133
+ "learn more"|"learn more >")
2134
+ # Typically at bottom half of screen
2135
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.5" | bc); found=true
2136
+ ;;
2137
+ "continue"|"continue button")
2138
+ # Typically at bottom of screen (75% down)
2139
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.75" | bc); found=true
2140
+ ;;
2141
+ "get started"|"start"|"begin")
2142
+ # Usually in lower half
2143
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.625" | bc); found=true
2144
+ ;;
2145
+ "sign in"|"log in"|"login")
2146
+ # Mid-lower screen
2147
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.56" | bc); found=true
2148
+ ;;
2149
+ "sign up"|"register"|"create account")
2150
+ # Lower screen
2151
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.625" | bc); found=true
2152
+ ;;
2153
+ "skip"|"skip for now"|"maybe later")
2154
+ # Top area, left-ish
2155
+ x=$(echo "$screen_width * 0.2" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2156
+ ;;
2157
+ "allow"|"allow access"|"ok"|"okay")
2158
+ # Dialog right side
2159
+ x=$(echo "$screen_width * 0.75" | bc); y=$(echo "$screen_height * 0.5" | bc); found=true
2160
+ ;;
2161
+ "don't allow"|"deny"|"not now")
2162
+ # Dialog left side
2163
+ x=$(echo "$screen_width * 0.25" | bc); y=$(echo "$screen_height * 0.5" | bc); found=true
2164
+ ;;
2165
+ "agree"|"accept"|"i agree")
2166
+ # Bottom area
2167
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.625" | bc); found=true
2168
+ ;;
2169
+ "try again"|"retry")
2170
+ # Mid screen
2171
+ x=$(echo "$screen_width / 2" | bc); y=$(echo "$screen_height * 0.56" | bc); found=true
2172
+ ;;
2173
+ # Navigation elements with device-aware coordinates
2174
+ "back"|"< back"|"back button"|"←"|"<")
2175
+ # Top-left corner
2176
+ x=$(echo "$screen_width * 0.075" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2177
+ ;;
2178
+ "close"|"×"|"x"|"close button")
2179
+ # Top-right corner
2180
+ x=$(echo "$screen_width * 0.875" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2181
+ ;;
2182
+ "done"|"done button")
2183
+ # Top-right
2184
+ x=$(echo "$screen_width * 0.875" | bc); y=$(echo "$screen_height * 0.125" | bc); found=true
2185
+ ;;
2186
+ "share"|"share button")
2187
+ x=350; y=100; found=true
2188
+ ;;
2189
+ "add"|"+ add"|"add item")
2190
+ x=350; y=100; found=true
2191
+ ;;
2192
+ "delete"|"remove"|"trash")
2193
+ x=350; y=100; found=true
2194
+ ;;
2195
+ "search"|"🔍 search")
2196
+ x=200; y=150; found=true
2197
+ ;;
2198
+ "more"|"more options"|"⋯"|"...")
2199
+ x=350; y=100; found=true
2200
+ ;;
2201
+ "info"|"ⓘ"|"information")
2202
+ x=350; y=100; found=true
2203
+ ;;
2204
+ *)
2205
+ found=false
2206
+ ;;
2207
+ esac
2208
+
2209
+ if [ "$found" = "true" ]; then
2210
+ echo "${log_prefix} ✅ Using known coordinates for '$text': ($x, $y)"
2211
+
2212
+ # Use CORRECT WebDriverAgent endpoint - try multiple methods
2213
+ echo "${log_prefix} Trying Method 1: W3C Actions API..."
2214
+ local actions_payload='{
2215
+ "actions": [
2216
+ {
2217
+ "type": "pointer",
2218
+ "id": "finger1",
2219
+ "parameters": {"pointerType": "touch"},
2220
+ "actions": [
2221
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
2222
+ {"type": "pointerDown", "button": 0},
2223
+ {"type": "pause", "duration": 100},
2224
+ {"type": "pointerUp", "button": 0}
2225
+ ]
2226
+ }
2227
+ ]
2228
+ }'
2229
+
2230
+ local tap_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
2231
+ -H 'Content-Type: application/json' \
2232
+ -d "$actions_payload" 2>/dev/null)
2233
+
2234
+
2235
+ if echo "$tap_response" | grep -q '"sessionId"' && ! echo "$tap_response" | grep -q '"error"'; then
2236
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - W3C Actions"
2237
+ else
2238
+ # Method 2: Try WebDriverAgent touch endpoint
2239
+ echo "${log_prefix} Trying Method 2: WDA touch endpoint..."
2240
+ local touch_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/touch/perform" \
2241
+ -H 'Content-Type: application/json' \
2242
+ -d '[{"action": "press", "options": {"x": '$x', "y": '$y'}}, {"action": "wait", "options": {"ms": 100}}, {"action": "release"}]' 2>/dev/null)
2243
+
2244
+
2245
+ if echo "$touch_response" | grep -q '"sessionId"' && ! echo "$touch_response" | grep -q '"error"'; then
2246
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - WDA Touch"
2247
+ else
2248
+ # Method 3: Try dragFromToForDuration with same point (acts as tap)
2249
+ echo "${log_prefix} Trying Method 3: Drag endpoint as tap..."
2250
+ local drag_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/dragfromtoforduration" \
2251
+ -H 'Content-Type: application/json' \
2252
+ -d '{"fromX": '$x', "fromY": '$y', "toX": '$x', "toY": '$y', "duration": 0.1}' 2>/dev/null)
2253
+
2254
+
2255
+ if echo "$drag_response" | grep -q '"sessionId"' && ! echo "$drag_response" | grep -q '"error"'; then
2256
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - Drag Method"
2257
+ else
2258
+ # Method 4: Try element-based approach with known coordinates
2259
+ echo "${log_prefix} Trying Method 4: Element at coordinates..."
2260
+ local element_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/element" \
2261
+ -H 'Content-Type: application/json' \
2262
+ -d '{"using": "xpath", "value": "//XCUIElementTypeApplication"}' 2>/dev/null)
2263
+
2264
+ if echo "$element_response" | grep -q '"ELEMENT"'; then
2265
+ local element_id=$(echo "$element_response" | python3 -c "
2266
+ import sys, json
2267
+ try:
2268
+ data = json.load(sys.stdin)
2269
+ print(data.get('value', {}).get('ELEMENT', ''))
2270
+ except: pass" 2>/dev/null)
2271
+
2272
+ if [ -n "$element_id" ]; then
2273
+ local click_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$element_id/click" \
2274
+ -H 'Content-Type: application/json' \
2275
+ -d "{\"x\": $x, \"y\": $y}" 2>/dev/null)
2276
+
2277
+
2278
+ if echo "$click_response" | grep -q '"sessionId"' && ! echo "$click_response" | grep -q '"error"'; then
2279
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($x, $y) - Element Click"
2280
+ else
2281
+ echo "${log_prefix} ❌ All click methods failed for '$text' at ($x, $y)"
2282
+ echo "${log_prefix} 💡 WebDriverAgent version may not support standard tap endpoints"
2283
+ echo "${log_prefix} 🔍 Debug info - WDA Response: $tap_response"
2284
+ fi
2285
+ fi
2286
+ else
2287
+ echo "${log_prefix} ❌ All click methods failed - no supported endpoints found"
2288
+ echo "${log_prefix} 💡 Check WebDriverAgent version and capabilities"
2289
+ fi
2290
+ fi
2291
+ fi
2292
+ fi
2293
+ else
2294
+ # Advanced fallback: Try element detection with multiple strategies
2295
+ echo "${log_prefix} Unknown element '$text' - using advanced detection..."
2296
+
2297
+ # Strategy 1: Try direct accessibility ID (most common)
2298
+ local element_found=false
2299
+ local element_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
2300
+ -H "Content-Type: application/json" \
2301
+ -d "{\"using\": \"accessibility id\", \"value\": \"$text\"}" 2>/dev/null)
2302
+
2303
+ if echo "$element_response" | grep -q '"ELEMENT"'; then
2304
+ echo "${log_prefix} ✅ Found via accessibility ID"
2305
+ # Extract element ID and try multiple interaction methods
2306
+ local element_id=$(echo "$element_response" | python3 -c "
2307
+ import sys, json
2308
+ try:
2309
+ data = json.load(sys.stdin)
2310
+ elements = data.get('value', [])
2311
+ if elements: print(elements[0].get('ELEMENT', ''))
2312
+ except: pass" 2>/dev/null)
2313
+
2314
+ if [ -n "$element_id" ]; then
2315
+ echo "${log_prefix} Element ID: $element_id"
2316
+
2317
+ # Method 1: Try direct element click
2318
+ echo "${log_prefix} Trying element click method..."
2319
+ local click_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$element_id/click" \
2320
+ -H 'Content-Type: application/json' \
2321
+ -d '{}' 2>/dev/null)
2322
+
2323
+ if echo "$click_response" | grep -q '"sessionId"' && ! echo "$click_response" | grep -q '"error"'; then
2324
+ echo "${log_prefix} ✅ Successfully clicked '$text' (Element click method)"
2325
+ element_found=true
2326
+ else
2327
+ # Method 2: Get element coordinates and use W3C Actions
2328
+ echo "${log_prefix} Trying coordinate extraction..."
2329
+ local rect_response=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/element/$element_id/rect" \
2330
+ -H 'Content-Type: application/json' 2>/dev/null)
2331
+
2332
+ local center_x=$(echo "$rect_response" | python3 -c "
2333
+ import sys, json
2334
+ try:
2335
+ data = json.load(sys.stdin)
2336
+ rect = data.get('value', {})
2337
+ x = rect.get('x', 0)
2338
+ width = rect.get('width', 0)
2339
+ print(int(x + width/2))
2340
+ except: print('0')" 2>/dev/null)
2341
+
2342
+ local center_y=$(echo "$rect_response" | python3 -c "
2343
+ import sys, json
2344
+ try:
2345
+ data = json.load(sys.stdin)
2346
+ rect = data.get('value', {})
2347
+ y = rect.get('y', 0)
2348
+ height = rect.get('height', 0)
2349
+ print(int(y + height/2))
2350
+ except: print('0')" 2>/dev/null)
2351
+
2352
+ if [[ "$center_x" =~ ^[0-9]+$ ]] && [[ "$center_y" =~ ^[0-9]+$ ]] && [ "$center_x" -gt 0 ] && [ "$center_y" -gt 0 ]; then
2353
+ echo "${log_prefix} Element center: ($center_x, $center_y)"
2354
+
2355
+ # Use W3C Actions with extracted coordinates
2356
+ local actions_payload='{
2357
+ "actions": [
2358
+ {
2359
+ "type": "pointer",
2360
+ "id": "finger1",
2361
+ "parameters": {"pointerType": "touch"},
2362
+ "actions": [
2363
+ {"type": "pointerMove", "duration": 0, "x": '$center_x', "y": '$center_y'},
2364
+ {"type": "pointerDown", "button": 0},
2365
+ {"type": "pause", "duration": 100},
2366
+ {"type": "pointerUp", "button": 0}
2367
+ ]
2368
+ }
2369
+ ]
2370
+ }'
2371
+
2372
+ local tap_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
2373
+ -H 'Content-Type: application/json' \
2374
+ -d "$actions_payload" 2>/dev/null)
2375
+
2376
+ if echo "$tap_response" | grep -q '"sessionId"' && ! echo "$tap_response" | grep -q '"error"'; then
2377
+ echo "${log_prefix} ✅ Successfully clicked '$text' at ($center_x, $center_y) (Coordinate method)"
2378
+ element_found=true
2379
+ else
2380
+ # Method 3: Fallback to Enter key (for text fields)
2381
+ echo "${log_prefix} Trying Enter key fallback..."
2382
+ local key_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$element_id/value" \
2383
+ -H 'Content-Type: application/json' \
2384
+ -d '{"value":["\n"]}' 2>/dev/null)
2385
+
2386
+ if echo "$key_response" | grep -q '"sessionId"' && ! echo "$key_response" | grep -q '"error"'; then
2387
+ echo "${log_prefix} ✅ Successfully activated '$text' (Enter key method)"
2388
+ element_found=true
2389
+ fi
2390
+ fi
2391
+ else
2392
+ echo "${log_prefix} ❌ Could not extract valid coordinates from element"
2393
+ fi
2394
+ fi
2395
+ else
2396
+ echo "${log_prefix} ❌ Could not extract element ID"
2397
+ fi
2398
+ fi
2399
+
2400
+ # Strategy 2: Try partial name matching if not found
2401
+ if [ "$element_found" = "false" ]; then
2402
+ local name_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
2403
+ -H "Content-Type: application/json" \
2404
+ -d "{\"using\": \"name\", \"value\": \"$text\"}" 2>/dev/null)
2405
+
2406
+ if echo "$name_response" | grep -q '"ELEMENT"'; then
2407
+ echo "${log_prefix} ✅ Found via name attribute"
2408
+ local element_id=$(echo "$name_response" | python3 -c "
2409
+ import sys, json
2410
+ try:
2411
+ data = json.load(sys.stdin)
2412
+ elements = data.get('value', [])
2413
+ if elements: print(elements[0].get('ELEMENT', ''))
2414
+ except: pass" 2>/dev/null)
2415
+
2416
+ if [ -n "$element_id" ]; then
2417
+ local key_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$element_id/value" \
2418
+ -H 'Content-Type: application/json' \
2419
+ -d '{"value":["\n"]}' 2>/dev/null)
2420
+
2421
+ if echo "$key_response" | grep -q '"sessionId"' && ! echo "$key_response" | grep -q '"error"'; then
2422
+ echo "${log_prefix} ✅ Successfully activated '$text' (Enter key method)"
2423
+ element_found=true
2424
+ fi
2425
+ fi
2426
+ fi
2427
+ fi
2428
+
2429
+ # Strategy 3: Suggest coordinate-based approach if all fails
2430
+ if [ "$element_found" = "false" ]; then
2431
+ echo "${log_prefix} ❌ Element '$text' not found with any method"
2432
+ echo "${log_prefix} 💡 Try using coordinates: click 200,300"
2433
+ echo "${log_prefix} 💡 Or add '$text' to the coordinate mapping system"
2434
+ fi
2435
+ fi
2436
+ else
2437
+ echo "${log_prefix} ❌ No session available"
2438
+ fi
2439
+ fi
2440
+ ;;
2441
+
2442
+ "longpress"|"longclick")
2443
+ echo "${log_prefix} CASE: Longpress matched!"
2444
+ local coords="$cmd_args"
2445
+ local duration="1.5" # Default 1.5 seconds
2446
+
2447
+ # Check if duration is specified (e.g., "100,200,2.0")
2448
+ if [[ "$coords" =~ ^[0-9]+,[0-9]+,[0-9.]+$ ]]; then
2449
+ duration=$(echo "$coords" | cut -d',' -f3)
2450
+ coords=$(echo "$coords" | cut -d',' -f1,2)
2451
+ fi
2452
+
2453
+ if [[ "$coords" =~ ^[0-9]+,[0-9]+$ ]]; then
2454
+ # Direct coordinates - use W3C Actions API for more reliable long press
2455
+ local x=$(echo "$coords" | cut -d',' -f1)
2456
+ local y=$(echo "$coords" | cut -d',' -f2)
2457
+ echo "${log_prefix} Long pressing at coordinates: ($x, $y) for ${duration}s"
2458
+
2459
+ local session=$(get_device_session "$device_name")
2460
+ if [ -n "$session" ]; then
2461
+ # Use W3C Actions API for precise long press
2462
+ local duration_ms=$(python3 -c "print(int(float('$duration') * 1000))" 2>/dev/null || echo "1500")
2463
+ local actions_payload='{
2464
+ "actions": [
2465
+ {
2466
+ "type": "pointer",
2467
+ "id": "finger1",
2468
+ "parameters": {"pointerType": "touch"},
2469
+ "actions": [
2470
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
2471
+ {"type": "pointerDown", "button": 0},
2472
+ {"type": "pause", "duration": '$duration_ms'},
2473
+ {"type": "pointerUp", "button": 0}
2474
+ ]
2475
+ }
2476
+ ]
2477
+ }'
2478
+
2479
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
2480
+ -H 'Content-Type: application/json' \
2481
+ -d "$actions_payload")
2482
+
2483
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
2484
+ echo "${log_prefix} ✅ Long pressed at ($x, $y) for ${duration}s"
2485
+ else
2486
+ # Fallback to touchAndHold endpoint
2487
+ echo "${log_prefix} Trying fallback touchAndHold..."
2488
+ response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/touchAndHold" \
2489
+ -H 'Content-Type: application/json' \
2490
+ -d "{\"x\":$x,\"y\":$y,\"duration\":$duration}")
2491
+
2492
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
2493
+ echo "${log_prefix} ✅ Long pressed at ($x, $y)"
2494
+ else
2495
+ echo "${log_prefix} ❌ Long press failed"
2496
+ echo "${log_prefix} Response: $response"
2497
+ fi
2498
+ fi
2499
+ else
2500
+ echo "${log_prefix} ❌ No session available"
2501
+ fi
2502
+ else
2503
+ # Search for element by text - PROFESSIONAL APPIUM WAY
2504
+ local text="$coords"
2505
+ echo "${log_prefix} Searching for element to long press: '$text'"
2506
+
2507
+ local session=$(get_device_session "$device_name")
2508
+ if [ -n "$session" ]; then
2509
+ # Use helper function to find element
2510
+ local search_result=$(find_element_by_text "$session" "$host" "$text" "$device_name")
2511
+ local element_id=$(echo "$search_result" | cut -d'|' -f1)
2512
+ local found_strategy=$(echo "$search_result" | cut -d'|' -f2)
2513
+
2514
+ if [ -n "$element_id" ]; then
2515
+ echo "${log_prefix} ✅ Found element using: $found_strategy"
2516
+
2517
+ # Get element center coordinates
2518
+ local coordinates=$(get_element_center "$session" "$host" "$element_id")
2519
+ local x=$(echo "$coordinates" | cut -d',' -f1)
2520
+ local y=$(echo "$coordinates" | cut -d',' -f2)
2521
+
2522
+ if [[ "$x" =~ ^[0-9]+$ ]] && [[ "$y" =~ ^[0-9]+$ ]] && [ "$x" -gt 0 ] && [ "$y" -gt 0 ]; then
2523
+ echo "${log_prefix} Element center: ($x, $y)"
2524
+
2525
+ # Use W3C Actions API for precise long press
2526
+ local duration_ms=$(python3 -c "print(int(float('$duration') * 1000))" 2>/dev/null || echo "1500")
2527
+ local actions_payload='{
2528
+ "actions": [
2529
+ {
2530
+ "type": "pointer",
2531
+ "id": "finger1",
2532
+ "parameters": {"pointerType": "touch"},
2533
+ "actions": [
2534
+ {"type": "pointerMove", "duration": 0, "x": '$x', "y": '$y'},
2535
+ {"type": "pointerDown", "button": 0},
2536
+ {"type": "pause", "duration": '$duration_ms'},
2537
+ {"type": "pointerUp", "button": 0}
2538
+ ]
2539
+ }
2540
+ ]
2541
+ }'
2542
+
2543
+ local longpress_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
2544
+ -H 'Content-Type: application/json' \
2545
+ -d "$actions_payload")
2546
+
2547
+ if echo "$longpress_response" | grep -q '"sessionId"' && ! echo "$longpress_response" | grep -q '"error"'; then
2548
+ echo "${log_prefix} ✅ Successfully long pressed '$text' for ${duration}s"
2549
+ else
2550
+ echo "${log_prefix} ❌ Long press failed on '$text'"
2551
+ echo "${log_prefix} Response: $longpress_response"
2552
+ fi
2553
+ else
2554
+ echo "${log_prefix} ❌ Could not get valid element coordinates"
2555
+ fi
2556
+ else
2557
+ echo "${log_prefix} ❌ Element '$text' not found with any locator strategy"
2558
+ fi
2559
+ else
2560
+ echo "${log_prefix} ❌ No session available"
2561
+ fi
2562
+ fi
2563
+ ;;
2564
+
2565
+ "swipe"|"swipe_up"|"swipe_down"|"swipe_left"|"swipe_right"|"scroll")
2566
+ echo "${log_prefix} CASE: Swipe gesture matched!"
2567
+ local swipe_params="$cmd_args"
2568
+ local direction=""
2569
+ local start_x="" start_y="" end_x="" end_y=""
2570
+ local duration="300" # Default 300ms
2571
+
2572
+ # Get device screen dimensions for optimal coordinates
2573
+ local screen_width screen_height
2574
+
2575
+ # Device-specific coordinate profiles (fallback when session detection fails)
2576
+ case "$device_name" in
2577
+ "iphone16p")
2578
+ screen_width=430; screen_height=932 # iPhone 16 Pro actual resolution
2579
+ echo "${log_prefix} 📱 Using iPhone 16 Pro profile: ${screen_width}×${screen_height}"
2580
+ ;;
2581
+ "iphone17")
2582
+ screen_width=402; screen_height=874 # iPhone 17 actual resolution
2583
+ echo "${log_prefix} 📱 Using iPhone 17 profile: ${screen_width}×${screen_height}"
2584
+ ;;
2585
+ "ipad")
2586
+ screen_width=834; screen_height=1210 # iPad actual resolution
2587
+ echo "${log_prefix} 📱 Using iPad profile: ${screen_width}×${screen_height}"
2588
+ ;;
2589
+ *)
2590
+ # Try to get from session first, then fallback
2591
+ if [ -n "$session" ]; then
2592
+ local screen_info=$(curl -s "http://$host:$WDA_PORT/session/$session/window/size" 2>/dev/null)
2593
+ if echo "$screen_info" | grep -q '"width"' 2>/dev/null; then
2594
+ screen_width=$(echo "$screen_info" | python3 -c "import sys,json; data=json.load(sys.stdin); print(data['value']['width']) if 'value' in data else print('400')" 2>/dev/null || echo "400")
2595
+ screen_height=$(echo "$screen_info" | python3 -c "import sys,json; data=json.load(sys.stdin); print(data['value']['height']) if 'value' in data else print('800')" 2>/dev/null || echo "800")
2596
+ echo "${log_prefix} 📱 Detected via session: ${screen_width}×${screen_height}"
2597
+ fi
2598
+ fi
2599
+ ;;
2600
+ esac
2601
+
2602
+ # Default fallback dimensions
2603
+ screen_width=${screen_width:-400}
2604
+ screen_height=${screen_height:-800}
2605
+
2606
+ # Parse swipe parameters
2607
+ if [[ "$swipe_params" =~ ^(up|down|left|right)$ ]]; then
2608
+ # Device-optimized directional swipes based on actual screen dimensions
2609
+ direction="$swipe_params"
2610
+ local center_x=$((screen_width / 2))
2611
+ local quarter_height=$((screen_height / 4))
2612
+ local three_quarter_height=$((screen_height * 3 / 4))
2613
+ local left_edge=$((screen_width / 8))
2614
+ local right_edge=$((screen_width * 7 / 8))
2615
+ local mid_height=$((screen_height / 2))
2616
+
2617
+ case "$direction" in
2618
+ "up")
2619
+ # Scroll up - start from 75% height, swipe to 25% height
2620
+ start_x=$center_x; start_y=$three_quarter_height; end_x=$center_x; end_y=$quarter_height
2621
+ duration="600" # Slower for better recognition
2622
+ echo "${log_prefix} 👆 Optimized swipe up for ${screen_width}×${screen_height}"
2623
+ ;;
2624
+ "down")
2625
+ # Scroll down - start from 25% height, swipe to 75% height
2626
+ start_x=$center_x; start_y=$quarter_height; end_x=$center_x; end_y=$three_quarter_height
2627
+ duration="600" # Slower for better recognition
2628
+ echo "${log_prefix} 👇 Optimized swipe down for ${screen_width}×${screen_height}"
2629
+ ;;
2630
+ "left")
2631
+ # Swipe left - from right edge to left edge at mid height
2632
+ start_x=$right_edge; start_y=$mid_height; end_x=$left_edge; end_y=$mid_height
2633
+ duration="500" # Medium speed for page transitions
2634
+ echo "${log_prefix} 👈 Optimized swipe left for ${screen_width}×${screen_height}"
2635
+ ;;
2636
+ "right")
2637
+ # Swipe right - from left edge to right edge at mid height
2638
+ start_x=$left_edge; start_y=$mid_height; end_x=$right_edge; end_y=$mid_height
2639
+ duration="500" # Medium speed for page transitions
2640
+ echo "${log_prefix} 👉 Optimized swipe right for ${screen_width}×${screen_height}"
2641
+ ;;
2642
+ esac
2643
+ elif [[ "$swipe_params" =~ ^[0-9]+,[0-9]+,[0-9]+,[0-9]+$ ]]; then
2644
+ # Custom coordinates: "x1,y1,x2,y2"
2645
+ start_x=$(echo "$swipe_params" | cut -d',' -f1)
2646
+ start_y=$(echo "$swipe_params" | cut -d',' -f2)
2647
+ end_x=$(echo "$swipe_params" | cut -d',' -f3)
2648
+ end_y=$(echo "$swipe_params" | cut -d',' -f4)
2649
+ direction="custom"
2650
+ elif [[ "$swipe_params" =~ ^[0-9]+,[0-9]+,[0-9]+,[0-9]+,[0-9]+$ ]]; then
2651
+ # Custom coordinates with duration: "x1,y1,x2,y2,duration_ms"
2652
+ start_x=$(echo "$swipe_params" | cut -d',' -f1)
2653
+ start_y=$(echo "$swipe_params" | cut -d',' -f2)
2654
+ end_x=$(echo "$swipe_params" | cut -d',' -f3)
2655
+ end_y=$(echo "$swipe_params" | cut -d',' -f4)
2656
+ duration=$(echo "$swipe_params" | cut -d',' -f5)
2657
+ direction="custom"
2658
+ else
2659
+ echo "${log_prefix} ❌ Invalid swipe format. Use: swipe up/down/left/right OR swipe x1,y1,x2,y2[,duration]"
2660
+ echo "${log_prefix} Examples: swipe up, swipe 100,200,300,400, swipe 50,50,350,600,500"
2661
+ continue
2662
+ fi
2663
+
2664
+ local session=$(get_device_session "$device_name")
2665
+ if [ -n "$session" ]; then
2666
+ if [ "$direction" = "custom" ]; then
2667
+ echo "${log_prefix} 👆 Swiping custom from ($start_x,$start_y) to ($end_x,$end_y) [${duration}ms]"
2668
+ else
2669
+ echo "${log_prefix} 👆 Swiping ${direction} from ($start_x,$start_y) to ($end_x,$end_y) [${duration}ms] - Screen ${screen_width}×${screen_height}"
2670
+ fi
2671
+
2672
+ # Use W3C Actions API for precise swipe gesture
2673
+ local swipe_payload='{
2674
+ "actions": [
2675
+ {
2676
+ "type": "pointer",
2677
+ "id": "finger1",
2678
+ "parameters": {"pointerType": "touch"},
2679
+ "actions": [
2680
+ {"type": "pointerMove", "duration": 0, "x": '$start_x', "y": '$start_y'},
2681
+ {"type": "pointerDown", "button": 0},
2682
+ {"type": "pointerMove", "duration": '$duration', "x": '$end_x', "y": '$end_y'},
2683
+ {"type": "pointerUp", "button": 0}
2684
+ ]
2685
+ }
2686
+ ]
2687
+ }'
2688
+
2689
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/actions" \
2690
+ -H 'Content-Type: application/json' \
2691
+ -d "$swipe_payload" 2>/dev/null)
2692
+
2693
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
2694
+ echo "${log_prefix} ✅ Swipe gesture successful"
2695
+ else
2696
+ # Fallback: Try WebDriverAgent drag endpoint
2697
+ echo "${log_prefix} Trying fallback drag method..."
2698
+ local duration_sec=$(python3 -c "print(float('$duration')/1000)" 2>/dev/null || echo "0.3")
2699
+ local drag_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/dragfromtoforduration" \
2700
+ -H 'Content-Type: application/json' \
2701
+ -d "{\"fromX\": $start_x, \"fromY\": $start_y, \"toX\": $end_x, \"toY\": $end_y, \"duration\": $duration_sec}" 2>/dev/null)
2702
+
2703
+ if echo "$drag_response" | grep -q '"sessionId"' && ! echo "$drag_response" | grep -q '"error"'; then
2704
+ echo "${log_prefix} ✅ Swipe gesture successful (drag method)"
2705
+ else
2706
+ echo "${log_prefix} ❌ Swipe gesture failed"
2707
+ echo "${log_prefix} Response: $response"
2708
+ fi
2709
+ fi
2710
+ else
2711
+ echo "${log_prefix} ❌ No session available"
2712
+ fi
2713
+ ;;
2714
+
2715
+ "getLocators"|"getlocators"|"locators")
2716
+ [ "$DEBUG" = "1" ] && echo "${log_prefix} 🔍 Getting all locators from current screen"
2717
+
2718
+ local session=$(get_device_session "$device_name")
2719
+ if [ -n "$session" ]; then
2720
+ # Check what app is currently active (don't disrupt it)
2721
+ [ "$DEBUG" = "1" ] && echo "${log_prefix} 📱 Checking active app..."
2722
+ local active_app_info=$(curl -s -X GET "http://$host:$WDA_PORT/wda/activeAppInfo" 2>/dev/null)
2723
+ local bundle_id=$(echo "$active_app_info" | python3 -c "
2724
+ import sys, json
2725
+ try:
2726
+ data = json.load(sys.stdin)
2727
+ print(data.get('value', {}).get('bundleId', 'com.apple.springboard'))
2728
+ except:
2729
+ print('com.apple.springboard')
2730
+ " 2>/dev/null)
2731
+
2732
+ if [ "$bundle_id" = "com.apple.springboard" ]; then
2733
+ [ "$DEBUG" = "1" ] && echo "${log_prefix} ℹ️ Currently on Home Screen"
2734
+ else
2735
+ [ "$DEBUG" = "1" ] && echo "${log_prefix} ℹ️ Active app: $bundle_id"
2736
+ fi
2737
+
2738
+ [ "$DEBUG" = "1" ] && echo "${log_prefix} 📱 Fetching page source..."
2739
+ local page_source=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/source" 2>/dev/null)
2740
+
2741
+ if [ -n "$page_source" ] && ! echo "$page_source" | grep -q '"error"'; then
2742
+ [ "$DEBUG" = "1" ] && echo "${log_prefix} 🔍 Extracting locators..."
2743
+
2744
+ # Extract all locator information using Python
2745
+ local locators=$(echo "$page_source" | python3 -c "
2746
+ import sys
2747
+ import json
2748
+ import xml.etree.ElementTree as ET
2749
+
2750
+ try:
2751
+ data = json.load(sys.stdin)
2752
+ xml_str = data.get('value', '')
2753
+
2754
+ if not xml_str:
2755
+ print('❌ No page source found')
2756
+ sys.exit(1)
2757
+
2758
+ root = ET.fromstring(xml_str)
2759
+ locators = []
2760
+
2761
+ # Extract locators from all elements
2762
+ for elem in root.iter():
2763
+ locator_info = {}
2764
+
2765
+ # Get element type
2766
+ elem_type = elem.tag
2767
+ locator_info['type'] = elem_type
2768
+
2769
+ # Get name/label
2770
+ name = elem.get('name', '')
2771
+ label = elem.get('label', '')
2772
+ value = elem.get('value', '')
2773
+
2774
+ if name:
2775
+ locator_info['name'] = name
2776
+ if label:
2777
+ locator_info['label'] = label
2778
+ if value:
2779
+ locator_info['value'] = value
2780
+
2781
+ # Get accessible property
2782
+ accessible = elem.get('accessible', 'false')
2783
+ locator_info['accessible'] = accessible
2784
+
2785
+ # Get visible property
2786
+ visible = elem.get('visible', 'false')
2787
+ locator_info['visible'] = visible
2788
+
2789
+ # Get enabled property
2790
+ enabled = elem.get('enabled', 'true')
2791
+ locator_info['enabled'] = enabled
2792
+
2793
+ # Get coordinates
2794
+ x = elem.get('x', '')
2795
+ y = elem.get('y', '')
2796
+ width = elem.get('width', '')
2797
+ height = elem.get('height', '')
2798
+
2799
+ if x and y and width and height:
2800
+ locator_info['coordinates'] = f'({x},{y}) {width}x{height}'
2801
+
2802
+ # Only add if we have meaningful locator information
2803
+ if name or label or value:
2804
+ locators.append(locator_info)
2805
+
2806
+ # Print formatted output - only in DEBUG mode
2807
+ import os
2808
+ debug = os.environ.get('DEBUG', '0') == '1'
2809
+
2810
+ if locators:
2811
+ if debug:
2812
+ import sys
2813
+ print(f'\\n📋 Found {len(locators)} interactive elements:\\n', file=sys.stderr)
2814
+ for i, loc in enumerate(locators, 1):
2815
+ # Build single line output with Name first, better formatting
2816
+ parts = []
2817
+
2818
+ # Start with number and Name (most important for clicking)
2819
+ name = loc.get('name', '')
2820
+ if name:
2821
+ parts.append(f'{i:3d}. 📍 {name:40s}')
2822
+ else:
2823
+ parts.append(f'{i:3d}. 📍 {\"(no name)\":40s}')
2824
+
2825
+ # Add Type (shortened for readability)
2826
+ elem_type = loc.get('type', 'unknown').replace('XCUIElementType', '')
2827
+ parts.append(f'[{elem_type:15s}]')
2828
+
2829
+ # Add Label if different from Name
2830
+ label = loc.get('label', '')
2831
+ if label and label != name:
2832
+ parts.append(f'Label:{label:30s}' if len(label) <= 30 else f'Label:{label[:27]}...')
2833
+
2834
+ # Add Value if present
2835
+ value = loc.get('value', '')
2836
+ if value and value != name and value != label:
2837
+ parts.append(f'Val:{value:20s}' if len(value) <= 20 else f'Val:{value[:17]}...')
2838
+
2839
+ # Add Position
2840
+ if loc.get('coordinates'):
2841
+ parts.append(f'@ {loc[\"coordinates\"]}')
2842
+
2843
+ # Add status flags (compact)
2844
+ flags = []
2845
+ if loc.get('visible') == 'true':
2846
+ flags.append('✓Vis')
2847
+ if loc.get('enabled') == 'true':
2848
+ flags.append('✓En')
2849
+ if flags:
2850
+ parts.append(' '.join(flags))
2851
+
2852
+ print(' │ '.join(parts), file=sys.stderr)
2853
+
2854
+ # Output JSON to stdout (always, regardless of DEBUG)
2855
+ import json
2856
+ print(json.dumps(locators))
2857
+
2858
+ except Exception as e:
2859
+ import os
2860
+ import sys
2861
+ debug = os.environ.get('DEBUG', '0') == '1'
2862
+ if debug:
2863
+ print(f'❌ Error parsing page source: {e}', file=sys.stderr)
2864
+ sys.exit(1)
2865
+ " 2>&1)
2866
+
2867
+ if [ $? -eq 0 ]; then
2868
+ echo "${log_prefix} $locators"
2869
+ echo "${log_prefix} ✅ Locators extraction complete"
2870
+ else
2871
+ echo "${log_prefix} ❌ Failed to extract locators: $locators"
2872
+ fi
2873
+ else
2874
+ echo "${log_prefix} ❌ Failed to get page source"
2875
+ fi
2876
+ else
2877
+ echo "${log_prefix} ❌ No session available"
2878
+ fi
2879
+ ;;
2880
+
2881
+ *)
2882
+ # Text input - use existing persistent session (like original script)
2883
+ echo "${log_prefix} CASE: Default text input"
2884
+ echo "${log_prefix} Sending text: $command"
2885
+
2886
+ # Convert text to JSON array format (properly escape for shell safety)
2887
+ local json_array=$(python3 -c "
2888
+ import json
2889
+ import sys
2890
+ text = sys.argv[1] if len(sys.argv) > 1 else ''
2891
+ print(json.dumps([text]))" "$command" 2>/dev/null)
2892
+
2893
+ if [ -n "$json_array" ]; then
2894
+ # Use the persistent session for this device
2895
+ local session=$(get_device_session "$device_name")
2896
+
2897
+ if [ -n "$session" ]; then
2898
+ # Send text using existing session (preserves current app context)
2899
+ local response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/keys" \
2900
+ -H 'Content-Type: application/json' \
2901
+ -d "{\"value\":$json_array}")
2902
+
2903
+ if echo "$response" | grep -q '"sessionId"' && ! echo "$response" | grep -q '"error"'; then
2904
+ echo "${log_prefix} ✅ Text sent"
2905
+
2906
+ # Auto-press Enter after text with minimal delay
2907
+ sleep 0.1
2908
+ local enter_array=$(python3 -c "import json; print(json.dumps(['\\n']))" 2>/dev/null)
2909
+ local enter_response=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/keys" \
2910
+ -H 'Content-Type: application/json' \
2911
+ -d "{\"value\":$enter_array}")
2912
+
2913
+ if echo "$enter_response" | grep -q '"sessionId"' && ! echo "$enter_response" | grep -q '"error"'; then
2914
+ echo "${log_prefix} ✅ Auto-pressed Enter"
2915
+ fi
2916
+ else
2917
+ echo "${log_prefix} ❌ Text send failed"
2918
+ fi
2919
+
2920
+ # DO NOT clean up session - keep it for future commands!
2921
+ else
2922
+ echo "${log_prefix} ❌ No session available"
2923
+ fi
2924
+ else
2925
+ echo "${log_prefix} ❌ JSON encoding failed"
2926
+ fi
2927
+ ;;
2928
+ esac
2929
+ }
2930
+
2931
+ # Check current appearance mode (helper function)
2932
+ check_appearance_mode() {
2933
+ local device="$1"
2934
+
2935
+ local device_details
2936
+ device_details=$(get_device_details "$device")
2937
+ if [ $? -ne 0 ]; then
2938
+ print_color "$RED" "❌ Device not found: $device"
2939
+ return 1
2940
+ fi
2941
+
2942
+ local host=$(echo "$device_details" | cut -d',' -f1)
2943
+ local session=$(get_device_session "$device")
2944
+
2945
+ if [ -n "$session" ]; then
2946
+ # Try to get current appearance info via WDA
2947
+ local appearance_info=$(curl -s -X GET "http://$host:$WDA_PORT/session/$session/appium/device/current_activity" 2>/dev/null)
2948
+
2949
+ if echo "$appearance_info" | grep -q "dark\|Dark"; then
2950
+ print_color "$BLUE" "🌙 $device: Currently in Dark Mode"
2951
+ elif echo "$appearance_info" | grep -q "light\|Light"; then
2952
+ print_color "$YELLOW" "☀️ $device: Currently in Light Mode"
2953
+ else
2954
+ print_color "$CYAN" "❓ $device: Appearance mode unknown"
2955
+ fi
2956
+ else
2957
+ print_color "$RED" "❌ $device: No session available to check appearance"
2958
+ fi
2959
+ }
2960
+
2961
+ # Dark Mode / Light Mode Toggle
2962
+ toggle_dark_mode() {
2963
+ local device="$1"
2964
+ local mode="${2:-toggle}" # toggle, dark, light
2965
+
2966
+ local device_details
2967
+ device_details=$(get_device_details "$device")
2968
+ if [ $? -ne 0 ]; then
2969
+ print_color "$RED" "❌ Device not found: $device"
2970
+ return 1
2971
+ fi
2972
+
2973
+ local host=$(echo "$device_details" | cut -d',' -f1)
2974
+ local udid=$(echo "$device_details" | cut -d',' -f2)
2975
+
2976
+ # Method 1: Try using xcrun devicectl (iOS 17+)
2977
+ if command -v xcrun >/dev/null 2>&1; then
2978
+ print_color "$CYAN" "$device: Trying xcrun devicectl for appearance..."
2979
+
2980
+ case "$mode" in
2981
+ "dark")
2982
+ local result=$(xcrun devicectl device install app --device "$udid" --json 2>/dev/null || echo "not_supported")
2983
+ if [ "$result" != "not_supported" ]; then
2984
+ # Try using xcrun to set appearance (if supported)
2985
+ xcrun simctl ui "$udid" appearance dark 2>/dev/null && {
2986
+ print_color "$GREEN" "✅ $device: Set to Dark Mode via xcrun"
2987
+ return 0
2988
+ }
2989
+ fi
2990
+ ;;
2991
+ "light")
2992
+ xcrun simctl ui "$udid" appearance light 2>/dev/null && {
2993
+ print_color "$GREEN" "✅ $device: Set to Light Mode via xcrun"
2994
+ return 0
2995
+ }
2996
+ ;;
2997
+ esac
2998
+ fi
2999
+
3000
+ # Method 2: Try using idevice tools with terminal commands
3001
+ if command -v idevice-app-runner >/dev/null 2>&1 || command -v ios-deploy >/dev/null 2>&1; then
3002
+ print_color "$CYAN" "$device: Trying iOS terminal commands..."
3003
+
3004
+ # Use shortcuts if available (requires Shortcuts app)
3005
+ local shortcuts_bundle="com.apple.shortcuts"
3006
+ local session=$(get_device_session "$device")
3007
+
3008
+ if [ -n "$session" ]; then
3009
+ # Try to create a simple shortcut command via WDA
3010
+ local shortcut_url="http://$host:$WDA_PORT/session/$session/url"
3011
+
3012
+ case "$mode" in
3013
+ "dark")
3014
+ # Use URL scheme to trigger shortcuts or settings
3015
+ local result=$(curl -s -X POST "$shortcut_url" \
3016
+ -H "Content-Type: application/json" \
3017
+ -d '{"url": "prefs:root=DISPLAY&path=APPEARANCE"}' 2>/dev/null)
3018
+
3019
+ if echo "$result" | grep -q '"sessionId"'; then
3020
+ sleep 1
3021
+ # Try to tap the Dark option
3022
+ local tap_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/tap/0" \
3023
+ -H "Content-Type: application/json" \
3024
+ -d '{"x": 100, "y": 400}' 2>/dev/null)
3025
+
3026
+ print_color "$GREEN" "✅ $device: Attempted Dark Mode via Settings URL"
3027
+ execute_wda_command "$device" "home" >/dev/null
3028
+ return 0
3029
+ fi
3030
+ ;;
3031
+ "light")
3032
+ local result=$(curl -s -X POST "$shortcut_url" \
3033
+ -H "Content-Type: application/json" \
3034
+ -d '{"url": "prefs:root=DISPLAY&path=APPEARANCE"}' 2>/dev/null)
3035
+
3036
+ if echo "$result" | grep -q '"sessionId"'; then
3037
+ sleep 1
3038
+ # Try to tap the Light option
3039
+ local tap_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/tap/0" \
3040
+ -H "Content-Type: application/json" \
3041
+ -d '{"x": 100, "y": 300}' 2>/dev/null)
3042
+
3043
+ print_color "$GREEN" "✅ $device: Attempted Light Mode via Settings URL"
3044
+ execute_wda_command "$device" "home" >/dev/null
3045
+ return 0
3046
+ fi
3047
+ ;;
3048
+ esac
3049
+ fi
3050
+ fi
3051
+
3052
+ # Method 3: Enhanced WDA approach with better element detection
3053
+ local session=$(get_device_session "$device")
3054
+ if [ -n "$session" ]; then
3055
+ print_color "$CYAN" "$device: Using enhanced WDA method..."
3056
+
3057
+ # Open Settings using URL scheme first
3058
+ local settings_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/url" \
3059
+ -H "Content-Type: application/json" \
3060
+ -d '{"url": "App-prefs:DISPLAY"}' 2>/dev/null)
3061
+
3062
+ if ! echo "$settings_result" | grep -q '"sessionId"'; then
3063
+ # Fallback: Launch Settings app manually
3064
+ execute_wda_command "$device" "launch com.apple.Preferences" >/dev/null
3065
+ sleep 2
3066
+
3067
+ # Try to find and tap Display & Brightness
3068
+ local search_url="http://$host:$WDA_PORT/session/$session/elements"
3069
+ local elements_result=$(curl -s -X POST "$search_url" \
3070
+ -H "Content-Type: application/json" \
3071
+ -d '{"using": "partial link text", "value": "Display"}' 2>/dev/null)
3072
+
3073
+ if ! echo "$elements_result" | grep -q '"ELEMENT"'; then
3074
+ # Try alternative search methods
3075
+ elements_result=$(curl -s -X POST "$search_url" \
3076
+ -H "Content-Type: application/json" \
3077
+ -d '{"using": "accessibility id", "value": "Display & Brightness"}' 2>/dev/null)
3078
+ fi
3079
+
3080
+ if echo "$elements_result" | grep -q '"ELEMENT"'; then
3081
+ local element_id=$(echo "$elements_result" | grep -o '"ELEMENT":"[^"]*"' | head -1 | cut -d'"' -f4)
3082
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$element_id/click" >/dev/null
3083
+ sleep 1
3084
+ fi
3085
+ fi
3086
+
3087
+ sleep 2
3088
+
3089
+ # Now try to find and click the appearance option
3090
+ case "$mode" in
3091
+ "dark")
3092
+ # Look for Dark mode option with multiple strategies
3093
+ local dark_elements=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
3094
+ -H "Content-Type: application/json" \
3095
+ -d '{"using": "accessibility id", "value": "Dark"}' 2>/dev/null)
3096
+
3097
+ if echo "$dark_elements" | grep -q '"ELEMENT"'; then
3098
+ local dark_id=$(echo "$dark_elements" | grep -o '"ELEMENT":"[^"]*"' | head -1 | cut -d'"' -f4)
3099
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$dark_id/click" >/dev/null
3100
+ print_color "$GREEN" "✅ $device: Switched to Dark Mode"
3101
+ else
3102
+ # Fallback: Try coordinates (approximate location of Dark option)
3103
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/tap/0" \
3104
+ -H "Content-Type: application/json" \
3105
+ -d '{"x": 60, "y": 450}' >/dev/null
3106
+ print_color "$YELLOW" "⚠️ $device: Attempted Dark Mode (coordinate fallback)"
3107
+ fi
3108
+ ;;
3109
+ "light")
3110
+ local light_elements=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/elements" \
3111
+ -H "Content-Type: application/json" \
3112
+ -d '{"using": "accessibility id", "value": "Light"}' 2>/dev/null)
3113
+
3114
+ if echo "$light_elements" | grep -q '"ELEMENT"'; then
3115
+ local light_id=$(echo "$light_elements" | grep -o '"ELEMENT":"[^"]*"' | head -1 | cut -d'"' -f4)
3116
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$light_id/click" >/dev/null
3117
+ print_color "$GREEN" "✅ $device: Switched to Light Mode"
3118
+ else
3119
+ # Fallback: Try coordinates (approximate location of Light option)
3120
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/tap/0" \
3121
+ -H "Content-Type: application/json" \
3122
+ -d '{"x": 60, "y": 350}' >/dev/null
3123
+ print_color "$YELLOW" "⚠️ $device: Attempted Light Mode (coordinate fallback)"
3124
+ fi
3125
+ ;;
3126
+ *)
3127
+ # Toggle mode - try to detect current and switch
3128
+ print_color "$YELLOW" "⚠️ $device: Toggle mode - manual selection may be needed"
3129
+ ;;
3130
+ esac
3131
+
3132
+ # Go back to home
3133
+ sleep 1
3134
+ execute_wda_command "$device" "home" >/dev/null
3135
+ return 0
3136
+ else
3137
+ print_color "$RED" "❌ $device: No WDA session available"
3138
+ return 1
3139
+ fi
3140
+ }
3141
+
3142
+ # Screen Rotation Functions
3143
+ rotate_screen() {
3144
+ local device="$1"
3145
+ local orientation="${2:-toggle}" # portrait, landscape-left, landscape-right, portrait-upside-down, toggle
3146
+
3147
+ local device_details
3148
+ device_details=$(get_device_details "$device")
3149
+ if [ $? -ne 0 ]; then
3150
+ print_color "$RED" "❌ Device not found: $device"
3151
+ return 1
3152
+ fi
3153
+
3154
+ local host=$(echo "$device_details" | cut -d',' -f1)
3155
+ local udid=$(echo "$device_details" | cut -d',' -f2)
3156
+
3157
+ # Method 1: Try using xcrun devicectl for rotation (iOS 17+)
3158
+ if command -v xcrun >/dev/null 2>&1; then
3159
+ case "$orientation" in
3160
+ "portrait"|"1")
3161
+ if xcrun simctl status_bar "$udid" override --orientation portrait 2>/dev/null; then
3162
+ print_color "$GREEN" "✅ $device: Rotated to Portrait via xcrun"
3163
+ return 0
3164
+ fi
3165
+ ;;
3166
+ "landscape-left"|"landscape"|"3")
3167
+ if xcrun simctl status_bar "$udid" override --orientation landscapeLeft 2>/dev/null; then
3168
+ print_color "$GREEN" "✅ $device: Rotated to Landscape Left via xcrun"
3169
+ return 0
3170
+ fi
3171
+ ;;
3172
+ "landscape-right"|"4")
3173
+ if xcrun simctl status_bar "$udid" override --orientation landscapeRight 2>/dev/null; then
3174
+ print_color "$GREEN" "✅ $device: Rotated to Landscape Right via xcrun"
3175
+ return 0
3176
+ fi
3177
+ ;;
3178
+ esac
3179
+ fi
3180
+
3181
+ # Method 2: Use WDA orientation API
3182
+ local session=$(get_device_session "$device")
3183
+ if [ -n "$session" ]; then
3184
+ local orientation_url="http://$host:$WDA_PORT/session/$session/orientation"
3185
+
3186
+ # Get current orientation first
3187
+ local current_orientation=$(curl -s -X GET "$orientation_url" | grep -o '"value":"[^"]*"' | cut -d'"' -f4)
3188
+
3189
+ # Map orientation names to WDA values
3190
+ local orientation_value
3191
+ case "$orientation" in
3192
+ "portrait"|"1")
3193
+ orientation_value="PORTRAIT"
3194
+ ;;
3195
+ "landscape-left"|"landscape"|"3")
3196
+ orientation_value="LANDSCAPE"
3197
+ ;;
3198
+ "landscape-right"|"4")
3199
+ orientation_value="UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT"
3200
+ ;;
3201
+ "portrait-upside-down"|"2")
3202
+ orientation_value="UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN"
3203
+ ;;
3204
+ "toggle"|*)
3205
+ # Toggle between portrait and landscape
3206
+ case "$current_orientation" in
3207
+ "PORTRAIT") orientation_value="LANDSCAPE" ;;
3208
+ "LANDSCAPE") orientation_value="PORTRAIT" ;;
3209
+ *) orientation_value="LANDSCAPE" ;; # Default to landscape if unknown
3210
+ esac
3211
+ ;;
3212
+ esac
3213
+
3214
+ # Set orientation via WDA
3215
+ local response=$(curl -s -X POST "$orientation_url" \
3216
+ -H "Content-Type: application/json" \
3217
+ -d "{\"orientation\": \"$orientation_value\"}")
3218
+
3219
+ if echo "$response" | grep -q '"value":null\|"sessionId"'; then
3220
+ print_color "$GREEN" "✅ $device: Rotated to $orientation_value"
3221
+ return 0
3222
+ else
3223
+ print_color "$YELLOW" "⚠️ $device: Rotation may not be supported or device is locked"
3224
+ fi
3225
+ fi
3226
+
3227
+ # Method 3: Try using idevice tools if available
3228
+ if command -v idevice-app-runner >/dev/null 2>&1; then
3229
+ print_color "$CYAN" "$device: Trying idevice rotation tools..."
3230
+ # Note: Most idevice tools don't support rotation directly
3231
+ print_color "$YELLOW" "⚠️ $device: idevice rotation not directly supported"
3232
+ fi
3233
+
3234
+ print_color "$RED" "❌ $device: Could not rotate screen - may need manual rotation"
3235
+ return 1
3236
+ }
3237
+
3238
+ # Lock/Unlock Rotation
3239
+ toggle_rotation_lock() {
3240
+ local device="$1"
3241
+ local lock_state="${2:-toggle}" # lock, unlock, toggle
3242
+
3243
+ local device_details
3244
+ device_details=$(get_device_details "$device")
3245
+ if [ $? -ne 0 ]; then
3246
+ print_color "$RED" "❌ Device not found: $device"
3247
+ return 1
3248
+ fi
3249
+
3250
+ local host=$(echo "$device_details" | cut -d',' -f1)
3251
+ local udid=$(echo "$device_details" | cut -d',' -f2)
3252
+
3253
+ # Method 1: Try using terminal/xcrun commands
3254
+ if command -v xcrun >/dev/null 2>&1; then
3255
+ # Note: xcrun doesn't directly support rotation lock for real devices
3256
+ print_color "$CYAN" "$device: Rotation lock via terminal not directly supported for physical devices"
3257
+ fi
3258
+
3259
+ # Method 2: Use WDA to access Control Center
3260
+ local session=$(get_device_session "$device")
3261
+ if [ -n "$session" ]; then
3262
+ print_color "$CYAN" "$device: Opening Control Center to toggle rotation lock..."
3263
+
3264
+ # Open Control Center by swiping from top-right (iOS 12+) or bottom (older iOS)
3265
+ # Try modern swipe first (from top-right corner)
3266
+ local swipe_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/dragfromtoforduration" \
3267
+ -H "Content-Type: application/json" \
3268
+ -d '{"fromX": 350, "fromY": 10, "toX": 350, "toY": 200, "duration": 0.5}' 2>/dev/null)
3269
+
3270
+ if ! echo "$swipe_result" | grep -q '"sessionId"'; then
3271
+ # Fallback: Try swipe up from bottom (older iOS)
3272
+ swipe_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/dragfromtoforduration" \
3273
+ -H "Content-Type: application/json" \
3274
+ -d '{"fromX": 200, "fromY": 800, "toX": 200, "toY": 400, "duration": 0.5}' 2>/dev/null)
3275
+ fi
3276
+
3277
+ sleep 2
3278
+
3279
+ # Look for rotation lock button with multiple strategies
3280
+ local search_url="http://$host:$WDA_PORT/session/$session/elements"
3281
+
3282
+ # Strategy 1: Find by accessibility identifier
3283
+ local rotation_elements=$(curl -s -X POST "$search_url" \
3284
+ -H "Content-Type: application/json" \
3285
+ -d '{"using": "accessibility id", "value": "portrait-orientation-lock"}' 2>/dev/null)
3286
+
3287
+ if ! echo "$rotation_elements" | grep -q '"ELEMENT"'; then
3288
+ # Strategy 2: Find by name
3289
+ rotation_elements=$(curl -s -X POST "$search_url" \
3290
+ -H "Content-Type: application/json" \
3291
+ -d '{"using": "name", "value": "Portrait Orientation Lock"}' 2>/dev/null)
3292
+ fi
3293
+
3294
+ if ! echo "$rotation_elements" | grep -q '"ELEMENT"'; then
3295
+ # Strategy 3: Find by partial name match
3296
+ rotation_elements=$(curl -s -X POST "$search_url" \
3297
+ -H "Content-Type: application/json" \
3298
+ -d '{"using": "partial link text", "value": "orientation"}' 2>/dev/null)
3299
+ fi
3300
+
3301
+ if echo "$rotation_elements" | grep -q '"ELEMENT"'; then
3302
+ local rotation_id=$(echo "$rotation_elements" | grep -o '"ELEMENT":"[^"]*"' | head -1 | cut -d'"' -f4)
3303
+ local tap_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/element/$rotation_id/click" 2>/dev/null)
3304
+
3305
+ if echo "$tap_result" | grep -q '"sessionId"'; then
3306
+ print_color "$GREEN" "✅ $device: Toggled rotation lock"
3307
+ else
3308
+ print_color "$YELLOW" "⚠️ $device: Rotation lock button found but tap may have failed"
3309
+ fi
3310
+ else
3311
+ # Fallback: Try common coordinates for rotation lock button
3312
+ print_color "$YELLOW" "$device: Using coordinate fallback for rotation lock..."
3313
+
3314
+ # Common positions for rotation lock in Control Center
3315
+ local coords=("60,300" "60,350" "80,320" "100,340")
3316
+
3317
+ for coord in "${coords[@]}"; do
3318
+ local x=$(echo "$coord" | cut -d',' -f1)
3319
+ local y=$(echo "$coord" | cut -d',' -f2)
3320
+
3321
+ local tap_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/tap/0" \
3322
+ -H "Content-Type: application/json" \
3323
+ -d "{\"x\": $x, \"y\": $y}" 2>/dev/null)
3324
+
3325
+ if echo "$tap_result" | grep -q '"sessionId"'; then
3326
+ print_color "$YELLOW" "⚠️ $device: Attempted rotation lock toggle at ($x,$y)"
3327
+ break
3328
+ fi
3329
+ done
3330
+ fi
3331
+
3332
+ # Close Control Center by tapping outside or swiping down
3333
+ sleep 1
3334
+ local close_result=$(curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/tap/0" \
3335
+ -H "Content-Type: application/json" \
3336
+ -d '{"x": 200, "y": 600}' 2>/dev/null)
3337
+
3338
+ # Alternative: Swipe down to close Control Center
3339
+ if ! echo "$close_result" | grep -q '"sessionId"'; then
3340
+ curl -s -X POST "http://$host:$WDA_PORT/session/$session/wda/dragfromtoforduration" \
3341
+ -H "Content-Type: application/json" \
3342
+ -d '{"fromX": 200, "fromY": 200, "toX": 200, "toY": 600, "duration": 0.3}' >/dev/null
3343
+ fi
3344
+
3345
+ return 0
3346
+ else
3347
+ print_color "$RED" "❌ $device: No WDA session available for rotation lock"
3348
+ return 1
3349
+ fi
3350
+ }
3351
+
3352
+ # Discover and connect devices
3353
+ discover_and_connect_devices() {
3354
+ print_color "$BLUE" "🔍 Discovering iOS devices..."
3355
+ echo ""
3356
+
3357
+ local ALL_DEVICES=($(get_all_device_names))
3358
+
3359
+ if [ ${#ALL_DEVICES[@]} -eq 0 ]; then
3360
+ print_color "$RED" "❌ No devices found in $CONFIG_FILE"
3361
+ exit 1
3362
+ fi
3363
+
3364
+ print_color "$YELLOW" "Configured devices:"
3365
+ for device in "${ALL_DEVICES[@]}"; do
3366
+ local details=$(get_device_details "$device")
3367
+ local host=$(echo "$details" | cut -d',' -f1)
3368
+ print_color "$CYAN" " • $device ($host)"
3369
+ done
3370
+ echo ""
3371
+
3372
+ print_color "$YELLOW" "Testing connections in parallel..."
3373
+ echo ""
3374
+
3375
+ CONNECTED_DEVICES=()
3376
+ local failed_devices=()
3377
+
3378
+ # Create temp file for parallel results
3379
+ local results_file="/tmp/wireless_connect_results_$$.txt"
3380
+ : > "$results_file"
3381
+
3382
+ # Test all devices in parallel
3383
+ local pids=()
3384
+ for device in "${ALL_DEVICES[@]}"; do
3385
+ {
3386
+ local details=$(get_device_details "$device")
3387
+ local host=$(echo "$details" | cut -d',' -f1)
3388
+ print_color "$CYAN" "📱 Testing $device ($host):"
3389
+
3390
+ if check_device_connectivity "$device"; then
3391
+ # Use lock for atomic write
3392
+ local lock_file="${results_file}.lock"
3393
+ while ! mkdir "$lock_file" 2>/dev/null; do
3394
+ sleep 0.01
3395
+ done
3396
+ echo "SUCCESS:$device" >> "$results_file"
3397
+ rmdir "$lock_file"
3398
+ print_color "$GREEN" " ✅ $device ready"
3399
+ else
3400
+ # Use lock for atomic write
3401
+ local lock_file="${results_file}.lock"
3402
+ while ! mkdir "$lock_file" 2>/dev/null; do
3403
+ sleep 0.01
3404
+ done
3405
+ echo "FAILED:$device" >> "$results_file"
3406
+ rmdir "$lock_file"
3407
+ print_color "$RED" " ❌ $device failed to connect"
3408
+ fi
3409
+ echo ""
3410
+ } &
3411
+ pids+=($!)
3412
+ done
3413
+
3414
+ # Wait for all connectivity checks to complete
3415
+ for pid in "${pids[@]}"; do
3416
+ wait "$pid"
3417
+ done
3418
+
3419
+ # Parse results
3420
+ while IFS=':' read -r status device; do
3421
+ if [ "$status" = "SUCCESS" ]; then
3422
+ CONNECTED_DEVICES+=("$device")
3423
+ elif [ "$status" = "FAILED" ]; then
3424
+ failed_devices+=("$device")
3425
+ fi
3426
+ done < "$results_file"
3427
+
3428
+ # Cleanup temp file
3429
+ rm -f "$results_file" "${results_file}.lock" 2>/dev/null
3430
+
3431
+ print_color "$YELLOW" "📋 Connection Summary:"
3432
+ echo ""
3433
+
3434
+ if [ ${#CONNECTED_DEVICES[@]} -gt 0 ]; then
3435
+ print_color "$GREEN" "🎉 ${#CONNECTED_DEVICES[@]} device(s) ready!"
3436
+ print_color "$CYAN" "Connected: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
3437
+ if [ ${#failed_devices[@]} -gt 0 ]; then
3438
+ print_color "$YELLOW" "⚠️ Offline: $(IFS=', '; echo "${failed_devices[*]}")"
3439
+ fi
3440
+ else
3441
+ print_color "$RED" "❌ No devices are online"
3442
+ exit 1
3443
+ fi
3444
+
3445
+ echo ""
3446
+ }
3447
+
3448
+ # Send command to multiple devices
3449
+ send_to_all_devices() {
3450
+ local devices=("$@")
3451
+ local pids=()
3452
+
3453
+ print_color "$YELLOW" "🚀 Executing on ${#devices[@]} device(s)..."
3454
+ echo ""
3455
+
3456
+ for device in "${devices[@]}"; do
3457
+ {
3458
+ execute_wda_command "$device" "$TEXT"
3459
+ echo "[${device}] Completed"
3460
+ } &
3461
+ pids+=($!)
3462
+ print_color "$CYAN" "📱 Started: $device (PID: $!)"
3463
+ done
3464
+
3465
+ echo ""
3466
+ print_color "$YELLOW" "⏳ Waiting for completion..."
3467
+
3468
+ for pid in "${pids[@]}"; do
3469
+ wait "$pid"
3470
+ done
3471
+
3472
+ echo ""
3473
+ print_color "$GREEN" "✅ All devices completed!"
3474
+ }
3475
+
3476
+ # Cleanup all sessions and terminate WDA processes on respective devices
3477
+ # Graceful cleanup function (preserves device trust)
3478
+ graceful_cleanup() {
3479
+ local preserve_trust="${1:-true}"
3480
+
3481
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
3482
+ print_color "$YELLOW" "🧹 Gracefully closing sessions..."
3483
+
3484
+ # Only close WebDriver sessions, don't terminate WDA processes
3485
+ while IFS=':' read -r device session; do
3486
+ if [ -n "$device" ] && [ -n "$session" ]; then
3487
+ local device_details
3488
+ device_details=$(get_device_details "$device")
3489
+ local host=$(echo "$device_details" | cut -d',' -f1)
3490
+
3491
+ # Just close the WebDriver session, keep WDA running
3492
+ curl -s -X DELETE "http://$host:$WDA_PORT/session/$session" >/dev/null 2>&1 || true
3493
+ echo " ✅ Session closed: $device"
3494
+ fi
3495
+ done < "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
3496
+
3497
+ rm -f "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
3498
+
3499
+ if [ "$preserve_trust" = "true" ]; then
3500
+ print_color "$GREEN" "✅ Sessions closed gracefully"
3501
+ print_color "$CYAN" "💡 WebDriverAgent processes left running to preserve device trust"
3502
+ print_color "$YELLOW" " Note: Devices should remain trusted and ready for reconnection"
3503
+ else
3504
+ # Call the full cleanup if explicitly requested
3505
+ cleanup_all_sessions_full
3506
+ fi
3507
+ fi
3508
+ }
3509
+
3510
+ # Full cleanup function (original behavior - may affect trust)
3511
+ cleanup_all_sessions_full() {
3512
+ if [ -f "$DEVICE_SESSIONS_FILE" ]; then
3513
+ print_color "$YELLOW" "🧹 Cleaning up sessions and stopping WDA on devices..."
3514
+
3515
+ # Collect device UDIDs for WDA termination
3516
+ local device_udids=()
3517
+
3518
+ while IFS=':' read -r device session; do
3519
+ if [ -n "$device" ] && [ -n "$session" ]; then
3520
+ local device_details
3521
+ device_details=$(get_device_details "$device")
3522
+ local host=$(echo "$device_details" | cut -d',' -f1)
3523
+ local udid=$(echo "$device_details" | cut -d',' -f2)
3524
+
3525
+ # Close WebDriver session first
3526
+ curl -s -X DELETE "http://$host:$WDA_PORT/session/$session" >/dev/null 2>&1 || true
3527
+ echo " ✅ Session closed: $device"
3528
+
3529
+ # Collect UDID for WDA process termination
3530
+ if [ -n "$udid" ]; then
3531
+ device_udids+=("$udid")
3532
+ fi
3533
+ fi
3534
+ done < "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
3535
+
3536
+ rm -f "$DEVICE_SESSIONS_FILE" 2>/dev/null || true
3537
+
3538
+ # Terminate WDA processes for the connected devices
3539
+ print_color "$CYAN" "🛑 Terminating WDA processes on respective devices..."
3540
+
3541
+ if [ ${#device_udids[@]} -gt 0 ]; then
3542
+ for udid in "${device_udids[@]}"; do
3543
+ # Kill WDA process for this specific device UDID
3544
+ local killed_count=$(pkill -f "WebDriverAgentRunner.*$udid" 2>/dev/null; echo $?)
3545
+ if [ $killed_count -eq 0 ]; then
3546
+ echo " ✅ WDA terminated on device: ${udid:0:8}..."
3547
+ else
3548
+ echo " ℹ️ No WDA process found for: ${udid:0:8}..."
3549
+ fi
3550
+ done
3551
+ else
3552
+ # Fallback: stop all WDA processes if no specific devices found
3553
+ print_color "$YELLOW" " → No specific devices found, stopping all WDA processes..."
3554
+ pkill -f "WebDriverAgentRunner" 2>/dev/null || true
3555
+ fi
3556
+
3557
+ # Also terminate any lingering WDA processes (in case other scripts left them)
3558
+ print_color "$CYAN" "🧹 Cleaning up any remaining WDA processes..."
3559
+ local remaining=$(pgrep -f "WebDriverAgentRunner" 2>/dev/null | wc -l)
3560
+ if [ "$remaining" -gt 0 ]; then
3561
+ pkill -TERM -f "WebDriverAgentRunner" 2>/dev/null || true
3562
+ sleep 1
3563
+ pkill -KILL -f "WebDriverAgentRunner" 2>/dev/null || true
3564
+ echo " ✅ Cleaned up $remaining remaining WDA processes"
3565
+ else
3566
+ echo " ✅ No remaining WDA processes found"
3567
+ fi
3568
+
3569
+ print_color "$GREEN" "✅ All WDA sessions and processes terminated"
3570
+ print_color "$CYAN" "💡 Ready for manual testing - completely clean environment"
3571
+ print_color "$YELLOW" " Note: You may need to re-establish wireless connections in Xcode"
3572
+ fi
3573
+ }
3574
+
3575
+ # Legacy cleanup function name (redirects to graceful cleanup)
3576
+ cleanup_all_sessions() {
3577
+ graceful_cleanup true
3578
+ }
3579
+
3580
+ # Signal cleanup function (preserves device trust on interruption)
3581
+ cleanup_on_signal() {
3582
+ echo ""
3583
+ print_color "$YELLOW" "⚠️ Script interrupted - cleaning up gracefully..."
3584
+ graceful_cleanup true
3585
+ exit 0
3586
+ }
3587
+
3588
+ # Signal handlers for proper cleanup (only on interruption, not normal exit)
3589
+ trap cleanup_on_signal INT
3590
+ trap cleanup_on_signal TERM
3591
+
3592
+ # Main execution
3593
+ if [ -n "$DEVICE_NAME_REQUEST" ]; then
3594
+ # Single device mode
3595
+ if ! get_device_details "$DEVICE_NAME_REQUEST" >/dev/null; then
3596
+ print_color "$RED" "❌ Device '$DEVICE_NAME_REQUEST' not found"
3597
+ exit 1
3598
+ fi
3599
+
3600
+ print_color "$BLUE" "🎯 Single device: $DEVICE_NAME_REQUEST"
3601
+ if check_device_connectivity "$DEVICE_NAME_REQUEST"; then
3602
+ if [ -n "$TEXT" ]; then
3603
+ # Single command mode - execute and exit
3604
+ # Check if session already exists (e.g., from a launched app)
3605
+ existing_session=$(get_device_session "$DEVICE_NAME_REQUEST")
3606
+ session="$existing_session"
3607
+
3608
+ if [ -z "$session" ]; then
3609
+ # No existing session, create new SpringBoard session
3610
+ session=$(create_persistent_session "$DEVICE_NAME_REQUEST")
3611
+ else
3612
+ echo " ℹ️ Using existing session: $session"
3613
+ fi
3614
+
3615
+ if [ -n "$session" ]; then
3616
+ execute_wda_command "$DEVICE_NAME_REQUEST" "$TEXT"
3617
+ fi
3618
+ # DON'T cleanup session - keep app running!
3619
+ # cleanup_wda_session "$DEVICE_NAME_REQUEST"
3620
+ exit 0
3621
+ else
3622
+ # Single device interactive mode
3623
+ print_color "$YELLOW" "🔧 Initializing interactive session..."
3624
+ session=$(create_persistent_session "$DEVICE_NAME_REQUEST")
3625
+ if [ -n "$session" ]; then
3626
+ print_color "$GREEN" " ✅ Session ready: $DEVICE_NAME_REQUEST"
3627
+ else
3628
+ print_color "$RED" " ❌ Session failed: $DEVICE_NAME_REQUEST"
3629
+ exit 1
3630
+ fi
3631
+
3632
+ echo ""
3633
+ load_text_history
3634
+
3635
+ print_color "$CYAN" "🔄 Single-device Interactive Mode: $DEVICE_NAME_REQUEST"
3636
+ print_color "$YELLOW" " Commands: launch/kill/home/url/screenshot + text input"
3637
+ print_color "$YELLOW" " Type 'help' for commands • 'quit' to exit"
3638
+ echo ""
3639
+
3640
+ while true; do
3641
+ # Enable readline for this input with custom history
3642
+ set -o history
3643
+ HISTFILE="$HISTORY_FILE"
3644
+ HISTSIZE="$HISTORY_MAX"
3645
+ HISTFILESIZE="$HISTORY_MAX"
3646
+
3647
+ # Load history from file for this session
3648
+ if [ -f "$HISTORY_FILE" ]; then
3649
+ history -r "$HISTORY_FILE"
3650
+ fi
3651
+
3652
+ read -e -p "$DEVICE_NAME_REQUEST > " INPUT_TEXT
3653
+
3654
+ # Add any non-empty input to history (except quit commands)
3655
+ if [ -n "$INPUT_TEXT" ] && [[ "$INPUT_TEXT" != "quit" ]] && [[ "$INPUT_TEXT" != "exit" ]] && [[ "$INPUT_TEXT" != "q" ]] && [[ "$INPUT_TEXT" != "help" ]]; then
3656
+ add_to_history "$INPUT_TEXT"
3657
+ fi
3658
+
3659
+ # Disable history again to avoid shell command pollution
3660
+ set +o history
3661
+ unset HISTFILE
3662
+
3663
+ case "$INPUT_TEXT" in
3664
+ "quit"|"q"|"exit")
3665
+ print_color "$YELLOW" "👋 Goodbye!"
3666
+ cleanup_wda_session "$DEVICE_NAME_REQUEST"
3667
+ exit 0
3668
+ ;;
3669
+ "forcecleanup"|"killwda")
3670
+ print_color "$YELLOW" "🛑 Force cleaning up - this may affect device trust..."
3671
+ cleanup_all_sessions_full
3672
+ print_color "$YELLOW" "👋 Goodbye!"
3673
+ exit 0
3674
+ ;;
3675
+ "help"|"commands")
3676
+ print_color "$YELLOW" "📱 Available Commands:"
3677
+ echo ""
3678
+ print_color "$CYAN" "🚀 App Control:"
3679
+ print_color "$YELLOW" " launch <app> - Launch application"
3680
+ print_color "$YELLOW" " open <app> - Same as launch"
3681
+ print_color "$YELLOW" " kill <app> - Terminate application"
3682
+ print_color "$YELLOW" " close <app> - Same as kill"
3683
+ print_color "$YELLOW" " install <path> - Install IPA file (requires iOS tools)"
3684
+ print_color "$YELLOW" " uninstall <id> - Uninstall app by bundle ID (requires iOS tools)"
3685
+ print_color "$YELLOW" " appinfo <id> - Check app version/info by bundle ID"
3686
+ print_color "$YELLOW" " version <id> - Same as appinfo"
3687
+ print_color "$YELLOW" " listapps [word] - List all apps or search by name/word"
3688
+ print_color "$YELLOW" " apps [word] - Same as listapps (search any installed app)"
3689
+ print_color "$YELLOW" " debug - Test device connection and tools"
3690
+ echo ""
3691
+ print_color "$CYAN" "🌐 Navigation:"
3692
+ print_color "$YELLOW" " home - Go to home screen"
3693
+ print_color "$YELLOW" " back [context] - Smart back navigation (button/swipe/context)"
3694
+ print_color "$YELLOW" " goback - Same as back"
3695
+ print_color "$YELLOW" " url <url> - Open URL in Safari"
3696
+ print_color "$YELLOW" " screenshot - Take screenshot"
3697
+ print_color "$YELLOW" " click <element> - Click on element or coordinates"
3698
+ print_color "$YELLOW" " longpress <el> - Long press on element"
3699
+ print_color "$YELLOW" " swipe <dir> - Swipe up/down/left/right or custom coordinates"
3700
+ print_color "$YELLOW" " scroll <dir> - Same as swipe"
3701
+ echo ""
3702
+ print_color "$CYAN" "⚙️ Device Control:"
3703
+ print_color "$YELLOW" " restart - Restart device (wireless + USB supported)"
3704
+ print_color "$YELLOW" " reboot - Same as restart"
3705
+ print_color "$YELLOW" " shutdown - Shutdown device (USB only, wireless does restart)"
3706
+ print_color "$YELLOW" " poweroff - Same as shutdown"
3707
+ echo ""
3708
+ print_color "$CYAN" "🎨 Display & Appearance:"
3709
+ print_color "$YELLOW" " darkmode - Switch to dark mode"
3710
+ print_color "$YELLOW" " lightmode - Switch to light mode"
3711
+ print_color "$YELLOW" " theme <mode> - Set theme (dark/light/toggle)"
3712
+ print_color "$YELLOW" " appearance - Check current appearance mode"
3713
+ print_color "$CYAN" " Note: May open Settings for manual selection"
3714
+ echo ""
3715
+ print_color "$CYAN" "🔄 Screen Rotation:"
3716
+ print_color "$YELLOW" " rotate - Toggle orientation (portrait/landscape)"
3717
+ print_color "$YELLOW" " rotate <orient> - Set orientation (portrait/landscape-left/landscape-right)"
3718
+ print_color "$YELLOW" " rotationlock - Toggle rotation lock (opens Control Center)"
3719
+ print_color "$YELLOW" " lockrotation - Toggle rotation lock (same as rotationlock)"
3720
+ print_color "$CYAN" " Note: May require manual interaction in Control Center"
3721
+ echo ""
3722
+ print_color "$CYAN" "📝 Text & Control:"
3723
+ print_color "$YELLOW" " any text - Send to active field"
3724
+ print_color "$YELLOW" " click <x,y> - Click at coordinates (e.g., click 100,200)"
3725
+ print_color "$YELLOW" " click <text> - Click on button/text element"
3726
+ print_color "$YELLOW" " tap <x,y> - Same as click"
3727
+ print_color "$YELLOW" " longpress <x,y> - Long press at coordinates (1.5s default)"
3728
+ print_color "$YELLOW" " longpress <text>- Long press on text element"
3729
+ print_color "$YELLOW" " longclick <x,y> - Same as longpress"
3730
+ print_color "$YELLOW" " help - Show this help"
3731
+ print_color "$YELLOW" " quit - Exit and cleanup (preserves device trust)"
3732
+ print_color "$YELLOW" " forcecleanup - Force kill WDA processes (may affect trust)"
3733
+ echo ""
3734
+ print_color "$CYAN" "💡 Examples:"
3735
+ print_color "$YELLOW" " launch safari"
3736
+ print_color "$YELLOW" " url google.com"
3737
+ print_color "$YELLOW" " click 200,300"
3738
+ print_color "$YELLOW" " click 'Sign In'"
3739
+ print_color "$YELLOW" " longpress 'Settings'"
3740
+ print_color "$YELLOW" " longpress 100,150,2.0"
3741
+ print_color "$YELLOW" " darkmode"
3742
+ print_color "$YELLOW" " rotate landscape-left"
3743
+ print_color "$YELLOW" " rotationlock"
3744
+ print_color "$YELLOW" " restart"
3745
+ print_color "$YELLOW" " shutdown"
3746
+ print_color "$YELLOW" " install /path/to/app.ipa"
3747
+ print_color "$YELLOW" " uninstall com.example.app"
3748
+ print_color "$YELLOW" " appinfo com.example.app"
3749
+ print_color "$YELLOW" " listapps mobileiron"
3750
+ print_color "$YELLOW" " Hello World"
3751
+ print_color "$YELLOW" " kill safari"
3752
+ continue
3753
+ ;;
3754
+ "")
3755
+ continue
3756
+ ;;
3757
+ *)
3758
+ execute_wda_command "$DEVICE_NAME_REQUEST" "$INPUT_TEXT"
3759
+ echo ""
3760
+ ;;
3761
+ esac
3762
+ done
3763
+ fi
3764
+ fi
3765
+ exit 0
3766
+ fi
3767
+
3768
+ # Multi-device mode
3769
+ discover_and_connect_devices
3770
+
3771
+ if [ -z "$TEXT" ]; then
3772
+ # Interactive mode - create persistent sessions for all devices in parallel
3773
+ print_color "$YELLOW" "🔧 Initializing persistent sessions..."
3774
+
3775
+ session_pids=()
3776
+ for device in "${CONNECTED_DEVICES[@]}"; do
3777
+ {
3778
+ session=$(create_persistent_session "$device")
3779
+ if [ -n "$session" ]; then
3780
+ print_color "$GREEN" " ✅ Session ready: $device"
3781
+ else
3782
+ print_color "$RED" " ❌ Session failed: $device"
3783
+ fi
3784
+ } &
3785
+ session_pids+=($!)
3786
+ done
3787
+
3788
+ # Wait for all session creations to complete
3789
+ for pid in "${session_pids[@]}"; do
3790
+ wait "$pid"
3791
+ done
3792
+
3793
+ echo ""
3794
+ # Load history at startup
3795
+ load_text_history
3796
+
3797
+ # Initialize device selection (start with all devices selected)
3798
+ SELECTED_DEVICES=("${CONNECTED_DEVICES[@]}")
3799
+
3800
+ print_color "$CYAN" "🔄 Multi-device Interactive Mode"
3801
+ print_color "$YELLOW" " Commands: launch/kill/home/url/screenshot + text input"
3802
+ print_color "$YELLOW" " Type 'help' for commands • 'select' for device targeting • 'quit' to exit"
3803
+ print_color "$CYAN" " Available: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
3804
+ print_color "$GREEN" " Active: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
3805
+ echo ""
3806
+
3807
+ while true; do
3808
+ # Enable readline for this input with custom history
3809
+ set -o history
3810
+ HISTFILE="$HISTORY_FILE"
3811
+ HISTSIZE="$HISTORY_MAX"
3812
+ HISTFILESIZE="$HISTORY_MAX"
3813
+
3814
+ # Load history from file for this session
3815
+ if [ -f "$HISTORY_FILE" ]; then
3816
+ history -r "$HISTORY_FILE"
3817
+ fi
3818
+
3819
+ read -e -p "Multi-device [${#SELECTED_DEVICES[@]}/${#CONNECTED_DEVICES[@]}] > " INPUT_TEXT
3820
+
3821
+ # Add any non-empty input to history (except quit commands)
3822
+ if [ -n "$INPUT_TEXT" ] && [[ "$INPUT_TEXT" != "quit" ]] && [[ "$INPUT_TEXT" != "exit" ]] && [[ "$INPUT_TEXT" != "q" ]] && [[ "$INPUT_TEXT" != "help" ]]; then
3823
+ add_to_history "$INPUT_TEXT"
3824
+ fi
3825
+
3826
+ # Disable history again to avoid shell command pollution
3827
+ set +o history
3828
+ unset HISTFILE
3829
+
3830
+ case "$INPUT_TEXT" in
3831
+ "quit"|"q"|"exit")
3832
+ print_color "$YELLOW" "👋 Goodbye!"
3833
+ cleanup_all_sessions
3834
+ exit 0
3835
+ ;;
3836
+ "forcecleanup"|"killwda")
3837
+ print_color "$YELLOW" "🛑 Force cleaning up - this may affect device trust..."
3838
+ cleanup_all_sessions_full
3839
+ print_color "$YELLOW" "👋 Goodbye!"
3840
+ exit 0
3841
+ ;;
3842
+ "help"|"commands")
3843
+ print_color "$YELLOW" "📱 Available Commands:"
3844
+ echo ""
3845
+ print_color "$CYAN" "🚀 App Control:"
3846
+ print_color "$YELLOW" " launch <app> - Launch application"
3847
+ print_color "$YELLOW" " open <app> - Same as launch"
3848
+ print_color "$YELLOW" " kill <app> - Terminate application"
3849
+ print_color "$YELLOW" " close <app> - Same as kill"
3850
+ print_color "$YELLOW" " install <path> - Install IPA file (requires iOS tools)"
3851
+ print_color "$YELLOW" " uninstall <id> - Uninstall app by bundle ID (requires iOS tools)"
3852
+ print_color "$YELLOW" " appinfo <id> - Check app version/info by bundle ID"
3853
+ print_color "$YELLOW" " version <id> - Same as appinfo"
3854
+ print_color "$YELLOW" " listapps [word] - List all apps or search by name/word"
3855
+ print_color "$YELLOW" " apps [word] - Same as listapps (search any installed app)"
3856
+ print_color "$YELLOW" " debug - Test device connection and tools"
3857
+ echo ""
3858
+ print_color "$CYAN" "🌐 Navigation:"
3859
+ print_color "$YELLOW" " home - Go to home screen"
3860
+ print_color "$YELLOW" " back [context] - Smart back navigation (button/swipe/context)"
3861
+ print_color "$YELLOW" " goback - Same as back"
3862
+ print_color "$YELLOW" " url <url> - Open URL in Safari"
3863
+ print_color "$YELLOW" " screenshot - Take screenshot"
3864
+ print_color "$YELLOW" " click <element> - Click on element or coordinates"
3865
+ print_color "$YELLOW" " longpress <el> - Long press on element"
3866
+ print_color "$YELLOW" " swipe <dir> - Swipe up/down/left/right or custom coordinates"
3867
+ print_color "$YELLOW" " scroll <dir> - Same as swipe"
3868
+ echo ""
3869
+ print_color "$CYAN" "⚙️ Device Control:"
3870
+ print_color "$YELLOW" " restart - Restart device (wireless + USB supported)"
3871
+ print_color "$YELLOW" " reboot - Same as restart"
3872
+ print_color "$YELLOW" " shutdown - Shutdown device (USB only, wireless does restart)"
3873
+ print_color "$YELLOW" " poweroff - Same as shutdown"
3874
+ echo ""
3875
+ print_color "$CYAN" "🎨 Display & Appearance:"
3876
+ print_color "$YELLOW" " darkmode - Switch to dark mode"
3877
+ print_color "$YELLOW" " lightmode - Switch to light mode"
3878
+ print_color "$YELLOW" " theme <mode> - Set theme (dark/light/toggle)"
3879
+ print_color "$YELLOW" " appearance - Check current appearance mode"
3880
+ print_color "$CYAN" " Note: May open Settings for manual selection"
3881
+ echo ""
3882
+ print_color "$CYAN" "🔄 Screen Rotation:"
3883
+ print_color "$YELLOW" " rotate - Toggle orientation (portrait/landscape)"
3884
+ print_color "$YELLOW" " rotate <orient> - Set orientation (portrait/landscape-left/landscape-right)"
3885
+ print_color "$YELLOW" " rotationlock - Toggle rotation lock (opens Control Center)"
3886
+ print_color "$YELLOW" " lockrotation - Toggle rotation lock (same as rotationlock)"
3887
+ print_color "$CYAN" " Note: May require manual interaction in Control Center"
3888
+ echo ""
3889
+ print_color "$CYAN" "📝 Text & Control:"
3890
+ print_color "$YELLOW" " any text - Send to active field"
3891
+ print_color "$YELLOW" " click <x,y> - Click at coordinates (e.g., click 100,200)"
3892
+ print_color "$YELLOW" " click <text> - Click on button/text element"
3893
+ print_color "$YELLOW" " tap <x,y> - Same as click"
3894
+ print_color "$YELLOW" " longpress <x,y> - Long press at coordinates (1.5s default)"
3895
+ print_color "$YELLOW" " longpress <text>- Long press on text element"
3896
+ print_color "$YELLOW" " longclick <x,y> - Same as longpress"
3897
+ print_color "$YELLOW" " help - Show this help"
3898
+ print_color "$YELLOW" " quit - Exit and cleanup (preserves device trust)"
3899
+ print_color "$YELLOW" " forcecleanup - Force kill WDA processes (may affect trust)"
3900
+ echo ""
3901
+ print_color "$CYAN" "🎯 Device Targeting:"
3902
+ print_color "$YELLOW" " select - Choose which devices to target"
3903
+ print_color "$YELLOW" " -d <dev> <cmd> - Run command on specific device (e.g., -d ipad home)"
3904
+ print_color "$YELLOW" " status - Show connected devices"
3905
+ echo ""
3906
+ print_color "$CYAN" "💡 Examples:"
3907
+ print_color "$YELLOW" " launch safari"
3908
+ print_color "$YELLOW" " -d ipad home # Run home only on ipad"
3909
+ print_color "$YELLOW" " -d iphone16p getLocators # Get locators only from iphone16p"
3910
+ print_color "$YELLOW" " -d ipad click Submit # Click only on ipad"
3911
+ print_color "$YELLOW" " url google.com"
3912
+ print_color "$YELLOW" " click 200,300"
3913
+ print_color "$YELLOW" " click 'Sign In'"
3914
+ print_color "$YELLOW" " longpress 'Settings'"
3915
+ print_color "$YELLOW" " longpress 100,150,2.0"
3916
+ print_color "$YELLOW" " darkmode"
3917
+ print_color "$YELLOW" " rotate landscape-left"
3918
+ print_color "$YELLOW" " rotationlock"
3919
+ print_color "$YELLOW" " restart"
3920
+ print_color "$YELLOW" " shutdown"
3921
+ print_color "$YELLOW" " install /path/to/app.ipa"
3922
+ print_color "$YELLOW" " uninstall com.example.app"
3923
+ print_color "$YELLOW" " appinfo com.example.app"
3924
+ print_color "$YELLOW" " listapps mobileiron"
3925
+ print_color "$YELLOW" " Hello World"
3926
+ print_color "$YELLOW" " kill safari"
3927
+ continue
3928
+ ;;
3929
+ "status"|"devices")
3930
+ print_color "$CYAN" "📱 Connected: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
3931
+ continue
3932
+ ;;
3933
+ "")
3934
+ continue
3935
+ ;;
3936
+ "select"|"devices"|"target")
3937
+ print_color "$CYAN" "📱 Device Selection Menu:"
3938
+ echo ""
3939
+ print_color "$YELLOW" "Available devices:"
3940
+ for i in "${!CONNECTED_DEVICES[@]}"; do
3941
+ device="${CONNECTED_DEVICES[$i]}"
3942
+ is_selected=""
3943
+ if [[ " ${SELECTED_DEVICES[*]} " == *" $device "* ]]; then
3944
+ is_selected="✅"
3945
+ else
3946
+ is_selected="⬜"
3947
+ fi
3948
+ echo " $((i+1)). $is_selected $device"
3949
+ done
3950
+ echo ""
3951
+ print_color "$CYAN" "Commands:"
3952
+ print_color "$YELLOW" " all - Select all devices"
3953
+ print_color "$YELLOW" " none - Deselect all devices"
3954
+ print_color "$YELLOW" " 1,2,3 - Select devices by numbers"
3955
+ print_color "$YELLOW" " +1,2 - Add devices to selection"
3956
+ print_color "$YELLOW" " -1,2 - Remove devices from selection"
3957
+ print_color "$YELLOW" " toggle 1,2 - Toggle device selection"
3958
+ print_color "$YELLOW" " list - Show current selection"
3959
+ print_color "$YELLOW" " done - Finish selection"
3960
+ echo ""
3961
+
3962
+ while true; do
3963
+ read -e -p "Device Selection > " DEVICE_CMD
3964
+
3965
+ case "$DEVICE_CMD" in
3966
+ "done"|"exit"|"")
3967
+ break
3968
+ ;;
3969
+ "all")
3970
+ SELECTED_DEVICES=("${CONNECTED_DEVICES[@]}")
3971
+ print_color "$GREEN" "✅ All devices selected: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
3972
+ ;;
3973
+ "none"|"clear")
3974
+ SELECTED_DEVICES=()
3975
+ print_color "$YELLOW" "🔄 All devices deselected"
3976
+ ;;
3977
+ "list"|"show")
3978
+ if [ ${#SELECTED_DEVICES[@]} -eq 0 ]; then
3979
+ print_color "$RED" "❌ No devices selected"
3980
+ else
3981
+ print_color "$GREEN" "✅ Selected: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
3982
+ fi
3983
+ ;;
3984
+ +*)
3985
+ # Add devices
3986
+ add_nums="${DEVICE_CMD:1}"
3987
+ IFS=',' read -ra NUMS <<< "$add_nums"
3988
+ for num in "${NUMS[@]}"; do
3989
+ num=$(echo "$num" | xargs) # trim whitespace
3990
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le ${#CONNECTED_DEVICES[@]} ]; then
3991
+ device="${CONNECTED_DEVICES[$((num-1))]}"
3992
+ if [[ ! " ${SELECTED_DEVICES[*]} " == *" $device "* ]]; then
3993
+ SELECTED_DEVICES+=("$device")
3994
+ print_color "$GREEN" "✅ Added: $device"
3995
+ fi
3996
+ fi
3997
+ done
3998
+ ;;
3999
+ -*)
4000
+ # Remove devices
4001
+ remove_nums="${DEVICE_CMD:1}"
4002
+ IFS=',' read -ra NUMS <<< "$remove_nums"
4003
+ for num in "${NUMS[@]}"; do
4004
+ num=$(echo "$num" | xargs)
4005
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le ${#CONNECTED_DEVICES[@]} ]; then
4006
+ device="${CONNECTED_DEVICES[$((num-1))]}"
4007
+ new_selected=()
4008
+ for selected in "${SELECTED_DEVICES[@]}"; do
4009
+ if [ "$selected" != "$device" ]; then
4010
+ new_selected+=("$selected")
4011
+ fi
4012
+ done
4013
+ SELECTED_DEVICES=("${new_selected[@]}")
4014
+ print_color "$YELLOW" "➖ Removed: $device"
4015
+ fi
4016
+ done
4017
+ ;;
4018
+ toggle*)
4019
+ # Toggle devices
4020
+ toggle_nums="${DEVICE_CMD#toggle }"
4021
+ IFS=',' read -ra NUMS <<< "$toggle_nums"
4022
+ for num in "${NUMS[@]}"; do
4023
+ num=$(echo "$num" | xargs)
4024
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le ${#CONNECTED_DEVICES[@]} ]; then
4025
+ device="${CONNECTED_DEVICES[$((num-1))]}"
4026
+ if [[ " ${SELECTED_DEVICES[*]} " == *" $device "* ]]; then
4027
+ # Remove from selection
4028
+ new_selected=()
4029
+ for selected in "${SELECTED_DEVICES[@]}"; do
4030
+ if [ "$selected" != "$device" ]; then
4031
+ new_selected+=("$selected")
4032
+ fi
4033
+ done
4034
+ SELECTED_DEVICES=("${new_selected[@]}")
4035
+ print_color "$YELLOW" "⬜ Deselected: $device"
4036
+ else
4037
+ # Add to selection
4038
+ SELECTED_DEVICES+=("$device")
4039
+ print_color "$GREEN" "✅ Selected: $device"
4040
+ fi
4041
+ fi
4042
+ done
4043
+ ;;
4044
+ *)
4045
+ # Direct number selection (replace existing selection)
4046
+ if [[ "$DEVICE_CMD" =~ ^[0-9,[:space:]]+$ ]]; then
4047
+ SELECTED_DEVICES=()
4048
+ IFS=',' read -ra NUMS <<< "$DEVICE_CMD"
4049
+ for num in "${NUMS[@]}"; do
4050
+ num=$(echo "$num" | xargs)
4051
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le ${#CONNECTED_DEVICES[@]} ]; then
4052
+ device="${CONNECTED_DEVICES[$((num-1))]}"
4053
+ if [[ ! " ${SELECTED_DEVICES[*]} " == *" $device "* ]]; then
4054
+ SELECTED_DEVICES+=("$device")
4055
+ fi
4056
+ fi
4057
+ done
4058
+ if [ ${#SELECTED_DEVICES[@]} -gt 0 ]; then
4059
+ print_color "$GREEN" "✅ Selected: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
4060
+ else
4061
+ print_color "$RED" "❌ No valid device numbers provided"
4062
+ fi
4063
+ else
4064
+ print_color "$RED" "❌ Unknown command: $DEVICE_CMD"
4065
+ fi
4066
+ ;;
4067
+ esac
4068
+ done
4069
+
4070
+ # Update the prompt to show selected devices
4071
+ if [ ${#SELECTED_DEVICES[@]} -eq 0 ]; then
4072
+ SELECTED_DEVICES=("${CONNECTED_DEVICES[@]}")
4073
+ print_color "$YELLOW" "⚠️ No devices selected, using all devices"
4074
+ fi
4075
+
4076
+ print_color "$CYAN" "🎯 Active devices: $(IFS=', '; echo "${SELECTED_DEVICES[*]}")"
4077
+ continue
4078
+ ;;
4079
+ *)
4080
+ # Check if this is a runtime device selection: -d devicename command
4081
+ if [[ "$INPUT_TEXT" =~ ^-d[[:space:]]+([^[:space:]]+)[[:space:]]+(.+)$ ]]; then
4082
+ runtime_device="${BASH_REMATCH[1]}"
4083
+ runtime_command="${BASH_REMATCH[2]}"
4084
+
4085
+ # Validate device exists in connected devices
4086
+ device_found=false
4087
+ for device in "${CONNECTED_DEVICES[@]}"; do
4088
+ if [ "$device" = "$runtime_device" ]; then
4089
+ device_found=true
4090
+ break
4091
+ fi
4092
+ done
4093
+
4094
+ if [ "$device_found" = true ]; then
4095
+ print_color "$YELLOW" "🎯 Executing on single device: $runtime_device"
4096
+ print_color "$CYAN" " Command: $runtime_command"
4097
+ echo ""
4098
+
4099
+ execute_wda_command "$runtime_device" "$runtime_command"
4100
+ echo ""
4101
+ print_color "$GREEN" "✅ Completed on $runtime_device"
4102
+ else
4103
+ print_color "$RED" "❌ Device '$runtime_device' not found in connected devices"
4104
+ print_color "$YELLOW" " Available devices: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
4105
+ fi
4106
+ echo ""
4107
+ continue
4108
+ fi
4109
+
4110
+ # Check if this is a command (starts with known command keywords)
4111
+ first_word="${INPUT_TEXT%% *}" # More reliable word extraction
4112
+
4113
+ case "$first_word" in
4114
+ "launch"|"open"|"kill"|"close"|"home"|"url"|"screenshot"|"restart"|"reboot"|"shutdown"|"poweroff"|"darkmode"|"lightmode"|"theme"|"appearance"|"rotate"|"rotationlock"|"lockrotation"|"install"|"uninstall"|"appinfo"|"version"|"listapps"|"apps"|"debug"|"click"|"tap"|"longpress"|"longclick"|"back"|"goback"|"swipe"|"scroll"|"getLocators"|"getlocators"|"locators")
4115
+ # This is a command - send to execute_wda_command for each device
4116
+ print_color "$YELLOW" "🚀 Executing command on ${#SELECTED_DEVICES[@]} device(s): $INPUT_TEXT"
4117
+ echo ""
4118
+
4119
+ pids=()
4120
+ for device in "${SELECTED_DEVICES[@]}"; do
4121
+ {
4122
+ execute_wda_command "$device" "$INPUT_TEXT"
4123
+ echo "[${device}] Completed"
4124
+ } &
4125
+ pids+=($!)
4126
+ print_color "$CYAN" "📱 Started: $device (PID: $!)"
4127
+ done
4128
+
4129
+ echo ""
4130
+ print_color "$YELLOW" "⏳ Waiting for completion..."
4131
+
4132
+ for pid in "${pids[@]}"; do
4133
+ wait "$pid"
4134
+ done
4135
+
4136
+ echo ""
4137
+ print_color "$GREEN" "✅ All devices completed!"
4138
+ ;;
4139
+ *)
4140
+ # This is regular text - send as text input
4141
+ TEXT="$INPUT_TEXT"
4142
+ # Use selected devices instead of all devices
4143
+ if [ ${#SELECTED_DEVICES[@]} -eq 0 ]; then
4144
+ send_to_all_devices "${CONNECTED_DEVICES[@]}"
4145
+ else
4146
+ send_to_all_devices "${SELECTED_DEVICES[@]}"
4147
+ fi
4148
+ TEXT=""
4149
+ ;;
4150
+ esac
4151
+ echo ""
4152
+ ;;
4153
+ esac
4154
+ done
4155
+ else
4156
+ # Single command mode - create temporary sessions
4157
+ print_color "$YELLOW" "🎯 Command: $TEXT"
4158
+ print_color "$CYAN" " Targets: $(IFS=', '; echo "${CONNECTED_DEVICES[*]}")"
4159
+ echo ""
4160
+
4161
+ # Create sessions for all devices
4162
+ for device in "${CONNECTED_DEVICES[@]}"; do
4163
+ create_persistent_session "$device" >/dev/null
4164
+ done
4165
+
4166
+ send_to_all_devices "${CONNECTED_DEVICES[@]}"
4167
+ fi