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.
- package/AGENTS.md +327 -0
- package/LICENSE +21 -0
- package/README.md +491 -0
- package/VERSION +1 -0
- package/bin/device-devtools-mcp.js +106 -0
- package/bin/devicetools +315 -0
- package/config.example.json +63 -0
- package/integrations/agent-pointer.sh +48 -0
- package/integrations/claude/SKILL.md +8 -0
- package/integrations/cursor/devicetools.mdc +8 -0
- package/integrations/gemini/GEMINI.md +5 -0
- package/integrations/mcp/README.md +184 -0
- package/integrations/mcp/mcp.json +10 -0
- package/integrations/mcp/reference.sh +119 -0
- package/integrations/mcp/selftest.sh +158 -0
- package/integrations/mcp/server.sh +343 -0
- package/package.json +50 -0
- package/scripts/android/app.sh +241 -0
- package/scripts/android/back.sh +73 -0
- package/scripts/android/controls.sh +56 -0
- package/scripts/android/devices.sh +77 -0
- package/scripts/android/doctor.sh +253 -0
- package/scripts/android/lib.sh +699 -0
- package/scripts/android/logs.sh +289 -0
- package/scripts/android/permission.sh +119 -0
- package/scripts/android/settings.sh +63 -0
- package/scripts/android/setup.sh +97 -0
- package/scripts/android/tree.awk +166 -0
- package/scripts/android/tree.sh +127 -0
- package/scripts/android/type.sh +191 -0
- package/scripts/common/find.sh +95 -0
- package/scripts/common/key.sh +65 -0
- package/scripts/common/lib.sh +14 -0
- package/scripts/common/measure.sh +170 -0
- package/scripts/common/open.sh +131 -0
- package/scripts/common/screenshot.sh +102 -0
- package/scripts/common/scroll.sh +177 -0
- package/scripts/common/snapshot.sh +69 -0
- package/scripts/common/swipe.sh +171 -0
- package/scripts/common/tap.sh +226 -0
- package/scripts/common/wait.sh +212 -0
- package/scripts/common/waypoint.sh +141 -0
- package/scripts/dispatch.sh +21 -0
- package/scripts/flow.sh +266 -0
- package/scripts/init.sh +101 -0
- package/scripts/ios/app.sh +404 -0
- package/scripts/ios/back.sh +95 -0
- package/scripts/ios/controls.sh +68 -0
- package/scripts/ios/devices.sh +80 -0
- package/scripts/ios/doctor.sh +386 -0
- package/scripts/ios/lib.sh +864 -0
- package/scripts/ios/logs.sh +272 -0
- package/scripts/ios/permission.sh +108 -0
- package/scripts/ios/settings.sh +76 -0
- package/scripts/ios/setup.sh +175 -0
- package/scripts/ios/tree.sh +128 -0
- package/scripts/ios/type.sh +178 -0
- package/scripts/lib.sh +1032 -0
- package/scripts/links.tsv +44 -0
- package/scripts/relink.sh +121 -0
- package/scripts/run.sh +415 -0
- package/scripts/selftest.sh +1709 -0
- package/scripts/snapshot.awk +362 -0
- package/scripts/verify-npm-package.js +133 -0
- package/tests/fixtures/ios-contacts-list.expected +52 -0
- 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'
|