devicely 2.1.5 → 2.1.6

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.
@@ -1,848 +0,0 @@
1
- #!/bin/bash
2
- # Android Device Control Script
3
- # Complete feature parity with iOS control
4
- # Uses UIAutomator2 for automation
5
-
6
- # Configuration
7
- UIAUTOMATOR2_PORT_BASE=6790
8
- SCRIPT_DIR="$(dirname "$0")"
9
-
10
- # Function to find config files in multiple locations
11
- find_config() {
12
- local filename="$1"
13
- local search_paths=(
14
- "$SCRIPT_DIR/$filename"
15
- "$SCRIPT_DIR/../../config/$filename"
16
- "$SCRIPT_DIR/../config/$filename"
17
- "$HOME/.devicely/$filename"
18
- "/opt/homebrew/lib/node_modules/devicely/config/$filename"
19
- "$(npm root -g 2>/dev/null)/devicely/config/$filename"
20
- )
21
-
22
- for path in "${search_paths[@]}"; do
23
- if [ -f "$path" ]; then
24
- echo "$path"
25
- return 0
26
- fi
27
- done
28
-
29
- return 1
30
- }
31
-
32
- # Find config files
33
- CONFIG_FILE=$(find_config "devices.conf")
34
- if [ -z "$CONFIG_FILE" ]; then
35
- # Fallback to user home directory (created by postinstall)
36
- if [ -f "$HOME/.devicely/devices.conf" ]; then
37
- CONFIG_FILE="$HOME/.devicely/devices.conf"
38
- else
39
- echo "❌ Required file not found: devices.conf"
40
- echo " Searched in: script dir, config/, ~/.devicely/, global npm"
41
- exit 1
42
- fi
43
- fi
44
-
45
- APPS_PRESETS_FILE=$(find_config "apps_presets.conf")
46
- if [ -z "$APPS_PRESETS_FILE" ]; then
47
- if [ -f "$HOME/.devicely/apps_presets.conf" ]; then
48
- APPS_PRESETS_FILE="$HOME/.devicely/apps_presets.conf"
49
- else
50
- echo "❌ Required file not found: apps_presets.conf"
51
- echo " Searched in: script dir, config/, ~/.devicely/, global npm"
52
- exit 1
53
- fi
54
- fi
55
-
56
- # Session management (managed by backend server)
57
- SCRIPT_DIR_ABS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
58
- # Use OS temp directory for session file - works for both local and npm package
59
- if [[ "$OSTYPE" == "darwin"* ]]; then
60
- SESSION_MAP_FILE="$TMPDIR/devicely_android_sessions.map"
61
- else
62
- SESSION_MAP_FILE="/tmp/devicely_android_sessions.map"
63
- fi
64
- ELEMENT_CACHE_FILE="/tmp/android_elements_cache.$$"
65
- DEVICE_PORTS_FILE="/tmp/android_multi_ports"
66
-
67
- # Color codes for output
68
- GREEN='\033[0;32m'
69
- RED='\033[0;31m'
70
- YELLOW='\033[1;33m'
71
- BLUE='\033[0;34m'
72
- NC='\033[0m' # No Color
73
-
74
- # Cleanup on exit
75
- cleanup() {
76
- [ "$DEBUG" = "1" ] && echo -e "${YELLOW}Cleaning up Android connections...${NC}"
77
- # Don't kill port forwards - keep UIAutomator2 server running for performance
78
- # if [ -f "$DEVICE_PORTS_FILE" ]; then
79
- # while IFS=: read -r serial port; do
80
- # adb -s "$serial" forward --remove tcp:$port 2>/dev/null
81
- # done < "$DEVICE_PORTS_FILE"
82
- # fi
83
- # Don't remove session map - reuse sessions for better performance
84
- # rm -f "$SESSION_MAP_FILE" "$ELEMENT_CACHE_FILE" "$DEVICE_PORTS_FILE" 2>/dev/null
85
- }
86
- trap cleanup EXIT INT TERM
87
-
88
- # Check if UIAutomator2 session is available (managed by backend server)
89
- init_uiautomator2() {
90
- local serial=$1
91
- local port=${2:-8200} # Default port if not specified
92
-
93
- # Check if device has active session from backend server
94
- if [ -f "$SESSION_MAP_FILE" ]; then
95
- local session_info=$(grep "^$serial:" "$SESSION_MAP_FILE" 2>/dev/null | tail -1)
96
- if [ -n "$session_info" ]; then
97
- # Format: udid:sessionId:port
98
- # For IP:PORT format (100.87.246.132:5555:sessionId:port), we need the last field
99
- local stored_port=$(echo "$session_info" | awk -F':' '{print $NF}')
100
-
101
- # Verify port is numeric
102
- if [[ "$stored_port" =~ ^[0-9]+$ ]]; then
103
- echo -e "${GREEN}✓ Using UIAutomator2 session (port $stored_port)${NC}"
104
- return 0
105
- else
106
- echo -e "${YELLOW}⚠️ Invalid session format in file${NC}"
107
- fi
108
- fi
109
- fi
110
-
111
- # No session found
112
- echo -e "${RED}❌ No UIAutomator2 session found for $serial${NC}"
113
- echo -e "${YELLOW}Please connect device first using the web UI 'Connect' button${NC}"
114
- echo -e "${YELLOW}The UIAutomator2 session will be automatically started when you connect.${NC}"
115
- return 1
116
- }
117
-
118
- # Get or create UIAutomator2 session
119
- get_session() {
120
- local serial=$1
121
- local port=$2
122
-
123
- # Check if session exists in persistent file (created by backend)
124
- if [ -f "$SESSION_MAP_FILE" ]; then
125
- local session_info=$(grep "^$serial:" "$SESSION_MAP_FILE" 2>/dev/null | tail -1)
126
- if [ -n "$session_info" ]; then
127
- # Format: udid:sessionId:port
128
- # For IP:PORT udid (100.87.246.132:5555:sessionId:port), extract last 2 fields
129
- local stored_port=$(echo "$session_info" | awk -F':' '{print $NF}')
130
- local session_id=$(echo "$session_info" | awk -F':' '{print $(NF-1)}')
131
-
132
- # Verify port is numeric
133
- if [[ ! "$stored_port" =~ ^[0-9]+$ ]]; then
134
- echo -e "${RED}❌ Invalid session format${NC}" >&2
135
- return 1
136
- fi
137
-
138
- # Use the port from the backend
139
- port=$stored_port
140
-
141
- # Verify session is still active
142
- if curl -s "http://localhost:$port/wd/hub/session/$session_id/status" &>/dev/null 2>&1; then
143
- echo "$session_id"
144
- return 0
145
- fi
146
- fi
147
- fi
148
-
149
- # No persistent session found - need to connect device first
150
- echo -e "${RED}No active session for $serial${NC}" >&2
151
- echo -e "${YELLOW}Please connect device first using the web UI 'Connect' button${NC}" >&2
152
- return 1
153
- }
154
-
155
- # Launch app
156
- launch_app() {
157
- local serial=$1
158
- local port=$2
159
- local package=$3
160
- local activity=$4
161
-
162
- echo -e "${BLUE}Launching $package...${NC}"
163
-
164
- # Use ADB for faster launch
165
- if [ -n "$activity" ]; then
166
- adb -s "$serial" shell am start -n "$package/$activity"
167
- else
168
- adb -s "$serial" shell monkey -p "$package" -c android.intent.category.LAUNCHER 1
169
- fi
170
-
171
- sleep 2
172
- echo -e "${GREEN}✓ App launched${NC}"
173
- }
174
-
175
- # Open URL in default browser
176
- open_url() {
177
- local serial=$1
178
- local url=$2
179
-
180
- # Add https:// if not present
181
- if ! echo "$url" | grep -qE '^[a-z]+://'; then
182
- url="https://$url"
183
- fi
184
-
185
- echo -e "${BLUE}Opening URL: $url${NC}"
186
- adb -s "$serial" shell am start -a android.intent.action.VIEW -d "$url"
187
- sleep 2
188
- echo -e "${GREEN}✓ URL opened${NC}"
189
- }
190
-
191
- # Close app
192
- close_app() {
193
- local serial=$1
194
- local package=$2
195
-
196
- echo -e "${BLUE}Closing $package...${NC}"
197
- adb -s "$serial" shell am force-stop "$package"
198
- echo -e "${GREEN}✓ App closed${NC}"
199
- }
200
-
201
- # Get UI hierarchy
202
- get_ui_hierarchy() {
203
- local serial=$1
204
- local port=$2
205
- local session_id=$3
206
-
207
- # Get page source from UIAutomator2
208
- curl -s "http://localhost:$port/wd/hub/session/$session_id/source" | python3 -c '
209
- import sys, json
210
- try:
211
- data = json.load(sys.stdin)
212
- print(data.get("value", ""))
213
- except:
214
- pass
215
- '
216
- }
217
-
218
- # Get locators from current screen
219
- get_locators() {
220
- local serial=$1
221
-
222
- [ "$DEBUG" = "1" ] && echo "🔍 Getting all locators from current screen..."
223
-
224
- # Get port from session file
225
- local port=""
226
- if [ -f "$SESSION_MAP_FILE" ]; then
227
- local session_info=$(grep "^$serial:" "$SESSION_MAP_FILE" 2>/dev/null | tail -1)
228
- if [ -n "$session_info" ]; then
229
- port=$(echo "$session_info" | awk -F':' '{print $NF}')
230
- fi
231
- fi
232
-
233
- if [ -z "$port" ]; then
234
- [ "$DEBUG" = "1" ] && echo -e "${RED}❌ No active session found${NC}"
235
- return 1
236
- fi
237
-
238
- # Get existing session ID
239
- local session_id=$(get_session "$serial" "$port")
240
-
241
- if [ -z "$session_id" ] || [ "$session_id" == "None" ]; then
242
- [ "$DEBUG" = "1" ] && echo -e "${RED}❌ No active UIAutomator2 session${NC}"
243
- return 1
244
- fi
245
-
246
- [ "$DEBUG" = "1" ] && echo "📱 Using session: $session_id (port: $port)"
247
- [ "$DEBUG" = "1" ] && echo "📱 Fetching page source..."
248
-
249
- # Get page source
250
- local page_source=$(curl -s "http://localhost:$port/wd/hub/session/$session_id/source" 2>/dev/null)
251
-
252
- if [ -n "$page_source" ] && ! echo "$page_source" | grep -q '"error"'; then
253
- [ "$DEBUG" = "1" ] && echo "🔍 Extracting locators..."
254
-
255
- # Save page source to temp file for Python to read
256
- local temp_source="/tmp/android_source_$$.json"
257
- echo "$page_source" > "$temp_source"
258
-
259
- # Extract all locator information using Python
260
- python3 << PYTHON_SCRIPT
261
- import sys
262
- import json
263
- import xml.etree.ElementTree as ET
264
-
265
- try:
266
- with open('$temp_source') as f:
267
- data = json.load(f)
268
- xml_str = data.get('value', '')
269
-
270
- if not xml_str or len(xml_str) < 10:
271
- print('\n❌ No page source found or empty XML')
272
- print('💡 This may be a Camera/Video/Game app that uses native rendering')
273
- print('💡 Try using coordinate-based tapping: tap X,Y')
274
- sys.exit(1)
275
-
276
- root = ET.fromstring(xml_str)
277
- locators = []
278
-
279
- # Extract locators from all elements
280
- for elem in root.iter():
281
- locator_info = {}
282
-
283
- # Get element class
284
- elem_class = elem.get('class', '')
285
- if elem_class:
286
- # Shorten Android class names
287
- elem_class = elem_class.split('.')[-1]
288
- locator_info['type'] = elem_class
289
-
290
- # Get text, content-desc, resource-id
291
- text = elem.get('text', '')
292
- content_desc = elem.get('content-desc', '')
293
- resource_id = elem.get('resource-id', '')
294
-
295
- # Extract just the ID part from resource-id
296
- if resource_id and ':id/' in resource_id:
297
- resource_id = resource_id.split(':id/')[-1]
298
-
299
- if text:
300
- locator_info['text'] = text
301
- if content_desc:
302
- locator_info['content-desc'] = content_desc
303
- if resource_id:
304
- locator_info['resource-id'] = resource_id
305
-
306
- # Get properties
307
- clickable = elem.get('clickable', 'false')
308
- enabled = elem.get('enabled', 'true')
309
- focusable = elem.get('focusable', 'false')
310
-
311
- locator_info['clickable'] = clickable
312
- locator_info['enabled'] = enabled
313
-
314
- # Get coordinates from bounds [x1,y1][x2,y2]
315
- bounds = elem.get('bounds', '')
316
- if bounds:
317
- import re
318
- match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
319
- if match:
320
- x1, y1, x2, y2 = map(int, match.groups())
321
- center_x = (x1 + x2) // 2
322
- center_y = (y1 + y2) // 2
323
- width = x2 - x1
324
- height = y2 - y1
325
- locator_info['bounds'] = f'({center_x},{center_y}) {width}x{height}'
326
-
327
- # Only add if we have meaningful locator information
328
- if text or content_desc or resource_id:
329
- locators.append(locator_info)
330
-
331
- # Print formatted output - only in DEBUG mode
332
- if locators:
333
- import os
334
- debug = os.environ.get('DEBUG', '0') == '1'
335
- if debug:
336
- print(f'\n📋 Found {len(locators)} interactive elements:\n', file=sys.stderr)
337
- print('=' * 120, file=sys.stderr)
338
-
339
- for i, loc in enumerate(locators, 1):
340
- if debug:
341
- # Build single line output with Name first
342
- parts = []
343
-
344
- # Start with number and primary identifier
345
- text = loc.get('text', '')
346
- desc = loc.get('content-desc', '')
347
- res_id = loc.get('resource-id', '')
348
-
349
- # Choose primary name (prefer text, then desc, then id)
350
- primary_name = text or desc or res_id or '(no name)'
351
- parts.append(f'{i:3d}. 📍 {primary_name:40s}')
352
-
353
- # Add Type
354
- elem_type = loc.get('type', 'Unknown')
355
- parts.append(f'[{elem_type:15s}]')
356
-
357
- # Add Label/Desc if different from primary name
358
- if desc and desc != primary_name:
359
- parts.append(f'Label:{desc[:30]}{"..." if len(desc) > 30 else ""}')
360
-
361
- # Add Value/Text if different from primary
362
- if text and text != primary_name and text != desc:
363
- parts.append(f'Val:{text[:20]}{"..." if len(text) > 20 else ""}')
364
-
365
- # Add resource ID if not primary
366
- if res_id and res_id != primary_name:
367
- parts.append(f'ID:{res_id}')
368
-
369
- # Add Position
370
- if loc.get('bounds'):
371
- parts.append(f'@ {loc["bounds"]}')
372
-
373
- # Add status flags
374
- flags = []
375
- if loc.get('enabled') == 'true':
376
- flags.append('✓En')
377
- if loc.get('clickable') == 'true':
378
- flags.append('✓Click')
379
- if flags:
380
- parts.append(' '.join(flags))
381
-
382
- print(' ' + ' │ '.join(parts), file=sys.stderr)
383
-
384
- if debug:
385
- print('=' * 120, file=sys.stderr)
386
-
387
- # Output JSON to stdout (always, regardless of DEBUG)
388
- import json
389
- print(json.dumps(locators))
390
-
391
- except ET.ParseError as e:
392
- import os
393
- debug = os.environ.get('DEBUG', '0') == '1'
394
- if debug:
395
- print(f'\n❌ Error parsing XML: {e}', file=sys.stderr)
396
- print('💡 The page source may be corrupted or in unexpected format', file=sys.stderr)
397
- sys.exit(1)
398
- except Exception as e:
399
- import os
400
- debug = os.environ.get('DEBUG', '0') == '1'
401
- if debug:
402
- print(f'\n❌ Error extracting locators: {e}', file=sys.stderr)
403
- sys.exit(1)
404
- PYTHON_SCRIPT
405
-
406
- local exit_code=$?
407
-
408
- # Cleanup temp file only (keep session alive for reuse)
409
- rm -f "$temp_source"
410
-
411
- if [ $exit_code -eq 0 ]; then
412
- [ "$DEBUG" = "1" ] && echo -e "${GREEN}✅ Locators extraction complete${NC}"
413
- return 0
414
- else
415
- echo -e "${YELLOW}⚠️ Locators extraction completed with warnings${NC}"
416
- return 0
417
- fi
418
- else
419
- echo -e "${RED}❌ Failed to get page source from device${NC}"
420
- echo -e "${YELLOW}💡 Possible reasons:${NC}"
421
- echo -e " • Camera/Video apps don't expose UI to accessibility"
422
- echo -e " • Screen may be locked"
423
- echo -e " • UIAutomator2 server not responding"
424
- echo ""
425
- echo -e "${BLUE}💡 Try:${NC}"
426
- echo -e " 1. screenshot - Take a screenshot"
427
- echo -e " 2. tap X,Y - Use coordinate-based tapping"
428
- echo -e " 3. home - Go to home screen first"
429
-
430
- # Cleanup session
431
- curl -s -X DELETE "http://localhost:$port/wd/hub/session/$session_id" > /dev/null 2>&1
432
- return 1
433
- fi
434
- }
435
-
436
- # Helper function to find element with UIAutomator2
437
- find_element_uia2() {
438
- local session_id=$1
439
- local port=$2
440
- local strategy=$3
441
- local selector=$4
442
- local temp_file="/tmp/uia2_find_$$.json"
443
-
444
- # Use correct JSON keys based on strategy type
445
- if [[ "$strategy" == "xpath" ]]; then
446
- # XPath uses 'using' and 'value'
447
- curl -s -X POST "http://localhost:$port/wd/hub/session/$session_id/element" \
448
- -H "Content-Type: application/json" \
449
- -d "{\"using\": \"$strategy\", \"value\": \"$selector\"}" \
450
- > "$temp_file" 2>/dev/null
451
- else
452
- # UIAutomator uses 'strategy' and 'selector'
453
- curl -s -X POST "http://localhost:$port/wd/hub/session/$session_id/element" \
454
- -H "Content-Type: application/json" \
455
- -d "{\"strategy\": \"$strategy\", \"selector\": \"$selector\"}" \
456
- > "$temp_file" 2>/dev/null
457
- fi
458
-
459
- local element_id=$(python3 -c "
460
- import json
461
- try:
462
- with open('$temp_file') as f:
463
- data = json.load(f)
464
- value = data.get('value', {})
465
- elem = value.get('element-6066-11e4-a52e-4f735466cecf') or value.get('ELEMENT')
466
- if elem:
467
- print(elem)
468
- except:
469
- pass
470
- " 2>/dev/null)
471
-
472
- rm -f "$temp_file"
473
- echo "$element_id"
474
- }
475
-
476
- # Click element by text or coordinates
477
- click_element() {
478
- local serial=$1
479
- local port=$2
480
- local target="$3"
481
-
482
- # Strip surrounding quotes if present (from UI commands like click "VPN")
483
- target="${target#\"}" # Remove leading "
484
- target="${target%\"}" # Remove trailing "
485
- target="${target#\'}" # Remove leading '
486
- target="${target%\'}" # Remove trailing '
487
-
488
- # Check if coordinates (x,y format)
489
- if [[ "$target" =~ ^[0-9]+,[0-9]+$ ]]; then
490
- local x=$(echo "$target" | cut -d',' -f1)
491
- local y=$(echo "$target" | cut -d',' -f2)
492
-
493
- echo -e "${BLUE}Clicking at coordinates ($x, $y) using ADB...${NC}"
494
- adb -s "$serial" shell input tap $x $y
495
- echo -e "${GREEN}✓ Click completed${NC}"
496
- return 0
497
- fi
498
-
499
- # For text-based clicking, we need UIAutomator2
500
- local session_id=$(get_session "$serial" "$port")
501
- if [ -z "$session_id" ]; then
502
- echo -e "${YELLOW}Tip: Use coordinates (x,y) to click without UIAutomator2${NC}"
503
- return 1
504
- fi
505
-
506
- # Find element by text
507
- echo -e "${BLUE}🔎 Accessibility search: '$target'${NC}"
508
-
509
- # Try multiple strategies (like Appium Inspector)
510
- local element_id=""
511
- local response=""
512
-
513
- # Strategy 1: Android UiAutomator text (MOST RELIABLE - what Appium uses)
514
- echo -e "${BLUE} Strategy 1: UiAutomator text()...${NC}"
515
- element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().text(\\\"$target\\\")")
516
- [ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.text() - Element: $element_id${NC}"
517
-
518
- # Strategy 2: UiAutomator textContains (partial match)
519
- if [ -z "$element_id" ]; then
520
- echo -e "${BLUE} Strategy 2: UiAutomator textContains()...${NC}"
521
- element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().textContains(\\\"$target\\\")")
522
- [ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.textContains()${NC}"
523
- fi
524
-
525
- # Strategy 3: UiAutomator description (content-desc)
526
- if [ -z "$element_id" ]; then
527
- echo -e "${BLUE} Strategy 3: UiAutomator description()...${NC}"
528
- element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().description(\\\"$target\\\")")
529
- [ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.description()${NC}"
530
- fi
531
-
532
- # Strategy 4: UiAutomator descriptionContains
533
- if [ -z "$element_id" ]; then
534
- echo -e "${BLUE} Strategy 4: UiAutomator descriptionContains()...${NC}"
535
- element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().descriptionContains(\\\"$target\\\")")
536
- [ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.descriptionContains()${NC}"
537
- fi
538
-
539
- # Strategy 5: UiAutomator resourceId
540
- if [ -z "$element_id" ]; then
541
- echo -e "${BLUE} Strategy 5: UiAutomator resourceId()...${NC}"
542
- element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().resourceIdMatches(\\\".*$target.*\\\")")
543
- [ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.resourceId()${NC}"
544
- fi
545
-
546
- # Strategy 6: XPath exact text (fallback)
547
- if [ -z "$element_id" ]; then
548
- echo -e "${BLUE} Strategy 6: XPath exact text...${NC}"
549
- element_id=$(find_element_uia2 "$session_id" "$port" "xpath" "//*[@text='$target']")
550
- [ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by XPath text${NC}"
551
- fi
552
-
553
- # Strategy 7: XPath content-desc
554
- if [ -z "$element_id" ]; then
555
- echo -e "${BLUE} Strategy 7: XPath content-desc...${NC}"
556
- element_id=$(find_element_uia2 "$session_id" "$port" "xpath" "//*[@content-desc='$target']")
557
- [ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by XPath content-desc${NC}"
558
- fi
559
-
560
- # Strategy 8: XPath partial text
561
- if [ -z "$element_id" ]; then
562
- echo -e "${BLUE} Strategy 8: XPath partial text...${NC}"
563
- element_id=$(find_element_uia2 "$session_id" "$port" "xpath" "//*[contains(@text, '$target')]")
564
- [ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by XPath partial text${NC}"
565
- fi
566
-
567
- if [ -z "$element_id" ]; then
568
- echo -e "${RED}✗ Element not found: $target${NC}"
569
- echo -e "${YELLOW}Tip: Use coordinates (x,y) to click specific locations${NC}"
570
- return 1
571
- fi
572
-
573
- echo -e "${GREEN}✓ Element found, clicking...${NC}"
574
- curl -s -X POST "http://localhost:$port/wd/hub/session/$session_id/element/$element_id/click" &>/dev/null
575
-
576
- echo -e "${GREEN}✓ Click completed${NC}"
577
- }
578
-
579
- # Type text
580
- type_text() {
581
- local serial=$1
582
- local text=$2
583
-
584
- echo -e "${BLUE}Typing: $text${NC}"
585
-
586
- # Use ADB directly for text input (no UIAutomator2 needed)
587
- # Replace spaces with %s for ADB compatibility
588
- local formatted_text=$(echo "$text" | sed 's/ /%s/g')
589
- adb -s "$serial" shell input text "$formatted_text"
590
-
591
- echo -e "${GREEN}✓ Text entered${NC}"
592
- }
593
-
594
- # Swipe gesture
595
- swipe() {
596
- local serial=$1
597
- local direction=$2
598
-
599
- echo -e "${BLUE}Swiping $direction...${NC}"
600
-
601
- # Get screen size using ADB
602
- local screen_size=$(adb -s "$serial" shell wm size | grep -o '[0-9]*x[0-9]*')
603
- local width=$(echo "$screen_size" | cut -d'x' -f1)
604
- local height=$(echo "$screen_size" | cut -d'x' -f2)
605
-
606
- local x1=$((width / 2))
607
- local y1=$((height / 2))
608
- local x2=$x1
609
- local y2=$y1
610
-
611
- case "$direction" in
612
- up)
613
- y1=$((height * 3 / 4))
614
- y2=$((height / 4))
615
- ;;
616
- down)
617
- y1=$((height / 4))
618
- y2=$((height * 3 / 4))
619
- ;;
620
- left)
621
- x1=$((width * 3 / 4))
622
- x2=$((width / 4))
623
- ;;
624
- right)
625
- x1=$((width / 4))
626
- x2=$((width * 3 / 4))
627
- ;;
628
- esac
629
-
630
- adb -s "$serial" shell input swipe $x1 $y1 $x2 $y2 300
631
- echo -e "${GREEN}✓ Swipe completed${NC}"
632
- }
633
-
634
- # Screenshot
635
- take_screenshot() {
636
- local serial=$1
637
- # Save to webapp screenshots directory
638
- local screenshots_dir="$SCRIPT_DIR/webapp/backend/screenshots"
639
- mkdir -p "$screenshots_dir"
640
- local output="${screenshots_dir}/screenshot_android_${serial}_$(date +%Y%m%d_%H%M%S).png"
641
-
642
- echo -e "${BLUE}Taking screenshot...${NC}"
643
- adb -s "$serial" exec-out screencap -p > "$output"
644
- echo -e "${GREEN}✓ Screenshot saved: $output${NC}"
645
- }
646
-
647
- # Home button
648
- press_home() {
649
- local serial=$1
650
- echo -e "${BLUE}Pressing Home button...${NC}"
651
- adb -s "$serial" shell input keyevent KEYCODE_HOME
652
- echo -e "${GREEN}✓ Home pressed${NC}"
653
- }
654
-
655
- # Back button
656
- press_back() {
657
- local serial=$1
658
- echo -e "${BLUE}Pressing Back button...${NC}"
659
- adb -s "$serial" shell input keyevent KEYCODE_BACK
660
- echo -e "${GREEN}✓ Back pressed${NC}"
661
- }
662
-
663
- # Lock device
664
- lock_device() {
665
- local serial=$1
666
- echo -e "${BLUE}Locking device...${NC}"
667
- adb -s "$serial" shell input keyevent KEYCODE_POWER
668
- echo -e "${GREEN}✓ Device locked${NC}"
669
- }
670
-
671
- # Unlock device
672
- unlock_device() {
673
- local serial=$1
674
- echo -e "${BLUE}Unlocking device...${NC}"
675
- adb -s "$serial" shell input keyevent KEYCODE_MENU
676
- echo -e "${GREEN}✓ Device unlocked (if screen on)${NC}"
677
- }
678
-
679
- # Restart device
680
- restart_device() {
681
- local serial=$1
682
- echo -e "${BLUE}Restarting device...${NC}"
683
- adb -s "$serial" reboot
684
- echo -e "${GREEN}✓ Restart command sent${NC}"
685
- }
686
-
687
- # Shutdown device
688
- shutdown_device() {
689
- local serial=$1
690
- echo -e "${BLUE}Shutting down device...${NC}"
691
- adb -s "$serial" shell reboot -p
692
- echo -e "${GREEN}✓ Shutdown command sent${NC}"
693
- }
694
-
695
- # Get device info
696
- get_device_info() {
697
- local serial=$1
698
-
699
- echo -e "${BLUE}Device Information:${NC}"
700
- echo "Serial: $serial"
701
- echo "Model: $(adb -s "$serial" shell getprop ro.product.model | tr -d '\r')"
702
- echo "Android: $(adb -s "$serial" shell getprop ro.build.version.release | tr -d '\r')"
703
- echo "SDK: $(adb -s "$serial" shell getprop ro.build.version.sdk | tr -d '\r')"
704
- echo "Battery: $(adb -s "$serial" shell dumpsys battery | grep level | cut -d':' -f2 | tr -d ' \r')%"
705
- }
706
-
707
- # List installed apps
708
- list_apps() {
709
- local serial=$1
710
- echo -e "${BLUE}Installed Apps:${NC}"
711
- adb -s "$serial" shell pm list packages -3 | sed 's/package://' | sort
712
- }
713
-
714
- # Main command processor
715
- process_command() {
716
- local serial=$1
717
- local port=$2
718
- local command=$3
719
- shift 3
720
- local args="$@"
721
-
722
- case "$command" in
723
- launch)
724
- launch_app "$serial" "$port" "$args"
725
- ;;
726
- close|kill)
727
- close_app "$serial" "$args"
728
- ;;
729
- url)
730
- open_url "$serial" "$args"
731
- ;;
732
- click|tap)
733
- click_element "$serial" "$port" "$args"
734
- ;;
735
- type)
736
- # Use ADB directly, no port needed
737
- type_text "$serial" "$args"
738
- ;;
739
- swipe)
740
- # Use ADB directly, no port needed
741
- swipe "$serial" "$args"
742
- ;;
743
- screenshot)
744
- take_screenshot "$serial" "$args"
745
- ;;
746
- getLocators)
747
- get_locators "$serial"
748
- ;;
749
- home)
750
- press_home "$serial"
751
- ;;
752
- back)
753
- press_back "$serial"
754
- ;;
755
- lock)
756
- lock_device "$serial"
757
- ;;
758
- unlock)
759
- unlock_device "$serial"
760
- ;;
761
- restart|reboot)
762
- restart_device "$serial"
763
- ;;
764
- shutdown|poweroff)
765
- shutdown_device "$serial"
766
- ;;
767
- info)
768
- get_device_info "$serial"
769
- ;;
770
- listapps)
771
- list_apps "$serial"
772
- ;;
773
- *)
774
- echo -e "${RED}Unknown command: $command${NC}"
775
- return 1
776
- ;;
777
- esac
778
- }
779
-
780
- # Main execution
781
- main() {
782
- if [ $# -lt 2 ]; then
783
- echo "Usage: $0 <device_serial> <command> [args...]"
784
- echo ""
785
- echo "Commands:"
786
- echo " launch <package> [activity] - Launch app"
787
- echo " close <package> - Close app"
788
- echo " url <url> - Open URL in browser"
789
- echo " click <text|x,y> - Click element"
790
- echo " type <text> - Type text"
791
- echo " swipe <up|down|left|right> - Swipe gesture"
792
- echo " screenshot [filename] - Take screenshot"
793
- echo " getLocators - Get UI elements"
794
- echo " home - Press home button"
795
- echo " back - Press back button"
796
- echo " lock - Lock device"
797
- echo " unlock - Unlock device"
798
- echo " restart - Restart device"
799
- echo " shutdown - Shutdown device"
800
- echo " info - Device information"
801
- echo " listapps - List installed apps"
802
- exit 1
803
- fi
804
-
805
- local serial=$1
806
- shift
807
-
808
- # Verify device is connected
809
- if ! adb devices | grep -q "$serial"; then
810
- echo -e "${RED}Device not found: $serial${NC}"
811
- echo "Available devices:"
812
- adb devices
813
- exit 1
814
- fi
815
-
816
- # Assign port and store mapping
817
- local port=$((UIAUTOMATOR2_PORT_BASE))
818
- echo "$serial:$port" >> "$DEVICE_PORTS_FILE"
819
-
820
- # Initialize UIAutomator2 ONLY if needed (for commands that require it)
821
- local command=$1
822
- local requires_uiautomator=false
823
-
824
- case "$command" in
825
- click|tap|getLocators)
826
- # Only require UIAutomator2 if NOT using coordinates
827
- if [[ "$command" == "getLocators" ]] || ! [[ "$2" =~ ^[0-9]+,[0-9]+$ ]]; then
828
- requires_uiautomator=true
829
- fi
830
- ;;
831
- esac
832
-
833
- if [ "$requires_uiautomator" = true ]; then
834
- echo -e "${YELLOW}Note: This command requires UIAutomator2${NC}"
835
- if ! init_uiautomator2 "$serial" "$port"; then
836
- echo -e "${YELLOW}Tip: Most commands (type, swipe, home, back, launch, screenshot) work without UIAutomator2${NC}"
837
- exit 1
838
- fi
839
- fi
840
-
841
- # Process command
842
- process_command "$serial" "$port" "$@"
843
- }
844
-
845
- # Run if not sourced
846
- if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
847
- main "$@"
848
- fi