device-devtools-mcp 0.1.0

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 (66) hide show
  1. package/AGENTS.md +327 -0
  2. package/LICENSE +21 -0
  3. package/README.md +491 -0
  4. package/VERSION +1 -0
  5. package/bin/device-devtools-mcp.js +106 -0
  6. package/bin/devicetools +315 -0
  7. package/config.example.json +63 -0
  8. package/integrations/agent-pointer.sh +48 -0
  9. package/integrations/claude/SKILL.md +8 -0
  10. package/integrations/cursor/devicetools.mdc +8 -0
  11. package/integrations/gemini/GEMINI.md +5 -0
  12. package/integrations/mcp/README.md +184 -0
  13. package/integrations/mcp/mcp.json +10 -0
  14. package/integrations/mcp/reference.sh +119 -0
  15. package/integrations/mcp/selftest.sh +158 -0
  16. package/integrations/mcp/server.sh +343 -0
  17. package/package.json +50 -0
  18. package/scripts/android/app.sh +241 -0
  19. package/scripts/android/back.sh +73 -0
  20. package/scripts/android/controls.sh +56 -0
  21. package/scripts/android/devices.sh +77 -0
  22. package/scripts/android/doctor.sh +253 -0
  23. package/scripts/android/lib.sh +699 -0
  24. package/scripts/android/logs.sh +289 -0
  25. package/scripts/android/permission.sh +119 -0
  26. package/scripts/android/settings.sh +63 -0
  27. package/scripts/android/setup.sh +97 -0
  28. package/scripts/android/tree.awk +166 -0
  29. package/scripts/android/tree.sh +127 -0
  30. package/scripts/android/type.sh +191 -0
  31. package/scripts/common/find.sh +95 -0
  32. package/scripts/common/key.sh +65 -0
  33. package/scripts/common/lib.sh +14 -0
  34. package/scripts/common/measure.sh +170 -0
  35. package/scripts/common/open.sh +131 -0
  36. package/scripts/common/screenshot.sh +102 -0
  37. package/scripts/common/scroll.sh +177 -0
  38. package/scripts/common/snapshot.sh +69 -0
  39. package/scripts/common/swipe.sh +171 -0
  40. package/scripts/common/tap.sh +226 -0
  41. package/scripts/common/wait.sh +212 -0
  42. package/scripts/common/waypoint.sh +141 -0
  43. package/scripts/dispatch.sh +21 -0
  44. package/scripts/flow.sh +266 -0
  45. package/scripts/init.sh +101 -0
  46. package/scripts/ios/app.sh +404 -0
  47. package/scripts/ios/back.sh +95 -0
  48. package/scripts/ios/controls.sh +68 -0
  49. package/scripts/ios/devices.sh +80 -0
  50. package/scripts/ios/doctor.sh +386 -0
  51. package/scripts/ios/lib.sh +864 -0
  52. package/scripts/ios/logs.sh +272 -0
  53. package/scripts/ios/permission.sh +108 -0
  54. package/scripts/ios/settings.sh +76 -0
  55. package/scripts/ios/setup.sh +175 -0
  56. package/scripts/ios/tree.sh +128 -0
  57. package/scripts/ios/type.sh +178 -0
  58. package/scripts/lib.sh +1032 -0
  59. package/scripts/links.tsv +44 -0
  60. package/scripts/relink.sh +121 -0
  61. package/scripts/run.sh +415 -0
  62. package/scripts/selftest.sh +1709 -0
  63. package/scripts/snapshot.awk +362 -0
  64. package/scripts/verify-npm-package.js +133 -0
  65. package/tests/fixtures/ios-contacts-list.expected +52 -0
  66. package/tests/fixtures/ios-contacts-list.rows +140 -0
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env bash
2
+ # logs.sh — device logs.
3
+ #
4
+ # logs.sh start clear the buffer and begin collecting
5
+ # logs.sh stop stop collecting
6
+ # logs.sh status is it collecting, and since when
7
+ # logs.sh [--since Ns] [--process P] [--grep S] [--lines N]
8
+ # read what is available
9
+ # --network only lines that look like HTTP requests
10
+ # or responses, from the app under test.
11
+ # THIS IS NOT A PROXY: it filters what the
12
+ # app itself chose to print, so it shows no
13
+ # bodies, no headers the app did not log,
14
+ # and nothing at all from an app that logs
15
+ # none of it. In exchange it needs no CA
16
+ # certificate on the device and certificate
17
+ # pinning cannot block it.
18
+ # The regex is logs.network_pattern.
19
+ #
20
+ # WHY READING WORKS HERE WITHOUT A COLLECTOR, UNLIKE iOS
21
+ #
22
+ # logcat keeps a ring buffer on the device, so the recent past can be asked for
23
+ # after the fact. The iOS adapter cannot do that — idevicesyslog is a live
24
+ # stream with no history — which is why it must start a collector and tell you
25
+ # the history begins now. On Android a read with no collector running still
26
+ # returns real logs, and says where they came from.
27
+ #
28
+ # A collector is still worth starting for a long run. The ring buffer is a few
29
+ # hundred kilobytes per buffer, and a chatty app overruns it in under a minute,
30
+ # so anything older than that is gone. `start` clears the buffer first, which
31
+ # also scopes everything read afterwards to the current run.
32
+ #
33
+ # NO --subsystem
34
+ #
35
+ # That is an os_log concept and has no equivalent here. `--process` filters on
36
+ # the tag or package, and `--grep` on the message, which is what logcat has.
37
+ #
38
+ # exit 0 lines printed, or a lifecycle verb succeeded
39
+ # exit 1 usage error
40
+ # exit 2 environment problem
41
+ # exit 4 nothing matched
42
+
43
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
44
+
45
+ SINCE=""
46
+ PROCESS=""
47
+ GREP=""
48
+ NETWORK=0
49
+ LINES=""
50
+ VERB="read"
51
+
52
+ while [ $# -gt 0 ]; do
53
+ case "$1" in
54
+ start|stop|status) VERB="$1"; shift ;;
55
+ --since)
56
+ [ $# -ge 2 ] || die "--since needs a number of seconds" 1
57
+ case "${2%s}" in (*[!0-9]*|"") die "--since needs seconds, e.g. --since 30 or --since 30s, got: $2" 1 ;; esac
58
+ SINCE="${2%s}"; shift 2 ;;
59
+ --process)
60
+ [ $# -ge 2 ] || die "--process needs a name" 1
61
+ PROCESS="$2"; shift 2 ;;
62
+ --network) NETWORK=1; shift ;;
63
+ --grep)
64
+ [ $# -ge 2 ] || die "--grep needs a string" 1
65
+ GREP="$2"; shift 2 ;;
66
+ --lines)
67
+ [ $# -ge 2 ] || die "--lines needs a number" 1
68
+ case "$2" in (*[!0-9]*|"") die "--lines needs a positive integer, got: $2" 1 ;; esac
69
+ LINES="$2"; shift 2 ;;
70
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
71
+ *) die "unknown argument: $1 (usage: logs.sh [start|stop|status] [--since Ns] [--process P] [--grep S] [--lines N])" 1 ;;
72
+ esac
73
+ done
74
+
75
+ load_config
76
+ need_cmd adb
77
+
78
+ SD="$(state_dir)"
79
+ PIDFILE="$SD/logcat.pid"
80
+ LOGFILE="$SD/logcat.log"
81
+
82
+ collector_pid() {
83
+ [ -f "$PIDFILE" ] || return 1
84
+ local p
85
+ p="$(cat "$PIDFILE" 2>/dev/null || true)"
86
+ [ -n "$p" ] || return 1
87
+ kill -0 "$p" 2>/dev/null || return 1
88
+ printf '%s' "$p"
89
+ }
90
+
91
+ # device_now — the device's clock, in epoch seconds.
92
+ #
93
+ # Every cutoff is computed from this and never from the host's clock. logcat
94
+ # stamps its records with the device's time, and on an emulator or a phone that
95
+ # has been off the network the two disagree; a --since built on the host clock
96
+ # then silently returns the wrong window, which is worse than returning nothing.
97
+ device_now() {
98
+ local t
99
+ t="$(adb_dev shell date +%s 2>/dev/null | tr -d '\r ')"
100
+ case "$t" in
101
+ ''|*[!0-9]*) die "could not read the device clock — run $(as_cmd doctor)" 3 ;;
102
+ esac
103
+ printf '%s' "$t"
104
+ }
105
+
106
+ # The collector asks logcat for `-v epoch`, so records arrive already stamped
107
+ # with an unambiguous absolute time.
108
+ #
109
+ # The obvious alternative — stamping each line in awk with systime() — does not
110
+ # work: systime() is a gawk extension and macOS ships the one true awk, which
111
+ # answers "calling undefined function systime". Because the collector runs
112
+ # detached with its output discarded, that failure was completely silent and
113
+ # produced an empty log file that looked exactly like a quiet device.
114
+ #
115
+ # Rotation stays in the writer for the same reason it does on iOS: truncating a
116
+ # file that an open writer is appending to leaves its offset past the new end,
117
+ # and every later line lands after a gap of NULs.
118
+ start_collector() {
119
+ local max serial
120
+ serial="$(device_serial)"
121
+ max="$(cfg '.logs.max_lines_kept' '200000')"
122
+ adb -s "$serial" logcat -c >/dev/null 2>&1 || true
123
+ nohup sh -c "adb -s '$serial' logcat -v epoch 2>/dev/null | awk -v f='$LOGFILE' -v max='$max' '
124
+ { n++
125
+ print >> f
126
+ fflush(f)
127
+ if (n >= max) { close(f); system(\"mv \" f \" \" f \".1\"); n = 0 }
128
+ }'" >/dev/null 2>&1 &
129
+ printf '%s' "$!" > "$PIDFILE"
130
+ local i=0
131
+ while [ "$i" -lt 20 ]; do
132
+ collector_pid >/dev/null 2>&1 && break
133
+ sleep 0.2
134
+ i=$((i + 1))
135
+ done
136
+ }
137
+
138
+ # Epoch seconds to the device's local wall clock, without a subprocess per line.
139
+ # strftime() is another gawk extension, so the offset is fetched once and the
140
+ # arithmetic done by hand.
141
+ TZ_OFFSET=""
142
+ tz_offset() {
143
+ [ -n "$TZ_OFFSET" ] && { printf '%s' "$TZ_OFFSET"; return 0; }
144
+ local z sign h m
145
+ z="$(adb_dev shell date +%z 2>/dev/null | tr -d '\r ')"
146
+ case "$z" in
147
+ [+-][0-9][0-9][0-9][0-9])
148
+ sign="${z%"${z#?}"}"; h="${z:1:2}"; m="${z:3:2}"
149
+ TZ_OFFSET="$(( ${sign}1 * (10#$h * 3600 + 10#$m * 60) ))" ;;
150
+ *) TZ_OFFSET=0 ;;
151
+ esac
152
+ printf '%s' "$TZ_OFFSET"
153
+ }
154
+
155
+ format_lines() {
156
+ awk -v off="$(tz_offset)" '
157
+ {
158
+ if ($1 ~ /^[0-9]+\.[0-9]+$/) {
159
+ sec = int($1) + off
160
+ tod = sec % 86400
161
+ stamp = sprintf("%02d:%02d:%02d", int(tod / 3600), int((tod % 3600) / 60), tod % 60)
162
+ $1 = stamp
163
+ }
164
+ print
165
+ }'
166
+ }
167
+
168
+ case "$VERB" in
169
+ start)
170
+ if pid="$(collector_pid)"; then
171
+ printf 'OK logs — already collecting (pid %s, %s lines)\n' \
172
+ "$pid" "$(wc -l < "$LOGFILE" 2>/dev/null | tr -d ' ' || echo 0)"
173
+ exit 0
174
+ fi
175
+ rm -f "$LOGFILE" "$LOGFILE.1"
176
+ start_collector
177
+ pid="$(collector_pid)" || die "could not start logcat — run $(as_cmd doctor)" 2
178
+ printf 'OK logs — buffer cleared, collecting (pid %s) into %s\n' "$pid" "$LOGFILE"
179
+ exit 0
180
+ ;;
181
+
182
+ stop)
183
+ if ! pid="$(collector_pid)"; then
184
+ printf 'OK logs — not collecting\n'
185
+ exit 0
186
+ fi
187
+ # The collector is a shell wrapping a pipeline, so the children go too or
188
+ # adb keeps the logcat channel open.
189
+ pkill -P "$pid" 2>/dev/null || true
190
+ kill "$pid" 2>/dev/null || true
191
+ rm -f "$PIDFILE"
192
+ printf 'OK logs — stopped (%s lines kept in %s)\n' \
193
+ "$(wc -l < "$LOGFILE" 2>/dev/null | tr -d ' ' || echo 0)" "$LOGFILE"
194
+ exit 0
195
+ ;;
196
+
197
+ status)
198
+ if pid="$(collector_pid)"; then
199
+ since="$(awk '$1 ~ /^[0-9]+\.[0-9]+$/ { print int($1); exit }' "$LOGFILE" 2>/dev/null || true)"
200
+ printf 'OK logs — collecting (pid %s, %s lines) since %s\n' \
201
+ "$pid" "$(wc -l < "$LOGFILE" 2>/dev/null | tr -d ' ' || echo 0)" \
202
+ "$( [ -n "$since" ] && date -r "$since" '+%H:%M:%S' 2>/dev/null || echo 'unknown' )"
203
+ else
204
+ printf 'OK logs — not collecting (reads fall back to the device ring buffer)\n'
205
+ fi
206
+ exit 0
207
+ ;;
208
+ esac
209
+
210
+ # --- read ---------------------------------------------------------------------
211
+
212
+ [ -n "$LINES" ] || LINES="$(cfg '.logs.max_lines' '50')"
213
+
214
+ if collector_pid >/dev/null 2>&1; then
215
+ SOURCE="collector"
216
+ CUTOFF=0
217
+ [ -n "$SINCE" ] && CUTOFF="$(( $(device_now) - SINCE ))"
218
+ ROTATED=""
219
+ [ -s "$LOGFILE.1" ] && ROTATED="$LOGFILE.1"
220
+ # The rotated half first, so the result stays in chronological order.
221
+ raw="$(awk -v cutoff="$CUTOFF" '$1 + 0 >= cutoff' $ROTATED "$LOGFILE" 2>/dev/null || true)"
222
+ else
223
+ SOURCE="device ring buffer"
224
+ if [ -n "$SINCE" ]; then
225
+ # `-t 30s` is not a thing: logcat wants a real timestamp, and it wants it in
226
+ # the device's own local time. Asking the device to format its own clock is
227
+ # what keeps this correct when the phone and the Mac disagree.
228
+ STAMP="$(adb_dev shell "date -d @$(( $(device_now) - SINCE )) +'%m-%d %H:%M:%S.000'" 2>/dev/null | tr -d '\r')"
229
+ if [ -n "$STAMP" ]; then
230
+ raw="$(adb_dev logcat -d -v epoch -T "$STAMP" 2>/dev/null || true)"
231
+ else
232
+ raw="$(adb_dev logcat -d -v epoch 2>/dev/null || true)"
233
+ fi
234
+ else
235
+ raw="$(adb_dev logcat -d -v epoch 2>/dev/null || true)"
236
+ fi
237
+ fi
238
+
239
+ [ -n "$raw" ] || die "no logs available from the $SOURCE" 4
240
+
241
+ out="$raw"
242
+ if [ -n "$PROCESS" ]; then
243
+ out="$(printf '%s\n' "$out" | grep -F -- "$PROCESS" || true)"
244
+ fi
245
+ if [ -n "$GREP" ]; then
246
+ out="$(printf '%s\n' "$out" | grep -F -i -- "$GREP" || true)"
247
+ fi
248
+
249
+ if [ "$NETWORK" -eq 1 ]; then
250
+ # THIS IS NOT A PROXY, AND IT CANNOT BE ONE HERE. A proxy needs its CA
251
+ # certificate installed on the device, and the app under test pins its
252
+ # certificates — it would see a handshake failure and nothing else. So this
253
+ # filters the app's own log output: what the app chose to print, and nothing
254
+ # more. No bodies, no headers the app did not log, and nothing at all from an
255
+ # app that logs nothing.
256
+ #
257
+ # THE PATTERN IS STRICTER THAN IT LOOKS, AND THE DEVICE IS WHY. A plain
258
+ # /GET|POST|.../ matched bluetoothd on the phone this was built against,
259
+ # because BD_VSC_OLYMPIC_GET_EXT_ADV_DROPPED contains GET. So a verb has to
260
+ # sit on a word boundary AND be followed by something that looks like a URL.
261
+ # "forgetting the cache" was never a GET either.
262
+ # The Network panel means the app's traffic. Without this the filter searches
263
+ # the whole device syslog, which on this phone is thirty-five thousand lines
264
+ # of other people's processes.
265
+ app="$(cfg '.app.package' '')"
266
+ if [ -n "$app" ]; then
267
+ out="$(printf '%s\n' "$out" | grep -i -F -- "$app" || true)"
268
+ fi
269
+ PAT="$(cfg '.logs.network_pattern' '(^|[^A-Za-z0-9_])(GET|POST|PUT|PATCH|DELETE)[[:space:]]+(https?://|/)|HTTP/[0-9]')"
270
+ out="$(printf '%s\n' "$out" | awk -v pat="$PAT" '$0 ~ pat' || true)"
271
+ if [ -z "$out" ]; then
272
+ die "no log lines matched /$PAT/ — this reads the app's own logging rather than a proxy, so an app that does not print its requests shows nothing here. Set logs.network_pattern in the config to match how yours logs them" 4
273
+ fi
274
+ fi
275
+
276
+ if [ -z "$out" ]; then
277
+ what=""
278
+ [ -n "$PROCESS" ] && what=" --process '$PROCESS'"
279
+ [ -n "$GREP" ] && what="$what --grep '$GREP'"
280
+ die "nothing matched$what in the $SOURCE" 4
281
+ fi
282
+
283
+ n="$(printf '%s\n' "$out" | wc -l | tr -d ' ')"
284
+ if [ "$n" -gt "$LINES" ]; then
285
+ printf '%s\n' "$out" | tail -n "$LINES" | format_lines
286
+ printf '… %s earlier line(s) not shown (--lines N)\n' "$(( n - LINES ))"
287
+ else
288
+ printf '%s\n' "$out" | format_lines
289
+ fi
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # permission.sh — grant or deny a runtime permission before the app asks.
3
+ #
4
+ # permission.sh <name>=<allow|deny> [<name>=<allow|deny> ...]
5
+ #
6
+ # permission.sh camera=allow location=deny
7
+ #
8
+ # WHY THIS IS A STEP AND NOT AUTOMATIC
9
+ #
10
+ # Other tools dismiss permission dialogs for you. That hides whether the app
11
+ # asked at all — and for a production app, whether it asks, and for what, is itself
12
+ # under test. A prompt that quietly stops appearing is a real regression, and a
13
+ # tool that taps it away can never report one.
14
+ #
15
+ # So the decision is written in the test. Granting up front also means the
16
+ # dialog never appears, which is more reliable than racing to tap it; when you
17
+ # do want to exercise the dialog itself, leave this out and use
18
+ # `tap: { label: Allow, optional: true }`.
19
+ #
20
+ # Names are platform-neutral and each maps to whatever the platform calls it.
21
+ # A name this platform cannot express is refused rather than silently ignored,
22
+ # because a permission step that did nothing is worse than one that failed.
23
+ #
24
+ # camera microphone location location-always contacts photos calendar
25
+ # notifications storage phone sms
26
+ #
27
+ # exit 0 every permission is now in the requested state
28
+ # exit 1 usage error, or a name this platform does not have
29
+ # exit 2 environment or configuration problem
30
+ # exit 3 device unreachable — run doctor.sh
31
+ # exit 4 the change was applied but did not take
32
+
33
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
34
+
35
+ [ $# -ge 1 ] || die "missing permission (usage: permission.sh <name>=<allow|deny> [...])" 1
36
+ case "$1" in
37
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
38
+ esac
39
+
40
+ load_config
41
+ PKG="$(cfg '.app.package' '')"
42
+ [ -n "$PKG" ] || die "app.package is not set in $DT_CONFIG — permissions are granted to a package" 2
43
+
44
+ # One neutral name can cover several Android permissions: "contacts" is read and
45
+ # write, and "storage" changed shape in Android 13. All of them are applied, and
46
+ # ones the device does not define are skipped rather than failing the step — a
47
+ # device that has no READ_MEDIA_IMAGES is not a test failure.
48
+ android_perms() {
49
+ case "$1" in
50
+ camera) printf 'android.permission.CAMERA' ;;
51
+ microphone) printf 'android.permission.RECORD_AUDIO' ;;
52
+ location) printf 'android.permission.ACCESS_FINE_LOCATION android.permission.ACCESS_COARSE_LOCATION' ;;
53
+ location-always) printf 'android.permission.ACCESS_BACKGROUND_LOCATION android.permission.ACCESS_FINE_LOCATION' ;;
54
+ contacts) printf 'android.permission.READ_CONTACTS android.permission.WRITE_CONTACTS' ;;
55
+ photos) printf 'android.permission.READ_MEDIA_IMAGES android.permission.READ_EXTERNAL_STORAGE' ;;
56
+ calendar) printf 'android.permission.READ_CALENDAR android.permission.WRITE_CALENDAR' ;;
57
+ notifications) printf 'android.permission.POST_NOTIFICATIONS' ;;
58
+ storage) printf 'android.permission.READ_EXTERNAL_STORAGE android.permission.WRITE_EXTERNAL_STORAGE' ;;
59
+ phone) printf 'android.permission.READ_PHONE_STATE' ;;
60
+ sms) printf 'android.permission.READ_SMS android.permission.SEND_SMS' ;;
61
+ *) return 1 ;;
62
+ esac
63
+ }
64
+
65
+ pkg_installed "$PKG" \
66
+ || die "$PKG is not installed — run $(as_cmd app install)" 4
67
+
68
+ # granted <permission> — true when the package currently holds it.
69
+ granted() {
70
+ adb_dev shell dumpsys package "$PKG" 2>/dev/null | tr -d '\r' \
71
+ | awk -v p="$1" '$0 ~ p": granted=true" { found = 1 } END { exit found ? 0 : 1 }'
72
+ }
73
+
74
+ DONE=""
75
+ for pair in "$@"; do
76
+ case "$pair" in
77
+ *=*) ;;
78
+ *) die "expected <name>=<allow|deny>, got '$pair'" 1 ;;
79
+ esac
80
+ name="${pair%%=*}"
81
+ want="${pair#*=}"
82
+ case "$want" in
83
+ allow|deny) ;;
84
+ *) die "'$name' must be allow or deny, got '$want'" 1 ;;
85
+ esac
86
+
87
+ perms="$(android_perms "$name" || true)"
88
+ [ -n "$perms" ] \
89
+ || die "no such permission name '$name' — known: camera microphone location location-always contacts photos calendar notifications storage phone sms" 1
90
+
91
+ applied=0
92
+ for p in $perms; do
93
+ if [ "$want" = allow ]; then
94
+ adb_dev shell pm grant "$PKG" "$p" >/dev/null 2>&1 && applied=$((applied + 1)) || true
95
+ else
96
+ adb_dev shell pm revoke "$PKG" "$p" >/dev/null 2>&1 && applied=$((applied + 1)) || true
97
+ fi
98
+ done
99
+
100
+ # Not every name resolves on every Android version, but at least one of the
101
+ # permissions behind a name has to. Zero means the app does not declare any of
102
+ # them, which makes the step a silent no-op — and a permission step that did
103
+ # nothing is exactly what this refuses to be.
104
+ [ "$applied" -gt 0 ] \
105
+ || die "could not $want '$name' for $PKG — the app declares none of: $perms" 4
106
+
107
+ # Verified against the package manager rather than trusted from the exit code
108
+ # of pm, which reports success for a permission the app never declared.
109
+ first="${perms%% *}"
110
+ if [ "$want" = allow ]; then
111
+ granted "$first" || die "granted '$name' to $PKG but $first still reads as not granted" 4
112
+ else
113
+ granted "$first" && die "revoked '$name' from $PKG but $first still reads as granted" 4
114
+ fi
115
+
116
+ DONE="$DONE $name=$want"
117
+ done
118
+
119
+ printf 'OK permissions for %s —%s\n' "$PKG" "$DONE"
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ # settings.sh — the device conditions a person would change by hand.
3
+ #
4
+ # settings.sh orientation portrait|landscape
5
+ # settings.sh appearance light|dark
6
+ # settings.sh textsize <scale> 1.0, 1.15, 1.3, 1.5
7
+ #
8
+ # These are the conditions a layout breaks under, and none of them is visible in
9
+ # the accessibility tree — dark mode and text size change colours and metrics
10
+ # that neither driver reports. So the way to find out is to set them and look,
11
+ # which is what this verb is for.
12
+ #
13
+ # exit 0 set
14
+ # exit 1 usage error
15
+ # exit 3 device unreachable — run doctor.sh
16
+
17
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
18
+
19
+ case "${1-}" in
20
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
21
+ esac
22
+ KEY="${1-}"; VAL="${2-}"
23
+ [ -n "$KEY" ] && [ -n "$VAL" ] \
24
+ || die "usage: settings.sh <orientation|appearance|textsize> <value>" 1
25
+ [ $# -eq 2 ] || die "too many arguments (usage: settings.sh <key> <value>)" 1
26
+
27
+ load_config
28
+ need_cmd adb
29
+
30
+ case "$KEY" in
31
+ orientation)
32
+ case "$VAL" in
33
+ portrait) rot=0 ;;
34
+ landscape) rot=1 ;;
35
+ *) die "orientation is portrait or landscape, got '$VAL'" 1 ;;
36
+ esac
37
+ # Auto-rotate has to go off first, or the accelerometer puts it straight
38
+ # back and the verb reports a change that lasted half a second.
39
+ adb_dev shell settings put system accelerometer_rotation 0 >/dev/null 2>&1 \
40
+ || die "could not reach the device — run $(as_cmd doctor)" 3
41
+ adb_dev shell settings put system user_rotation "$rot" >/dev/null 2>&1 \
42
+ || die "could not reach the device — run $(as_cmd doctor)" 3
43
+ ;;
44
+ appearance)
45
+ case "$VAL" in
46
+ light) mode=no ;;
47
+ dark) mode=yes ;;
48
+ *) die "appearance is light or dark, got '$VAL'" 1 ;;
49
+ esac
50
+ adb_dev shell "cmd uimode night $mode" >/dev/null 2>&1 \
51
+ || die "could not reach the device — run $(as_cmd doctor)" 3
52
+ ;;
53
+ textsize)
54
+ case "$VAL" in
55
+ *[!0-9.]*|"") die "textsize is a scale like 1.0 or 1.3, got '$VAL'" 1 ;;
56
+ esac
57
+ adb_dev shell settings put system font_scale "$VAL" >/dev/null 2>&1 \
58
+ || die "could not reach the device — run $(as_cmd doctor)" 3
59
+ ;;
60
+ *) die "unknown setting '$KEY' — orientation, appearance or textsize" 1 ;;
61
+ esac
62
+
63
+ printf 'OK settings — %s is now %s\n' "$KEY" "$VAL"
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # setup.sh — put the UiAutomator2 driver on the device.
3
+ #
4
+ # setup.sh [--force]
5
+ #
6
+ # WHY THIS EXISTS AS A COMMAND
7
+ #
8
+ # Provisioning an Android device is four steps: fetch a pinned release and
9
+ # install two APKs. A person following a README had to find the same two URLs by
10
+ # hand. The knowledge was already written down — it was simply written down
11
+ # somewhere no user looks.
12
+ #
13
+ # TWO PACKAGES, AND BOTH ARE REQUIRED.
14
+ #
15
+ # `io.appium.uiautomator2.server` is the server; `…server.test` is the
16
+ # instrumentation that starts it. Installing only the first leaves a device that
17
+ # fails later with "the server will not start", which sends people looking at
18
+ # ports. doctor reports them separately for the same reason.
19
+ #
20
+ # Idempotent: an APK already downloaded is not fetched again, and `adb install
21
+ # -r` replaces cleanly. --force re-downloads, which is what a corrupted or
22
+ # truncated file needs.
23
+ #
24
+ # exit 0 the driver is installed and verified
25
+ # exit 1 usage error
26
+ # exit 2 something could not be downloaded or installed
27
+ # exit 3 no device
28
+
29
+ source "$(dirname "$0")/lib.sh"
30
+ load_config
31
+
32
+ FORCE=0
33
+ while [ $# -gt 0 ]; do
34
+ case "$1" in
35
+ --force) FORCE=1; shift ;;
36
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
37
+ *) die "unknown argument: $1" 1 ;;
38
+ esac
39
+ done
40
+
41
+ need_cmd curl
42
+ need_cmd adb
43
+
44
+ say() { printf '%s %s — %s\n' "$1" "$2" "$3"; }
45
+
46
+ VERSION="$(cfg '.ua2.version' 'v10.6.2')"
47
+ APK_DIR="$(expand_path "$(cfg '.ua2.apk_dir' '~/tools/uiautomator2')")"
48
+ SERVER_APK="$APK_DIR/appium-uiautomator2-server-$VERSION.apk"
49
+ TEST_APK="$APK_DIR/appium-uiautomator2-server-debug-androidTest.apk"
50
+ BASE="https://github.com/appium/appium-uiautomator2-server/releases/download/$VERSION"
51
+
52
+ mkdir -p "$APK_DIR" || die "could not create $APK_DIR" 2
53
+
54
+ fetch() { # fetch <url> <dest>
55
+ if [ -s "$2" ] && [ "$FORCE" -eq 0 ]; then
56
+ say OK apk "$(basename "$2") already at $2"
57
+ return 0
58
+ fi
59
+ # To a temporary name first: a half-downloaded APK left at the final path
60
+ # looks present to every check above and fails at install with a message
61
+ # about the archive rather than about the download.
62
+ # curl's own stderr is suppressed and its exit code translated. "curl: (56)
63
+ # The requested URL returned error: 404" above our message is two lines
64
+ # saying one thing, and the one that names the remedy is the second.
65
+ curl -fsSL -o "$2.part" "$1" 2>/dev/null \
66
+ || { rm -f "$2.part"
67
+ die "could not download $(basename "$2") from $1 — check the network, or set ua2.version to a release that exists" 2; }
68
+ mv "$2.part" "$2"
69
+ say OK apk "$(basename "$2") downloaded"
70
+ }
71
+
72
+ fetch "$BASE/appium-uiautomator2-server-$VERSION.apk" "$SERVER_APK"
73
+ fetch "$BASE/appium-uiautomator2-server-debug-androidTest.apk" "$TEST_APK"
74
+
75
+ SERIAL="$(device_serial)"
76
+ adb -s "$SERIAL" get-state >/dev/null 2>&1 \
77
+ || die "device '$SERIAL' is not connected — run $(as_cmd devices) to see what is" 3
78
+
79
+ # -g grants the runtime permissions the server needs up front. Without it the
80
+ # first test that touches a permission dialog fails in a way that looks like the
81
+ # app's fault.
82
+ for apk in "$SERVER_APK" "$TEST_APK"; do
83
+ out="$(adb -s "$SERIAL" install -r -g "$apk" 2>&1 </dev/null)" \
84
+ || die "could not install $(basename "$apk"): $(printf '%s' "$out" | tr '\n' ' ' | head -c 200)" 2
85
+ done
86
+ say OK install "both packages installed on $SERIAL"
87
+
88
+ UA2_PKG=io.appium.uiautomator2.server
89
+ pkg_installed "$UA2_PKG" \
90
+ || die "installed without error but $UA2_PKG is not present — try: adb -s $SERIAL uninstall $UA2_PKG, then run this again" 2
91
+ pkg_installed "$UA2_PKG.test" \
92
+ || die "installed without error but $UA2_PKG.test is not present — try: adb -s $SERIAL uninstall $UA2_PKG.test, then run this again" 2
93
+
94
+ VER="$(adb -s "$SERIAL" shell dumpsys package "$UA2_PKG" 2>/dev/null </dev/null \
95
+ | awk -F= '/versionName/ { print $2; exit }' | tr -d '\r')"
96
+ say OK verified "UiAutomator2 server ${VER:-$VERSION} on $SERIAL"
97
+ printf 'NEXT scripts/doctor.sh\n'