-
Notifications
You must be signed in to change notification settings - Fork 48
Expand file tree
/
Copy pathcommon_tools
More file actions
641 lines (572 loc) · 20.9 KB
/
Copy pathcommon_tools
File metadata and controls
641 lines (572 loc) · 20.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
#!/bin/bash
# shellcheck disable=SC2034
##############################################################################
### VARs
SCRIPT_LOCATION=$(dirname "$0")
# Runtime state lives in the user's state dir, not inside the install location -
# this keeps the toolkit working when installed read-only (e.g. via Homebrew).
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/mobile-toolkit"
mkdir -p "$STATE_DIR" 2>/dev/null
LAST_CHECK_DATE_FILE="$STATE_DIR/last_check_date.txt"
TEMPORARY_FILE="/private/tmp/mobile-toolkit-cache"
REGEX_NUMBER='^[0-9]+$'
##############################################################################
### Android
android_check_connected(){
android_get_devices
#No device connected
if [ ${#DEVICES[@]} -eq 0 ]
then
echo "❌ No Android devices detected"
exit 1
fi
}
android_wait_for_device(){
check_adb_dependency
echo "⏳ Waiting for Android device..."
adb wait-for-any-device
android_get_devices
}
android_get_devices_auth_dump(){
DEVICES_DUMP="$STATE_DIR/adb_devices_dump.txt"
rm -f "$DEVICES_DUMP"
adb devices | grep -v "List" >> "$DEVICES_DUMP"
if cat "$DEVICES_DUMP" | grep -q unauthorized ; then
read -r -p $'🚨 Unauthorized Android device detected!\n🔌 Reconnect it, allow USB debugging and press ENTER...'
android_get_devices_auth_dump
fi
if cat "$DEVICES_DUMP" | grep -q offline ; then
read -r -p $'🚨 Offline Android device detected!\n🔌 Wait until the startup is complete, then press ENTER...'
android_get_devices_auth_dump
fi
}
android_get_devices(){
#Populate array with device ids
DEVICES=()
android_get_devices_auth_dump
for LINE in $(cat "$DEVICES_DUMP" | awk '{print $1}')
do
DEVICE=$(echo "$LINE" | awk '{print $1}')
DEVICES+=("$DEVICE")
done
}
android_get_device_sdk(){
SDK=$(adb -s "$1" shell getprop ro.build.version.sdk | tr -cd '[[:alnum:]]._-')
}
android_device_info(){
MANUFACTURER=$(adb -s "$1" shell getprop ro.product.manufacturer | tr -cd '[[:alnum:]]._-')
MODEL=$(adb -s "$1" shell getprop ro.product.model | tr -cd '[[:alnum:]]._-')
VERSION=$(adb -s "$1" shell getprop ro.build.version.release | tr -cd '[[:alnum:]]._-')
SDK=$(adb -s "$1" shell getprop ro.build.version.sdk | tr -cd '[[:alnum:]]._-')
INFO=$(printf "%s) %s %s %s (API %s) - %s" "$NUMBER" "$MANUFACTURER" "$MODEL" "$VERSION" "$SDK" "$1")
}
android_choose_device() {
check_for_update
check_adb_dependency
android_check_connected
#Gather device info and choose device
if [ ${#DEVICES[@]} -gt 1 ]
then
NUMBER=1
echo "📱 Available devices:"
for ID in "${DEVICES[@]}"
do
android_device_info "$ID"
echo "$INFO"
((NUMBER++))
done
read -r -p "📝 Select a device: " CHOICE
while :;
do
if [[ ! $CHOICE =~ $REGEX_NUMBER ]] || [ "$CHOICE" -le "0" -o "$CHOICE" -gt "${#DEVICES[@]}" ]; then
echo -en "\033[1A\033[2K" #deletes last echoed line in terminal
read -r -p "🤷 Invalid input, try again: " CHOICE
else
break
fi
done
SELECTED_DEVICE=${DEVICES[(($CHOICE-1))]}
else
SELECTED_DEVICE="${DEVICES[0]}"
fi
SELECTED_DEVICE_MODEL=$(adb -s "$SELECTED_DEVICE" shell getprop ro.product.model | tr -cd '[[:alnum:]]._-')
SELECTED_DEVICE_SDK=$(adb -s "$SELECTED_DEVICE" shell getprop ro.build.version.sdk | tr -cd '[[:alnum:]]._-')
}
android_device_unlocked(){
echo "📱 Checking screen status..."
adb -s "$1" shell dumpsys power | grep "mWakefulness=" | grep "Awake" &> /dev/null
return $?
}
android_get_foreground_package(){
# Print the package name of the foreground app. Extracts "package/activity"
# via regex (not positional cut), which works uniformly from API 21 to 36 -
# the old per-SDK "dumpsys activity recents" field-counting broke whenever the
# Task{...} format shifted (e.g. it returned "cz.app}" with a trailing brace).
# Primary source: the resumed activity; fallback: the focused window.
local PKG
PKG=$(adb -s "$SELECTED_DEVICE" shell dumpsys activity activities 2>/dev/null \
| grep -E 'mResumedActivity|topResumedActivity' \
| grep -oE '[a-zA-Z0-9_.]+/[a-zA-Z0-9_.$]+' | head -1 | cut -d/ -f1)
if [ -z "$PKG" ]; then
PKG=$(adb -s "$SELECTED_DEVICE" shell dumpsys window displays 2>/dev/null \
| grep 'mCurrentFocus' | grep -v 'null' \
| grep -oE '[a-zA-Z0-9_.]+/[a-zA-Z0-9_.$]+' | head -1 | cut -d/ -f1)
fi
echo "$PKG"
}
android_get_storage_location_per_SDK(){
if (( "$1" < 30 )); then
DEVICE_FILE_PATH="/mnt/sdcard"
else
DEVICE_FILE_PATH="/storage/self/primary"
fi
}
android_is_package_installed() {
adb -s "$SELECTED_DEVICE" shell pm list packages -f | sed -e 's/.*=//' | grep -w "$1" &> /dev/null
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "🤷 Package \"$1\" is not installed"
exit 1
fi
}
android_detect_package_info(){
echo "🔍 Detecting package name..."
AAPPT_PATH=$(find ~/Library/Android/sdk -name 'aapt' | sort | tail -1)
"$AAPPT_PATH" dump badging "$1" > "$TEMPORARY_FILE"
PACKAGE_NAME=$(cat "$TEMPORARY_FILE" | grep package:\ name);
PACKAGE_NAME=$(echo "$PACKAGE_NAME" | sed 's/^[^'\'']*'\''//');
PACKAGE_NAME=$(echo "$PACKAGE_NAME" | sed 's/'\''.*//');
APP_NAME=$(cat "$TEMPORARY_FILE" | grep application-label:)
APP_NAME=${APP_NAME#"application-label:"}
}
android_unlock_device(){
# arg1 = DEVICE_ID
# arg2 = MAX_RETRIES
MAX_RETRIES="$2"
UNLOCK_RETRIES=1
until android_device_unlocked "$1";
do
if [ "$UNLOCK_RETRIES" -le "$MAX_RETRIES" ]; then
echo "🔆 Screen on attempt $UNLOCK_RETRIES..."
((UNLOCK_RETRIES++));
adb -s "$1" shell input keyevent KEYCODE_POWER
adb -s "$1" shell input keyevent 82
sleep 1;
else
read -r -p "❌ Screen-wake failed, press ANY KEY after manual unlock..."
break
fi
done
delete_lastline
echo "📱 Screen unlocked..."
}
##############################################################################
### iOS
# go-ios is pinned to a released version (binary download with checksum verification)
# instead of an unpinned source build - see https://github.com/danielpaulus/go-ios/releases
GO_IOS_PINNED_VERSION="1.2.0"
GO_IOS_MAC_ZIP_SHA256="bdf5d388b85e106915ebc81bd7bd068cb635a5ae212479d13da1852b0b090613"
check_go_ios_version(){
if ! [ -x "$(command -v "go-ios")" ]; then
install_go_ios || abort "❌ go-ios is required but could not be installed"
return
fi
# Offer the pinned version at most once per pin, and only on an interactive terminal
GO_IOS_OFFERED_FILE="$STATE_DIR/go_ios_offered.txt"
if ! [ -t 0 ] || { [ -f "$GO_IOS_OFFERED_FILE" ] && [ "$(cat "$GO_IOS_OFFERED_FILE")" == "$GO_IOS_PINNED_VERSION" ]; }; then
return
fi
GO_IOS_VERSION=$(go-ios --version 2>/dev/null | tail -1 | sed 's/.*"version":"\([^"]*\)".*/\1/')
if [ "$GO_IOS_VERSION" != "$GO_IOS_PINNED_VERSION" ]; then
yes_or_no "🆕 This toolkit version is tested with go-ios $GO_IOS_PINNED_VERSION (installed: ${GO_IOS_VERSION:-unknown}), install it now?"
echo "$GO_IOS_PINNED_VERSION" > "$GO_IOS_OFFERED_FILE"
if [[ "$RESPONSE" == "y" || "$RESPONSE" == "Y" ]]; then
if install_go_ios; then
TOOLKIT_IOS_LOCATION=$(dirname "$0")
RESOLVED_GO_IOS=$(command -v go-ios)
if [ "$RESOLVED_GO_IOS" != "$TOOLKIT_IOS_LOCATION/go-ios" ]; then
echo "⚠️ Another go-ios at $RESOLVED_GO_IOS shadows the installed one in your PATH"
echo "📝 Remove it or reorder PATH so the toolkit version is used"
fi
else
echo "⚠️ go-ios install failed, continuing with the currently installed version"
fi
fi
fi
}
install_go_ios(){
echo "⏳ Installing go-ios $GO_IOS_PINNED_VERSION (https://github.com/danielpaulus/go-ios)..."
TOOLKIT_IOS_LOCATION=$(dirname "$0")
GO_IOS_TMP_DIR=$(mktemp -d) || return 1
GO_IOS_ARCHIVE="$GO_IOS_TMP_DIR/go-ios-mac.zip"
if ! curl -fsSL -o "$GO_IOS_ARCHIVE" \
"https://github.com/danielpaulus/go-ios/releases/download/v$GO_IOS_PINNED_VERSION/go-ios-mac.zip"; then
echo "❌ go-ios download failed, check your internet connection"
rm -rf "$GO_IOS_TMP_DIR"
return 1
fi
if ! echo "$GO_IOS_MAC_ZIP_SHA256 $GO_IOS_ARCHIVE" | shasum -a 256 -c - &> /dev/null; then
echo "❌ go-ios checksum verification failed"
rm -rf "$GO_IOS_TMP_DIR"
return 1
fi
if ! unzip -o -q "$GO_IOS_ARCHIVE" -d "$GO_IOS_TMP_DIR/unzipped"; then
echo "❌ go-ios unzip failed"
rm -rf "$GO_IOS_TMP_DIR"
return 1
fi
chmod +x "$GO_IOS_TMP_DIR/unzipped/ios"
if ! mv "$GO_IOS_TMP_DIR/unzipped/ios" "$TOOLKIT_IOS_LOCATION/go-ios"; then
echo "❌ go-ios install failed (cannot write to $TOOLKIT_IOS_LOCATION)"
rm -rf "$GO_IOS_TMP_DIR"
return 1
fi
rm -rf "$GO_IOS_TMP_DIR"
echo "✅ go-ios $GO_IOS_PINNED_VERSION installed"
}
ios_get_devices(){
check_go_ios_version
check_dependency "jq"
if [[ $(ps S | grep -c "go-ios tunnel") -ne 2 ]]; then
echo "♻️ Launching go-ios tunnel for maximum iOS compatibility (17+)"
# --pair-record-path keeps the tunnel self-identity and pairing records in
# the state dir instead of the current directory (where they would otherwise
# land - and could be committed by accident); also avoids the macOS 26 TCC
# block on the default /var/db/lockdown/RemotePairing path.
nohup go-ios tunnel start --userspace --pair-record-path="$STATE_DIR" --nojson >/dev/null 2>&1 &
GO_IOS_TUNNEL_PID=$!
#TODO save tunnel port and use it to avoid delays when trying various ports
disown $GO_IOS_TUNNEL_PID
sleep 1
fi
IOS_USB_DEVICES=( $(go-ios list --nojson | sort -u) )
}
ios_restart_tunnel(){
pkill -f "go-ios tunnel" &> /dev/null
sleep 1
nohup go-ios tunnel start --userspace --pair-record-path="$STATE_DIR" --nojson >/dev/null 2>&1 &
disown $!
sleep 3
}
ios_exec_dtx(){
# Runs a go-ios command that requires an active tunnel (DTX/instruments protocol).
# On tunnel or instrument error: restarts the tunnel once and retries automatically.
# Usage: ios_exec_dtx go-ios kill "$PACKAGE" --udid="$UDID"
local OUTPUT
OUTPUT=$("$@" 2>&1)
if echo "$OUTPUT" | grep -qi "tunnel\|DVTSecure\|processcontrol\|connection reset\|Timed out\|broken pipe\|InvalidService"; then
echo "⚠️ Instrument connection failed, restarting tunnel..."
ios_restart_tunnel
OUTPUT=$("$@" 2>&1)
if echo "$OUTPUT" | grep -qi "tunnel\|DVTSecure\|processcontrol\|connection reset\|Timed out\|broken pipe\|InvalidService"; then
echo "❌ Failed after tunnel restart"
echo "💡 Developer Mode must be enabled for this command:"
echo " 1. Open Xcode and connect your device - this reveals Developer Mode in Settings"
echo " 2. Settings → Privacy & Security → Developer Mode → enable → restart device"
echo " 3. Reconnect and retry"
return 1
fi
fi
if echo "$OUTPUT" | grep -qi "process not found"; then
echo "🤷 Process not found - app may not be running"
return 1
fi
return 0
}
ios_pair_device(){
go-ios pair --udid="$1" --nojson &> /dev/null
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
read -r -p "❌ Device is not paired - reconnect it, unlock screen, tap \"Trust\" and press ENTER..." \
|| abort "❌ Device is not paired"
ios_pair_device "$1"
fi
}
ios_check_pairing(){
go-ios info --udid="$1" &> "$TEMPORARY_FILE"
if cat "$TEMPORARY_FILE" | grep -q 'UntrustedHostBUID' ; then
read -r -p "❌ Device is not paired - reconnect it, unlock screen, tap \"Trust\" and press ENTER..." \
|| abort "❌ Device is not paired"
ios_pair_device "$1"
fi
if cat "$TEMPORARY_FILE" | grep -q 'could not retrieve PairRecord' ; then
read -r -p "❌ Device is not paired - reconnect it, unlock screen, tap \"Trust\" and press ENTER..." \
|| abort "❌ Device is not paired"
ios_pair_device "$1"
fi
}
ios_check_developer_mode(){
# On iOS 16+ the developer disk image cannot be mounted while Developer Mode is off
# (Apple's TSS then rejects the signature with a misleading "not eligible" error)
DEV_MODE=$( (go-ios devmode get --udid="$1") 2>/dev/null)
if [[ $DEV_MODE == *'"DeveloperModeEnabled":false'* ]]; then
go-ios devmode reveal --udid="$1" &> /dev/null
echo "❌ Developer Mode is turned off on this device"
echo "📱 Turn it on: Settings → Privacy & Security → Developer Mode → enable → restart the device"
echo " (the Developer Mode menu has just been revealed in Settings for you)"
abort "🔄 Then reconnect the device and run the command again"
fi
}
ios_check_developer_image(){
ios_check_developer_mode "$1"
IS_MOUNTED=$( (go-ios image list --udid="$1" --nojson) 2>&1)
if [[ $IS_MOUNTED == *"none"* ]]; then
echo "⏳ Mounting developer disk image (downloads it on first use, requires internet)..."
go-ios image auto --udid="$1" --basedir="$STATE_DIR/devimages" &> "$TEMPORARY_FILE"
IS_MOUNTED=$( (go-ios image list --udid="$1" --nojson) 2>&1)
if [[ $IS_MOUNTED == *"none"* ]]; then
echo "❌ Mounting failed:"
tail -3 "$TEMPORARY_FILE"
yes_or_no "🔁 Unlock the device screen, check your internet connection and retry?"
if [[ "$RESPONSE" == "y" || "$RESPONSE" == "Y" ]]; then
ios_check_developer_image "$1"
else
abort "❌ Developer disk image is required for this command"
fi
fi
fi
}
ios_check_developer_image_and_pairing(){
ios_check_pairing "$1"
ios_check_developer_image "$1"
}
ios_device_info(){
ios_check_developer_image_and_pairing "$1"
MANUFACTURER="Apple"
go-ios info --udid="$1" > "$TEMPORARY_FILE"
MODEL=$(ios_translate_name "$(cat "$TEMPORARY_FILE" | jq -r '.ProductType')")
VERSION=$(cat "$TEMPORARY_FILE" | jq -r '.ProductVersion')
INFO=$(printf "%s) %s %s %s - %s" "$NUMBER" "$MANUFACTURER" "$MODEL" "$VERSION" "$ID")
}
ios_choose_device(){
check_for_update
ios_get_devices
if [ ${#IOS_USB_DEVICES[@]} -eq 0 ] #No device connected
then
echo "❌ No iOS devices detected, reconnect the USB and try again"
exit 1
fi
if [ ${#IOS_USB_DEVICES[@]} -gt 1 ]
then
NUMBER=1
echo "📱 Available devices:"
for ID in "${IOS_USB_DEVICES[@]}"
do
ios_device_info "$ID"
echo "$INFO"
((NUMBER++))
done
read -r -p "📝 Select a device: " CHOICE
while :;
do
if [[ ! $CHOICE =~ $REGEX_NUMBER ]] || [ "$CHOICE" -le "0" -o "$CHOICE" -gt "${#IOS_USB_DEVICES[@]}" ]; then
echo -en "\033[1A\033[2K" #deletes last echoed line in terminal
read -r -p "🤷 Invalid input, try again: " CHOICE
else
break
fi
done
SELECTED_DEVICE=${IOS_USB_DEVICES[(($CHOICE-1))]}
else
SELECTED_DEVICE="${IOS_USB_DEVICES[0]}"
fi
}
ios_get_installed_package_list(){
echo "⏳ Getting third-party package list..."
INSTALLED_PACKAGES=($(go-ios apps --udid="$1" | jq -r '.[] | .CFBundleIdentifier'))
}
ios_get_all_package_list(){
echo "⏳ Getting all package list..."
INSTALLED_PACKAGES=($(go-ios apps --udid="$1" | jq -r '.[] | .CFBundleIdentifier'))
SYSTEM_PACKAGES=($(go-ios apps --udid="$1" --system | jq -r '.[] | .CFBundleIdentifier'))
ALL_PACKAGES=("${INSTALLED_PACKAGES[@]}" "${SYSTEM_PACKAGES[@]}")
}
ios_is_package_installed(){
ios_get_all_package_list "$SELECTED_DEVICE"
echo "${ALL_PACKAGES[*]}" | grep -w "$1" &> /dev/null
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "🤷 Package \"$1\" is not installed"
exit 1
fi
}
ios_translate_name(){
# arg1 = ProductType (e.g. "iPhone14,2"); prints the marketing name (e.g. "iPhone 13 Pro")
# Primary source: Xcode's device_traits.db; fallback: cached appledb.dev dataset
PRODUCT_TYPE=$1
NAME=""
TRAITS_DB="$(xcode-select -p 2>/dev/null)/Platforms/iPhoneOS.platform/usr/standalone/device_traits.db"
if [ -f "$TRAITS_DB" ] && command -v sqlite3 &> /dev/null; then
NAME=$(sqlite3 "$TRAITS_DB" "SELECT ProductDescription FROM Devices WHERE ProductType='${PRODUCT_TYPE//\'/}' LIMIT 1" 2>/dev/null)
fi
if [ -z "$NAME" ] && command -v jq &> /dev/null; then
APPLEDB_CACHE="$STATE_DIR/appledb_devices.json"
APPLEDB_FAILED_STAMP="$APPLEDB_CACHE.failed"
# Retry a failed download at most once per day to avoid stalling every command while offline
if ! [ -f "$APPLEDB_CACHE" ] && [ "$(cat "$APPLEDB_FAILED_STAMP" 2>/dev/null)" != "$(date +%Y-%m-%d)" ]; then
echo "⏳ Downloading device name database (first use only)..." >&2
APPLEDB_TMP="$APPLEDB_CACHE.tmp.$$"
if curl -fsSL --connect-timeout 3 --max-time 15 "https://api.appledb.dev/device/main.json" -o "$APPLEDB_TMP" 2>/dev/null \
&& jq -e 'type=="array"' "$APPLEDB_TMP" &> /dev/null; then
mv "$APPLEDB_TMP" "$APPLEDB_CACHE"
rm -f "$APPLEDB_FAILED_STAMP"
else
rm -f "$APPLEDB_TMP"
date +%Y-%m-%d > "$APPLEDB_FAILED_STAMP" 2>/dev/null
fi
fi
if [ -f "$APPLEDB_CACHE" ]; then
NAME=$(jq -r --arg id "$PRODUCT_TYPE" 'first(.[] | select(.identifier[]? == $id) | .name) // empty' "$APPLEDB_CACHE" 2>/dev/null)
fi
fi
if [ -z "$NAME" ]; then
NAME=$PRODUCT_TYPE
fi
echo "$NAME"
}
##############################################################################
### Commons
check_adb_dependency(){
if ! [ -x "$(command -v "adb")" ]; then
ANDROID_SDK_GUESS="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
ADB_CANDIDATE="$ANDROID_SDK_GUESS/platform-tools/adb"
if [ -x "$ADB_CANDIDATE" ]; then
export PATH="$ANDROID_SDK_GUESS/platform-tools:$PATH"
else
echo "🤷 Android Debug Bridge required!"
should_proceed "🔄 Install via homebrew? (this may take a while)"
brew install --cask "android-platform-tools"
fi
fi
}
check_simctl_dependency(){
if ! xcrun simctl help &> /dev/null; then
echo "❌ Xcode Simulator tools (simctl) are not accessible"
if [ -d "/Applications/Xcode.app" ]; then
echo "💡 Xcode is installed but xcode-select is pointing elsewhere"
should_proceed "🔄 Fix with: sudo xcode-select -s /Applications/Xcode.app"
sudo xcode-select -s /Applications/Xcode.app
else
echo "💡 Install Xcode from the App Store, then connect your device and trust your computer"
should_proceed "🛍 Open App Store?"
open -a "App Store"
exit 1
fi
fi
}
check_dependency(){
if ! [ -x "$(command -v "$1")" ]; then
echo "💥 \"$1\" command required!"
should_proceed "🛒 Install via homebrew? (this may take a while)"
brew install "$1"
fi
}
check_for_update(){
TODAY=$(date +%Y-%m-%d)
if [ -f "$LAST_CHECK_DATE_FILE" ]; then
LAST_CHECK_DATE=$(cat "$LAST_CHECK_DATE_FILE")
else
echo "$TODAY" > "$LAST_CHECK_DATE_FILE"
fi
CURRENT_DIR="$PWD"
cd "$SCRIPT_LOCATION/.." || return
# When installed via Homebrew the toolkit is not a git checkout - skip the
# git-based self-update silently (Homebrew handles updates via "brew upgrade").
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ "$CURRENT_BRANCH" == "master" && "$LAST_CHECK_DATE" != "$TODAY" ]]; then
echo "🔄 Checking for Mobile Toolkit update..."
echo "$TODAY" > "$LAST_CHECK_DATE_FILE"
git fetch origin &> /dev/null
COMMITS_BEHIND=$(git rev-list --count HEAD..origin/master 2>/dev/null)
if [ -n "$COMMITS_BEHIND" ] && [ "$COMMITS_BEHIND" -gt 0 ] 2>/dev/null; then
yes_or_no "🆕 Update available, download now?";
if [[ "$RESPONSE" == "y" || "$RESPONSE" == "Y" ]]; then
echo "⏬ Updating..."
if git pull --ff-only &> /dev/null; then
echo "✨ New features:"
cat "$SCRIPT_LOCATION"/../changelog.txt
echo
echo "✅ Update complete - the new version will be used on the next run"
else
echo "❌ Update failed (local changes in the toolkit directory?) - run \"git pull\" there manually"
fi
fi
fi
fi
cd "$CURRENT_DIR" || exit
}
delete_lastline(){
echo -en "\033[1A\033[2K"
}
yes_or_no(){
if ! read -r -n 1 -p "$1 [y/n] " RESPONSE; then
# stdin closed (non-interactive use) - treat as "no" instead of looping
RESPONSE="n"
echo
return
fi
case "$RESPONSE" in
[yY])
;;
[nN])
;;
*)
echo
echo "🤷 Invalid option"
yes_or_no "$1"
;;
esac
echo
}
should_proceed(){
read -r -n 1 -p "$1 [y/n] " RESPONSE
case "$RESPONSE" in
[yY])
;;
*)
exit
;;
esac
echo
}
choose_number(){
MAX=$1
if [ -n "$2" ]; then
read -r -p "$2" CHOICE || abort "❌ No input available"
else
read -r -p "📝 Choose number: " CHOICE || abort "❌ No input available"
fi
while :;
do
if [[ -z $CHOICE || $CHOICE -le 0 || $CHOICE -gt $MAX ]]; then
delete_lastline
read -r -p "🤷 Invalid choice, try again: " CHOICE || abort "❌ No input available"
else
((++CHOICE))
break
fi
done
}
check_url(){
URL=$1
if [[ $URL == "" ]]; then
read -r -p "📝 Insert web url: " URL || abort "❌ No input available"
check_url "$URL"
else
case $1 in
'http://'*)
;;
'https://'*)
;;
*'://'*)
;;
*)
URL='http://'$URL
;;
esac
fi
}
abort(){
echo "$1"
exit 1
}