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,699 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# DeviceTools — Android adapter internals: everything that knows about adb and the
|
|
3
|
+
# UiAutomator2 server.
|
|
4
|
+
#
|
|
5
|
+
# Sourced by the scripts in this directory, never by anything above it. The
|
|
6
|
+
# platform-neutral half lives in ../lib.sh and is sourced from here, so an
|
|
7
|
+
# adapter script sources exactly one file.
|
|
8
|
+
#
|
|
9
|
+
# WHY A SERVER ON THE DEVICE RATHER THAN PLAIN adb
|
|
10
|
+
#
|
|
11
|
+
# `adb shell uiautomator dump` needs nothing installed, and for reading the
|
|
12
|
+
# screen it works. It was rejected on two measurements:
|
|
13
|
+
#
|
|
14
|
+
# reading the tree dump 4.3s server 0.11s
|
|
15
|
+
# typing "Lê" dump — impossible
|
|
16
|
+
#
|
|
17
|
+
# `adb shell input text` rejects every non-ASCII string. Four encodings were
|
|
18
|
+
# tried; each returned "Exception occurred while executing 'text'" while the
|
|
19
|
+
# same call with ASCII returned 0. For an app whose login screen is in
|
|
20
|
+
# Vietnamese that is not a limitation, it is a disqualification.
|
|
21
|
+
#
|
|
22
|
+
# So Android gets the same shape as iOS: an HTTP driver installed on the device,
|
|
23
|
+
# separate from the app under test. It is a separate application, so the rule
|
|
24
|
+
# that no instrumentation code goes inside the app under test still holds.
|
|
25
|
+
|
|
26
|
+
source "$(dirname "${BASH_SOURCE[0]}")/../lib.sh"
|
|
27
|
+
|
|
28
|
+
ua2_base() {
|
|
29
|
+
printf 'http://%s:%s' "$(cfg '.ua2.host' '127.0.0.1')" "$(cfg '.ua2.local_port' '6790')"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
http_timeout() { printf '%s' "${DT_HTTP_TIMEOUT:-$(cfg '.timeouts.http_seconds' '10')}"; }
|
|
33
|
+
|
|
34
|
+
device_serial() { cfg '.device.serial'; }
|
|
35
|
+
|
|
36
|
+
# adb_dev — adb bound to the configured device, and detached from stdin.
|
|
37
|
+
#
|
|
38
|
+
# Never bare `adb`: with two devices attached, bare adb fails, and with one
|
|
39
|
+
# attached it succeeds against whichever one that is. A test suite that runs on
|
|
40
|
+
# the wrong phone is worse than one that refuses to run.
|
|
41
|
+
#
|
|
42
|
+
# THE `< /dev/null` IS NOT DECORATION. `adb shell` forwards its standard input
|
|
43
|
+
# to the device, so an adb call anywhere inside a pipeline eats the data flowing
|
|
44
|
+
# through that pipeline. It was found here as log lines vanishing: a
|
|
45
|
+
# `$(adb shell date +%z)` used to build an awk argument silently consumed every
|
|
46
|
+
# record awk was about to format, and the command reported success with no
|
|
47
|
+
# output. No adb invocation in this tool wants stdin, so it is closed for all of
|
|
48
|
+
# them rather than remembered at each call site.
|
|
49
|
+
adb_dev() { adb -s "$(device_serial)" "$@" < /dev/null; }
|
|
50
|
+
|
|
51
|
+
# pkg_installed <package> — is exactly this package present on the device?
|
|
52
|
+
#
|
|
53
|
+
# ASK ABOUT ONE PACKAGE. NEVER LIST THEM ALL AND SEARCH THE LIST.
|
|
54
|
+
#
|
|
55
|
+
# `pm list packages` with no argument returns every package — 243 of them on a
|
|
56
|
+
# CI emulator — and that output came back TRUNCATED, differently on two queries
|
|
57
|
+
# seconds apart. One run could not find the UiAutomator2 server; the next found
|
|
58
|
+
# the server but not its test harness; a plain `adb shell pm list packages` run
|
|
59
|
+
# by hand two seconds earlier had listed all three packages involved, and `adb
|
|
60
|
+
# install` had reported Success for both APKs. Nothing was wrong with the
|
|
61
|
+
# device or with the matching. The list was simply not arriving whole.
|
|
62
|
+
#
|
|
63
|
+
# Passing the name filters it on the device, so the reply is one or two lines
|
|
64
|
+
# and there is nothing to truncate. The filter is a substring match, so the
|
|
65
|
+
# exact comparison still has to happen here — `…server` must not answer for
|
|
66
|
+
# `…server.test`.
|
|
67
|
+
pkg_installed() {
|
|
68
|
+
[ -n "${1:-}" ] || return 1
|
|
69
|
+
adb_dev shell pm list packages "$1" 2>/dev/null | tr -d '\r' | grep -qx "package:$1"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# android_kind — "device" or "emulator", resolving "auto" by asking the device.
|
|
73
|
+
ANDROID_KIND=""
|
|
74
|
+
android_kind() {
|
|
75
|
+
[ -n "$ANDROID_KIND" ] && { printf '%s' "$ANDROID_KIND"; return 0; }
|
|
76
|
+
local k
|
|
77
|
+
k="$(target_kind)"
|
|
78
|
+
case "$k" in
|
|
79
|
+
device|emulator) ANDROID_KIND="$k" ;;
|
|
80
|
+
auto)
|
|
81
|
+
# ro.kernel.qemu is set on emulators and absent on hardware; the serial
|
|
82
|
+
# prefix agrees, and checking both means a renamed serial cannot mislead.
|
|
83
|
+
if [ "$(adb_dev shell getprop ro.kernel.qemu 2>/dev/null | tr -d '\r')" = "1" ] \
|
|
84
|
+
|| case "$(device_serial)" in emulator-*) true ;; *) false ;; esac; then
|
|
85
|
+
ANDROID_KIND=emulator
|
|
86
|
+
else
|
|
87
|
+
ANDROID_KIND=device
|
|
88
|
+
fi ;;
|
|
89
|
+
*) die "device.kind must be auto, device or emulator — got '$k'" 2 ;;
|
|
90
|
+
esac
|
|
91
|
+
printf '%s' "$ANDROID_KIND"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ua2_fail <curl-exit> <url> — one line saying what actually went wrong.
|
|
95
|
+
#
|
|
96
|
+
# Unreachable and timed-out call for different reactions, and collapsing them
|
|
97
|
+
# sends the caller to restart a server that is in fact running.
|
|
98
|
+
ua2_fail() {
|
|
99
|
+
case "$1" in
|
|
100
|
+
28) die "UiAutomator2 did not answer within $(http_timeout)s at $2 — it is running but stuck on this request" 3 ;;
|
|
101
|
+
7) die "UiAutomator2 unreachable at $2 (connection refused) — run $(as_cmd doctor)" 3 ;;
|
|
102
|
+
*) die "UiAutomator2 request failed at $2 (curl $1) — run $(as_cmd doctor)" 3 ;;
|
|
103
|
+
esac
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ua2_get() {
|
|
107
|
+
local body rc=0
|
|
108
|
+
body="$(curl -sS -m "$(http_timeout)" "$(ua2_base)$1" 2>/dev/null)" || rc=$?
|
|
109
|
+
[ "$rc" -eq 0 ] || ua2_fail "$rc" "$(ua2_base)$1"
|
|
110
|
+
printf '%s' "$body"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
ua2_post() {
|
|
114
|
+
local body rc=0
|
|
115
|
+
body="$(curl -sS -m "$(http_timeout)" -X POST -H 'Content-Type: application/json' \
|
|
116
|
+
-d "${2:-\{\}}" "$(ua2_base)$1" 2>/dev/null)" || rc=$?
|
|
117
|
+
[ "$rc" -eq 0 ] || ua2_fail "$rc" "$(ua2_base)$1"
|
|
118
|
+
printf '%s' "$body"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# ua2_error <response> — the error as one short line, or empty on success.
|
|
122
|
+
#
|
|
123
|
+
# UiAutomator2 answers a failure with a full Java stack trace. One "no such
|
|
124
|
+
# element" measured over 2,000 tokens of frames from io.appium.uiautomator2,
|
|
125
|
+
# androidx.test and java.lang, none of which say anything about the app. Passing
|
|
126
|
+
# that through would put a stack trace in the caller's context on every miss, so
|
|
127
|
+
# only the error name and message survive, and the message is capped.
|
|
128
|
+
ua2_error() {
|
|
129
|
+
printf '%s' "$1" | jq -r '
|
|
130
|
+
if (.value | type) == "object" and (.value.error // "") != "" then
|
|
131
|
+
(.value.error // "error")
|
|
132
|
+
+ (if (.value.message // "") != ""
|
|
133
|
+
then ": " + ((.value.message | gsub("[\n\r\t]+"; " "))[:160])
|
|
134
|
+
else "" end)
|
|
135
|
+
else empty end' 2>/dev/null || true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# --- sessions ----------------------------------------------------------------
|
|
139
|
+
#
|
|
140
|
+
# Every command that acts on the UI needs a session. Creating one per call costs
|
|
141
|
+
# a round trip on every action, so the id is cached on disk and reused. The
|
|
142
|
+
# cache is a hint and never truth: sessions die when the instrumentation
|
|
143
|
+
# restarts, so a stale id is detected from the response and replaced once.
|
|
144
|
+
|
|
145
|
+
session_file() { printf '%s/session.id' "$(state_dir)"; }
|
|
146
|
+
|
|
147
|
+
ua2_new_session() {
|
|
148
|
+
local resp sid err
|
|
149
|
+
resp="$(ua2_post '/session' '{"capabilities":{"alwaysMatch":{},"firstMatch":[{}]}}')"
|
|
150
|
+
err="$(ua2_error "$resp")"
|
|
151
|
+
[ -z "$err" ] || die "UiAutomator2 refused a new session — $err" 3
|
|
152
|
+
sid="$(printf '%s' "$resp" | jq -r '.value.sessionId // .sessionId // empty' 2>/dev/null)"
|
|
153
|
+
[ -n "$sid" ] || die "UiAutomator2 returned no session id — run $(as_cmd doctor)" 3
|
|
154
|
+
printf '%s' "$sid" > "$(session_file)"
|
|
155
|
+
printf '%s' "$sid"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
ua2_drop_session() { rm -f "$(session_file)" 2>/dev/null || true; }
|
|
159
|
+
|
|
160
|
+
ua2_session() {
|
|
161
|
+
local f sid
|
|
162
|
+
f="$(session_file)"
|
|
163
|
+
if [ -f "$f" ]; then
|
|
164
|
+
sid="$(cat "$f" 2>/dev/null || true)"
|
|
165
|
+
[ -n "$sid" ] && { printf '%s' "$sid"; return 0; }
|
|
166
|
+
fi
|
|
167
|
+
ua2_new_session
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# session_get / session_post — a request under the current session, retried once
|
|
171
|
+
# against a fresh session when the cached one turns out to be dead.
|
|
172
|
+
session_get() {
|
|
173
|
+
local sid resp err
|
|
174
|
+
sid="$(ua2_session)"
|
|
175
|
+
resp="$(ua2_get "/session/$sid$1")"
|
|
176
|
+
err="$(ua2_error "$resp")"
|
|
177
|
+
case "$err" in
|
|
178
|
+
*"invalid session"*|*"session not"*|*"no such session"*|*"terminated"*)
|
|
179
|
+
ua2_drop_session
|
|
180
|
+
sid="$(ua2_new_session)"
|
|
181
|
+
resp="$(ua2_get "/session/$sid$1")" ;;
|
|
182
|
+
esac
|
|
183
|
+
printf '%s' "$resp"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
session_post() {
|
|
187
|
+
local sid resp err
|
|
188
|
+
sid="$(ua2_session)"
|
|
189
|
+
resp="$(ua2_post "/session/$sid$1" "${2:-}")"
|
|
190
|
+
err="$(ua2_error "$resp")"
|
|
191
|
+
case "$err" in
|
|
192
|
+
*"invalid session"*|*"session not"*|*"no such session"*|*"terminated"*)
|
|
193
|
+
ua2_drop_session
|
|
194
|
+
sid="$(ua2_new_session)"
|
|
195
|
+
resp="$(ua2_post "/session/$sid$1" "${2:-}")" ;;
|
|
196
|
+
esac
|
|
197
|
+
printf '%s' "$resp"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# session_error — kept as a name shared with the iOS adapter so the verb scripts
|
|
201
|
+
# read the same on both platforms.
|
|
202
|
+
session_error() { ua2_error "$1"; }
|
|
203
|
+
|
|
204
|
+
# --- reading the screen ------------------------------------------------------
|
|
205
|
+
#
|
|
206
|
+
# TREE_AWK converts UiAutomator's XML into the line table every verb consumes.
|
|
207
|
+
# See tree.awk for why this is not a call to yq.
|
|
208
|
+
TREE_AWK="$(dirname "${BASH_SOURCE[0]}")/tree.awk"
|
|
209
|
+
|
|
210
|
+
# fetch_source — the parsed line table, including the leading WINDOW line.
|
|
211
|
+
#
|
|
212
|
+
# Internal only: nothing may print this. It is already far smaller than the XML
|
|
213
|
+
# it came from, but a full screen is still hundreds of lines.
|
|
214
|
+
# AN EMPTY TREE IS A MISSING ANSWER, NOT AN ANSWER. ASK AGAIN.
|
|
215
|
+
#
|
|
216
|
+
# UiAutomator dumps the hierarchy of whatever is on screen at that instant, and
|
|
217
|
+
# during a window transition that is briefly nothing at all — a valid XML
|
|
218
|
+
# document describing no nodes. Treating it as "the screen has no controls"
|
|
219
|
+
# turns a race into an assertion failure against a screen that is fine a
|
|
220
|
+
# hundred milliseconds later.
|
|
221
|
+
#
|
|
222
|
+
# Measured on a GitHub Actions emulator: `assert visible
|
|
223
|
+
# id:open_search_view_edit_text` failed here on the search screen opening, on a
|
|
224
|
+
# suite that passes every time on a developer's machine. A slower device does
|
|
225
|
+
# not have different windows, it has longer transitions.
|
|
226
|
+
#
|
|
227
|
+
# Three attempts, a short pause between them. It stays a hard failure if the
|
|
228
|
+
# tree is genuinely empty, so a driver that has actually stopped answering is
|
|
229
|
+
# still reported — this buys a transition time to finish, not silence.
|
|
230
|
+
FETCH_SOURCE_TRIES=3
|
|
231
|
+
|
|
232
|
+
fetch_source() {
|
|
233
|
+
local resp xml table i
|
|
234
|
+
i=1
|
|
235
|
+
while [ "$i" -le "$FETCH_SOURCE_TRIES" ]; do
|
|
236
|
+
resp="$(session_get '/source?format=xml')"
|
|
237
|
+
xml="$(printf '%s' "$resp" | jq -r '.value // empty' 2>/dev/null)"
|
|
238
|
+
if [ -n "$xml" ]; then
|
|
239
|
+
table="$(printf '%s' "$xml" | awk -f "$TREE_AWK")"
|
|
240
|
+
[ -n "$table" ] && { printf '%s' "$table"; return 0; }
|
|
241
|
+
fi
|
|
242
|
+
[ "$i" -lt "$FETCH_SOURCE_TRIES" ] && sleep 0.4
|
|
243
|
+
i=$((i + 1))
|
|
244
|
+
done
|
|
245
|
+
[ -n "$xml" ] || die "UiAutomator2 returned no tree — run $(as_cmd doctor)" 3
|
|
246
|
+
die "the screen hierarchy parsed to nothing $FETCH_SOURCE_TRIES times — run $(as_cmd doctor)" 3
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# try_fetch_source — like fetch_source, but returns 1 instead of dying.
|
|
250
|
+
#
|
|
251
|
+
# For advisory work that must not kill an operation which already succeeded: a
|
|
252
|
+
# screenshot that was written is still a screenshot, even if the check that
|
|
253
|
+
# annotates it could not run.
|
|
254
|
+
try_fetch_source() {
|
|
255
|
+
local sid resp xml table rc=0
|
|
256
|
+
sid="$(cat "$(session_file)" 2>/dev/null || true)"
|
|
257
|
+
[ -n "$sid" ] || return 1
|
|
258
|
+
resp="$(curl -sS -m "$(http_timeout)" "$(ua2_base)/session/$sid/source?format=xml" 2>/dev/null)" || rc=$?
|
|
259
|
+
[ "$rc" -eq 0 ] || return 1
|
|
260
|
+
xml="$(printf '%s' "$resp" | jq -r '.value // empty' 2>/dev/null)" || return 1
|
|
261
|
+
[ -n "$xml" ] || return 1
|
|
262
|
+
table="$(printf '%s' "$xml" | awk -f "$TREE_AWK")" || return 1
|
|
263
|
+
[ -n "$table" ] || return 1
|
|
264
|
+
printf '%s' "$table"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# window_size [source] — "<w> <h>", taken from the hierarchy root.
|
|
268
|
+
#
|
|
269
|
+
# iOS pays a request for this. Android does not: UiAutomator puts the window
|
|
270
|
+
# size on the root node of the tree, so a caller that already holds a source can
|
|
271
|
+
# pass it and pay nothing. The zero-argument form exists because that is the
|
|
272
|
+
# shape the iOS adapter exposes, and a verb that works on both platforms must
|
|
273
|
+
# not have to know which one it is talking to.
|
|
274
|
+
window_size() {
|
|
275
|
+
local src="${1:-}"
|
|
276
|
+
[ -n "$src" ] || src="$(fetch_source)"
|
|
277
|
+
printf '%s' "$src" | awk -F'|' '$1 == "WINDOW" { print $2, $3; exit }'
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# device_locale — the language and region the screen is currently rendered in,
|
|
281
|
+
# normalised to en-US form.
|
|
282
|
+
#
|
|
283
|
+
# Two properties, in this order: persist.sys.locale is what the user last chose
|
|
284
|
+
# in Settings, ro.product.locale is what the image shipped with. An emulator
|
|
285
|
+
# nobody has touched has only the second — measured, not assumed.
|
|
286
|
+
#
|
|
287
|
+
# Best-effort by contract. Every caller treats "" as "unknown" and carries on,
|
|
288
|
+
# because provenance must never be the thing that fails a step.
|
|
289
|
+
device_locale() {
|
|
290
|
+
local l
|
|
291
|
+
l="$(adb_dev shell getprop persist.sys.locale 2>/dev/null | tr -d '\r' || true)"
|
|
292
|
+
[ -n "$l" ] || l="$(adb_dev shell getprop ro.product.locale 2>/dev/null | tr -d '\r' || true)"
|
|
293
|
+
printf '%s' "${l//_/-}"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# --- gestures ----------------------------------------------------------------
|
|
297
|
+
#
|
|
298
|
+
# Three primitives. Everything a verb does to the screen goes through one of
|
|
299
|
+
# them, which is what keeps tap.sh, swipe.sh, scroll.sh and back.sh free of any
|
|
300
|
+
# knowledge that UiAutomator2 exists.
|
|
301
|
+
|
|
302
|
+
# drv_tap <x> <y>
|
|
303
|
+
drv_tap() {
|
|
304
|
+
local resp err
|
|
305
|
+
resp="$(session_post '/actions' "$(printf '{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":%s,"y":%s},{"type":"pointerDown","button":0},{"type":"pause","duration":80},{"type":"pointerUp","button":0}]}]}' "$1" "$2")")"
|
|
306
|
+
err="$(session_error "$resp")"
|
|
307
|
+
[ -z "$err" ] && return 0
|
|
308
|
+
printf '%s' "$err"
|
|
309
|
+
return 1
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# drv_drag <from-x> <from-y> <to-x> <to-y> <hold-ms> [travel-ms]
|
|
313
|
+
#
|
|
314
|
+
# The hold is the press before the drag begins; a short hold reads as a flick
|
|
315
|
+
# and scrolls with momentum, a long one reads as a deliberate drag and does not.
|
|
316
|
+
# The travel time is separate and is what actually decides how far a flick
|
|
317
|
+
# carries, so both are exposed rather than conflated.
|
|
318
|
+
drv_drag() {
|
|
319
|
+
local resp err travel="${6:-300}"
|
|
320
|
+
resp="$(session_post '/actions' "$(printf '{"actions":[{"type":"pointer","id":"finger1","parameters":{"pointerType":"touch"},"actions":[{"type":"pointerMove","duration":0,"x":%s,"y":%s},{"type":"pointerDown","button":0},{"type":"pause","duration":%s},{"type":"pointerMove","duration":%s,"x":%s,"y":%s},{"type":"pointerUp","button":0}]}]}' \
|
|
321
|
+
"$1" "$2" "$5" "$travel" "$3" "$4")")"
|
|
322
|
+
err="$(session_error "$resp")"
|
|
323
|
+
[ -z "$err" ] && return 0
|
|
324
|
+
printf '%s' "$err"
|
|
325
|
+
return 1
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
# drv_open_url <url> — hand a URL to the system to route.
|
|
329
|
+
#
|
|
330
|
+
# The URL is single-quoted for the shell that runs on the device, not just for
|
|
331
|
+
# the one running here. A link with a query string reaches `am` as several words
|
|
332
|
+
# otherwise, and `&` would background half of it — the sort of thing that works
|
|
333
|
+
# for every URL in the examples and breaks on the first real deep link.
|
|
334
|
+
drv_open_url() {
|
|
335
|
+
local out
|
|
336
|
+
out="$(adb_dev shell "am start -a android.intent.action.VIEW -d '$1'" 2>&1 | tr -d '\r' || true)"
|
|
337
|
+
case "$out" in
|
|
338
|
+
*Error*|*error*|*Exception*)
|
|
339
|
+
printf '%s' "$(printf '%s' "$out" | grep -m1 -i 'error\|exception')"
|
|
340
|
+
return 1 ;;
|
|
341
|
+
esac
|
|
342
|
+
return 0
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# drv_foreground_app — the package now in front, or empty.
|
|
346
|
+
drv_foreground_app() {
|
|
347
|
+
local line
|
|
348
|
+
line="$(adb_dev shell dumpsys window 2>/dev/null | tr -d '\r' | awk '/mCurrentFocus/ { print; exit }')"
|
|
349
|
+
printf '%s' "$line" | sed -n 's/.*[ {]\([a-zA-Z0-9_.]*\)\/[a-zA-Z0-9_.]*}.*/\1/p'
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# drv_fingerprint <source> — a number that changes when the screen changes.
|
|
353
|
+
#
|
|
354
|
+
# Deliberately excludes every element's text, and is sorted. Measured on an idle
|
|
355
|
+
# screen, a fingerprint that included text changed between consecutive samples
|
|
356
|
+
# with nothing touching the device, because clocks and carousels update
|
|
357
|
+
# themselves. Structure — types, identifiers and labels — was identical across
|
|
358
|
+
# the same samples. Including text would report every gesture as a success.
|
|
359
|
+
drv_fingerprint() {
|
|
360
|
+
printf '%s' "$1" | awk -F'|' '
|
|
361
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
362
|
+
$10 != 1 { next }
|
|
363
|
+
{ print $2 $3 $4 }' | sort | cksum
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# drv_fingerprint_pos <source> — as above, but including each element's y.
|
|
367
|
+
#
|
|
368
|
+
# For scrolling, where the structure is unchanged and only the position moves.
|
|
369
|
+
drv_fingerprint_pos() {
|
|
370
|
+
printf '%s' "$1" | awk -F'|' '
|
|
371
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
372
|
+
$10 != 1 { next }
|
|
373
|
+
{ print $2 $3 $4 $7 }' | sort | cksum
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# tree_rows <source> — the tree in the one shape both platforms agree on:
|
|
377
|
+
#
|
|
378
|
+
# depth|type|id|label|value|x|y|w|h
|
|
379
|
+
#
|
|
380
|
+
# Visible, non-degenerate elements only. This is what the resilient resolver in
|
|
381
|
+
# ../lib.sh works on, and it is the reason that resolver can be written once:
|
|
382
|
+
# the scoring never learns which platform produced the rows.
|
|
383
|
+
tree_rows() {
|
|
384
|
+
printf '%s' "$1" | awk -F'|' '
|
|
385
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
386
|
+
$10 != 1 || $8 <= 0 || $9 <= 0 { next }
|
|
387
|
+
{ printf "%s|%s|%s|%s|%s|%s|%s|%s|%s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9 }'
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# --- selectors ---------------------------------------------------------------
|
|
391
|
+
#
|
|
392
|
+
# strip_id_prefix — resource ids arrive fully qualified as
|
|
393
|
+
# "com.example.app:id/login_button" and the table stores only the last part,
|
|
394
|
+
# because the package is identical for every element of the app under test. A
|
|
395
|
+
# selector written either way must work, so the selector is reduced the same
|
|
396
|
+
# way before comparison rather than the table being expanded.
|
|
397
|
+
strip_id_prefix() { printf '%s' "${1##*:id/}"; }
|
|
398
|
+
|
|
399
|
+
# match_elements <source> <kind> <value>
|
|
400
|
+
# One line per exact, visible, non-degenerate match:
|
|
401
|
+
# x|y|w|h|type|id|label|text
|
|
402
|
+
#
|
|
403
|
+
# The columns and their order are identical to the iOS adapter's, so every verb
|
|
404
|
+
# above this line is written once.
|
|
405
|
+
#
|
|
406
|
+
# Matching is exact rather than substring on purpose. A screen measured here had
|
|
407
|
+
# five elements whose id was "title"; a substring match that quietly takes the
|
|
408
|
+
# first candidate taps whichever one document order happens to put first, which
|
|
409
|
+
# is the worst available failure mode because it mostly works.
|
|
410
|
+
# holding_elements <source> <text> — the control that contains that text.
|
|
411
|
+
#
|
|
412
|
+
# Android publishes `clickable`, so the container test is exact here rather than
|
|
413
|
+
# inferred from the type. The anchor ignores visibility for the same reason as
|
|
414
|
+
# on iOS: a caption drawn inside a control is still a caption.
|
|
415
|
+
holding_elements() {
|
|
416
|
+
printf '%s' "$1" | awk -F'|' -v val="$2" '
|
|
417
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
418
|
+
{
|
|
419
|
+
n++
|
|
420
|
+
X[n] = $6; Y[n] = $7; W[n] = $8; H[n] = $9
|
|
421
|
+
T[n] = $2; ID[n] = $3; LB[n] = $4; V[n] = $5
|
|
422
|
+
VIS[n] = $10; CLK[n] = $13
|
|
423
|
+
if (($4 == val || $5 == val) && $8 > 0 && $9 > 0) A[++an] = n
|
|
424
|
+
}
|
|
425
|
+
END {
|
|
426
|
+
for (i = 1; i <= an; i++) {
|
|
427
|
+
a = A[i]; best = 0; bestArea = 0
|
|
428
|
+
for (j = 1; j <= n; j++) {
|
|
429
|
+
if (VIS[j] != 1 || CLK[j] != 1 || W[j] <= 0 || H[j] <= 0) continue
|
|
430
|
+
if (X[j] <= X[a] && Y[j] <= Y[a] \
|
|
431
|
+
&& X[j] + W[j] >= X[a] + W[a] && Y[j] + H[j] >= Y[a] + H[a]) {
|
|
432
|
+
area = W[j] * H[j]
|
|
433
|
+
if (best == 0 || area < bestArea) { best = j; bestArea = area }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (best) {
|
|
437
|
+
key = X[best] "|" Y[best] "|" W[best] "|" H[best]
|
|
438
|
+
if (!(key in seen)) {
|
|
439
|
+
seen[key] = 1
|
|
440
|
+
printf "%s|%s|%s|%s|%s|%s|%s|%s\n",
|
|
441
|
+
X[best], Y[best], W[best], H[best], T[best], ID[best], LB[best], V[best]
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}'
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# keyboard_blocks <source> <x> <y> <w> <h> — "x y w h" of the keyboard when it
|
|
449
|
+
# stands between the caller and that element, nothing when it does not.
|
|
450
|
+
#
|
|
451
|
+
# The source argument is unused, and no membership test is needed: uiautomator
|
|
452
|
+
# dumps the application's window, the input method is a window of its own, so
|
|
453
|
+
# nothing this adapter can resolve is ever a key. It costs one adb round trip,
|
|
454
|
+
# measured at 70ms against the emulator this was built on, which is why the
|
|
455
|
+
# caller asks only when it is about to touch the screen.
|
|
456
|
+
#
|
|
457
|
+
# The touchable region is the right rectangle, not the frame — the IME window's
|
|
458
|
+
# frame spans the whole display while only its lower part accepts touches. When
|
|
459
|
+
# the keyboard is down the window is not among the visible ones and this prints
|
|
460
|
+
# nothing. A region split into several rectangles reports its first, so a split
|
|
461
|
+
# keyboard is under-reported rather than over-reported, which is the safe
|
|
462
|
+
# direction for a check that refuses to act.
|
|
463
|
+
keyboard_blocks() {
|
|
464
|
+
local kb cx cy
|
|
465
|
+
kb="$(keyboard_region)"
|
|
466
|
+
[ -n "$kb" ] || return 0
|
|
467
|
+
cx=$(( $2 + $4 / 2 ))
|
|
468
|
+
cy=$(( $3 + $5 / 2 ))
|
|
469
|
+
printf '%s\n' "$kb" | awk -v cx="$cx" -v cy="$cy" '
|
|
470
|
+
cx >= $1 && cx <= $3 && cy >= $2 && cy <= $4 { print $1, $2, $3 - $1, $4 - $2 }'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# drv_keyboard_dismiss — ask the keyboard to go away. BACK rather than ESCAPE:
|
|
474
|
+
# while the input method is up, BACK is consumed by it and closes it without
|
|
475
|
+
# navigating, which is what a person does and what every keyboard supports.
|
|
476
|
+
drv_keyboard_dismiss() {
|
|
477
|
+
adb_dev shell input keyevent 4 >/dev/null 2>&1
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
# keyboard_region — "left top right bottom" of the input method's touchable
|
|
481
|
+
# region, or nothing when the keyboard is down.
|
|
482
|
+
keyboard_region() {
|
|
483
|
+
adb_dev shell dumpsys window InputMethod 2>/dev/null \
|
|
484
|
+
| sed -n 's/.*InputMethod, frame=\[[^]]*\], touchableRegion=SkRegion((\([0-9]*\),\([0-9]*\),\([0-9]*\),\([0-9]*\))).*/\1 \2 \3 \4/p' \
|
|
485
|
+
| head -1
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# keyboard_rect <source> — "x y w h", or nothing when it is down. The source is
|
|
489
|
+
# unused; see keyboard_blocks.
|
|
490
|
+
keyboard_rect() {
|
|
491
|
+
keyboard_region | awk '{ print $1, $2, $3 - $1, $4 - $2 }'
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# unhittable_twin <source> <kind> <value> <x> <y> <w> <h> — the control this
|
|
495
|
+
# selector really meant, when the caller is about to settle for a label instead.
|
|
496
|
+
# See the iOS adapter for the failure this exists for; here `clickable` answers
|
|
497
|
+
# exactly what type has to be inferred from there.
|
|
498
|
+
unhittable_twin() {
|
|
499
|
+
local val="$3"
|
|
500
|
+
[ "$2" = id ] && val="$(strip_id_prefix "$3")"
|
|
501
|
+
printf '%s' "$1" | awk -F'|' -v kind="$2" -v val="$val" \
|
|
502
|
+
-v rx="$4" -v ry="$5" -v rw="$6" -v rh="$7" '
|
|
503
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
504
|
+
$8 <= 0 || $9 <= 0 { next }
|
|
505
|
+
{
|
|
506
|
+
if (kind == "id") { named = ($3 == val) }
|
|
507
|
+
else if (kind == "kind") { named = ($2 == val) }
|
|
508
|
+
else if (kind == "text") { named = ($5 == val) }
|
|
509
|
+
else { named = ($4 == val) }
|
|
510
|
+
if (!named) next
|
|
511
|
+
if ($6 == rx && $7 == ry && $8 == rw && $9 == rh) { if ($13 == 1) self_ok = 1; next }
|
|
512
|
+
if ($10 != 1 && $13 == 1 && !found) {
|
|
513
|
+
found = 1; desc = sprintf("%s at %s,%s,%s,%s", $2, $6, $7, $8, $9)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
END { if (!self_ok && found) print desc }'
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
# named_anywhere <source> <kind> <value> — every element the selector names, with
|
|
520
|
+
# the visibility filter off:
|
|
521
|
+
#
|
|
522
|
+
# x|y|w|h|type|elements-sharing-that-exact-rect|visible
|
|
523
|
+
#
|
|
524
|
+
# Reached only after match_elements has already come back empty, so anything
|
|
525
|
+
# here is either invisible or degenerate — which is precisely the distinction
|
|
526
|
+
# `nothing on screen contains it` was failing to draw. See invisible_note.
|
|
527
|
+
named_anywhere() {
|
|
528
|
+
local val="$3"
|
|
529
|
+
[ "$2" = id ] && val="$(strip_id_prefix "$3")"
|
|
530
|
+
printf '%s' "$1" | awk -F'|' -v kind="$2" -v val="$val" '
|
|
531
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
532
|
+
{ rows[NR] = $0; key = $6 "," $7 "," $8 "," $9; same[key]++ }
|
|
533
|
+
END {
|
|
534
|
+
for (i = 1; i <= NR; i++) {
|
|
535
|
+
n = split(rows[i], f, "|")
|
|
536
|
+
if (n < 10) continue
|
|
537
|
+
if (kind == "id") named = (f[3] == val)
|
|
538
|
+
else if (kind == "kind") named = (f[2] == val)
|
|
539
|
+
else if (kind == "text") named = (f[5] == val)
|
|
540
|
+
else if (kind == "any") named = (f[3] == val || f[4] == val || f[5] == val)
|
|
541
|
+
else named = (f[4] == val)
|
|
542
|
+
if (!named) continue
|
|
543
|
+
key = f[6] "," f[7] "," f[8] "," f[9]
|
|
544
|
+
printf "%s|%s|%s|%s|%s|%s|%s\n", f[6], f[7], f[8], f[9], f[2], same[key], (f[10] == 1 ? 1 : 0)
|
|
545
|
+
}
|
|
546
|
+
}'
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# text_anywhere <source> <text> — does that exact text exist at all, visible or
|
|
550
|
+
# not? Used only to tell a `holding:` failure apart from a missing screen.
|
|
551
|
+
text_anywhere() {
|
|
552
|
+
printf '%s' "$1" | awk -F'|' -v val="$2" '
|
|
553
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
554
|
+
($4 == val || $5 == val) { printf "%s|%s|%s|%s|%s\n", $6, $7, $8, $9, $2 }'
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
match_elements() {
|
|
558
|
+
local val="$3"
|
|
559
|
+
if [ "$2" = holding ]; then holding_elements "$1" "$3"; return; fi
|
|
560
|
+
[ "$2" = id ] && val="$(strip_id_prefix "$3")"
|
|
561
|
+
printf '%s' "$1" | awk -F'|' -v kind="$2" -v val="$val" '
|
|
562
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
563
|
+
$10 != 1 || $8 <= 0 || $9 <= 0 { next }
|
|
564
|
+
{
|
|
565
|
+
if (kind == "id") { ok = ($3 == val) }
|
|
566
|
+
else if (kind == "kind") { ok = ($2 == val) }
|
|
567
|
+
else if (kind == "text") { ok = ($5 == val) }
|
|
568
|
+
else { ok = ($4 == val) }
|
|
569
|
+
if (ok) printf "%s|%s|%s|%s|%s|%s|%s|%s\n", $6, $7, $8, $9, $2, $3, $4, $5
|
|
570
|
+
}'
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
# kinds_on_screen <source> — the element types present, commonest first, with
|
|
574
|
+
# counts. See the iOS adapter: candidate_elements searches identifiers, labels
|
|
575
|
+
# and values, so for a `kind:` that matched nothing it has nothing to say.
|
|
576
|
+
kinds_on_screen() {
|
|
577
|
+
printf '%s' "$1" | awk -F'|' '
|
|
578
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
579
|
+
$10 != 1 || $8 <= 0 || $9 <= 0 { next }
|
|
580
|
+
{ n[$2]++ }
|
|
581
|
+
END { for (k in n) printf "%s x%s\n", k, n[k] }' \
|
|
582
|
+
| sort -t'x' -k2 -rn | take_lines 8 | join_lines ', '
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# candidate_elements <source> <value> [limit]
|
|
586
|
+
# Near misses, for the message printed when an exact match fails. Being told
|
|
587
|
+
# what is actually on screen is what lets a caller recover without a screenshot.
|
|
588
|
+
# candidate_elements <source> <value> [n] — what is on screen that *contains*
|
|
589
|
+
# the value, for the message printed when nothing matched it exactly.
|
|
590
|
+
#
|
|
591
|
+
# A candidate that some field matches EXACTLY is named with the selector that
|
|
592
|
+
# would reach it. Reported from a real session: `tap text:8` was refused and the
|
|
593
|
+
# suggestion list offered `Key "8"` — which is exactly the thing being asked
|
|
594
|
+
# for, matching on its label rather than its value. Listing the answer without
|
|
595
|
+
# saying how to spell it is a riddle, not a hint.
|
|
596
|
+
candidate_elements() {
|
|
597
|
+
printf '%s' "$1" | awk -F'|' -v val="$2" '
|
|
598
|
+
function lower(s) { return tolower(s) }
|
|
599
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
600
|
+
$10 != 1 || $8 <= 0 || $9 <= 0 { next }
|
|
601
|
+
($3 == "" && $4 == "" && $5 == "") { next }
|
|
602
|
+
{
|
|
603
|
+
hay = lower($3 " " $4 " " $5)
|
|
604
|
+
if (index(hay, lower(val)) == 0) next
|
|
605
|
+
out = $2
|
|
606
|
+
if ($3 != "") out = out " #" $3
|
|
607
|
+
if ($4 != "") out = out " \"" $4 "\""
|
|
608
|
+
if ($5 != "" && $5 != $4) out = out " =" $5
|
|
609
|
+
if ($3 == val) out = out " → try id:" val
|
|
610
|
+
else if ($4 == val) out = out " → try label:" val
|
|
611
|
+
else if ($5 == val) out = out " → try text:" val
|
|
612
|
+
print out
|
|
613
|
+
}' | head -n "${3:-5}"
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
# --- the normalised screen -----------------------------------------------------
|
|
617
|
+
#
|
|
618
|
+
# snapshot_rows — see the iOS adapter for the format, which is the point: the
|
|
619
|
+
# two produce the same twelve fields and scripts/snapshot.awk cannot tell which
|
|
620
|
+
# driver was on the other end.
|
|
621
|
+
#
|
|
622
|
+
# This is a re-shaping rather than a second fetch. tree.awk already emits
|
|
623
|
+
# fourteen columns — depth|class|id|label|text|x|y|w|h|visible|enabled|password|
|
|
624
|
+
# clickable|hint — so the work here is renumbering, deriving the parent and
|
|
625
|
+
# folding three booleans into the flags field.
|
|
626
|
+
#
|
|
627
|
+
# NOT YET RUN AGAINST A DEVICE. There was no Android phone attached when this
|
|
628
|
+
# was written, and docs/RELEASE.md says so. The iOS side is proven; this is not,
|
|
629
|
+
# and a reader should treat it as a claim rather than a fact until it is.
|
|
630
|
+
snapshot_rows() {
|
|
631
|
+
local src w h orient
|
|
632
|
+
src="$(fetch_source)"
|
|
633
|
+
|
|
634
|
+
read -r w h <<< "$(window_size "$src")"
|
|
635
|
+
w="${w:-0}"; h="${h:-0}"
|
|
636
|
+
if [ "$w" -gt "$h" ] 2>/dev/null; then orient=landscape; else orient=portrait; fi
|
|
637
|
+
printf 'WINDOW|%s|%s|%s\n' "$w" "$h" "$orient"
|
|
638
|
+
|
|
639
|
+
printf '%s\n' "$src" | awk -F'|' -v OFS='|' '
|
|
640
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
641
|
+
{
|
|
642
|
+
n++
|
|
643
|
+
depth = $1 + 0
|
|
644
|
+
stack[depth] = n
|
|
645
|
+
parent = (depth == 0) ? 0 : stack[depth - 1] + 0
|
|
646
|
+
|
|
647
|
+
# Android publishes `clickable` on the element, so "actionable" here is a
|
|
648
|
+
# fact rather than the type-based inference the iOS adapter has to make.
|
|
649
|
+
flags = ""
|
|
650
|
+
if ($10 == 1) flags = flags (flags == "" ? "" : ",") "visible"
|
|
651
|
+
if ($11 == 1) flags = flags (flags == "" ? "" : ",") "enabled"
|
|
652
|
+
if ($13 == 1) flags = flags (flags == "" ? "" : ",") "actionable"
|
|
653
|
+
|
|
654
|
+
print n, depth, parent, $2, $3, $4, $5, $6, $7, $8, $9, flags
|
|
655
|
+
}'
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
# content_digest <source> — see the iOS adapter. Rows here, not JSON:
|
|
659
|
+
# depth|class|id|label|text|x|y|w|h|visible|enabled|password|clickable|hint
|
|
660
|
+
content_digest() {
|
|
661
|
+
printf '%s' "$1" | awk -F'|' '
|
|
662
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
663
|
+
$10 != 1 { next }
|
|
664
|
+
{ print $2 "\037" $4 "\037" $5 }' | cksum | awk '{ print $1 }'
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
# capture_png <path> — see the iOS adapter.
|
|
668
|
+
#
|
|
669
|
+
# `exec-out` rather than `shell`: adb's shell channel translates line endings,
|
|
670
|
+
# which corrupts a PNG in a way that is invisible until something tries to
|
|
671
|
+
# decode it.
|
|
672
|
+
capture_png() {
|
|
673
|
+
local out="$1"
|
|
674
|
+
adb_dev exec-out screencap -p > "$out" 2>/dev/null || return 3
|
|
675
|
+
[ -s "$out" ] || { rm -f "$out"; return 3; }
|
|
676
|
+
[ "$(head -c 8 "$out" | od -An -tx1 | tr -d ' \n')" = "89504e470d0a1a0a" ] \
|
|
677
|
+
|| { rm -f "$out"; return 3; }
|
|
678
|
+
return 0
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
# logs_since <unix-seconds> — see the iOS adapter for the two rules this obeys:
|
|
682
|
+
# it never starts a collector, and it never fails.
|
|
683
|
+
#
|
|
684
|
+
# The app filter is the package name, which Android does publish and which
|
|
685
|
+
# logcat lines carry, so this one is a match rather than the heuristic the iOS
|
|
686
|
+
# side has to make do with.
|
|
687
|
+
logs_since() {
|
|
688
|
+
local since="$1" f pkg out
|
|
689
|
+
f="$(state_dir)/logcat.log"
|
|
690
|
+
[ -s "$f" ] || return 0
|
|
691
|
+
out="$(awk -F'\t' -v c="$since" '$1 >= c { sub(/^[0-9]+\t/, ""); print }' "$f" 2>/dev/null || true)"
|
|
692
|
+
[ -n "$out" ] || return 0
|
|
693
|
+
pkg="$(cfg '.app.package' '')"
|
|
694
|
+
if [ -n "$pkg" ]; then
|
|
695
|
+
out="$(printf '%s\n' "$out" | grep -i -F -- "$pkg" || true)"
|
|
696
|
+
fi
|
|
697
|
+
[ -n "$out" ] || return 0
|
|
698
|
+
printf '%s\n' "$out" | sed '/^[[:space:]]*$/d' | tail -8
|
|
699
|
+
}
|