#!/bin/sh
# adb_uiautomator.sh
# 2026-04-20
# by Gernot Walzl
# Up to now, this is just an idea on
# how to tap buttons of various apps over adb.
# The device needs to be in unlocked state.
# Requirements:
# apt install adb libxml2-utils
# Notice: The position in xpath is 1-based, not 0-based.
set -e
dump () {
adb exec-out "uiautomator dump /dev/tty > /dev/null" \
| xmllint --format -
}
xpath () {
adb exec-out "uiautomator dump /dev/tty > /dev/null" \
| xmllint --xpath "$1" -
}
input_tap () {
XPATH=$1
read X_MIN Y_MIN X_MAX Y_MAX <<EOF
$(adb exec-out "uiautomator dump /dev/tty > /dev/null" \
| xmllint --xpath "$XPATH/@bounds" - \
| sed 's/.*\[\(.*\),\(.*\)\]\[\(.*\),\(.*\)\].*/\1 \2 \3 \4/')
EOF
if [ -z "$Y_MAX" ]; then
echo "WARNING: '$XPATH' not found."
return 1
else
adb shell "input tap $((($X_MIN + $X_MAX)/2)) $((($Y_MIN + $Y_MAX)/2))"
fi
}
input_text () {
adb shell "input text '$1'"
}
handle_documentsui_save () {
FILENAME=$1
input_tap '//node[@resource-id="com.android.documentsui:id/horizontal_breadcrumb"]/node[1]/node[1]'
input_tap '//node[@resource-id="com.android.documentsui:id/container_save"]/node[1]/node[2]'
input_text "$FILENAME"
input_tap '//node[@resource-id="com.android.documentsui:id/container_save"]/node[1]/node[3]'
}
wait_write () {
FILEPATH=$1
sleep 3
FILESIZE_PREV=0
FILESIZE_CURR=$(adb shell "stat --format %s '$FILEPATH'")
while [ "$FILESIZE_PREV" -ne "$FILESIZE_CURR" ]; do
sleep 3
FILESIZE_PREV=$FILESIZE_CURR
FILESIZE_CURR=$(adb shell "stat --format %s '$FILEPATH'")
done
}
export_contacts () {
APP='com.android.contacts'
REMOTEHOST=$(adb shell "echo \$HOSTNAME")
DATE=$(date +%Y-%m-%d)
FILENAME="contacts_${REMOTEHOST}_${DATE}.vcf"
adb shell "am stop-app $APP"
adb shell "am start $APP"
input_tap '//node[@resource-id="com.android.contacts:id/toolbar"]/node[1]'
input_tap '//node[@resource-id="com.android.contacts:id/nav_settings"]'
input_tap '//node[@package="com.android.contacts" and @resource-id="android:id/list"]/node[9]'
input_tap '//node[@package="com.android.contacts" and @resource-id="android:id/select_dialog_listview"]/node[1]'
handle_documentsui_save "$FILENAME"
wait_write "/sdcard/$FILENAME"
adb shell "mkdir -p /sdcard/Backup/Contacts"
adb shell "mv '/sdcard/$FILENAME' /sdcard/Backup/Contacts"
adb shell "am stop-app $APP"
}
export_messages () {
APP='com.github.tmo1.sms_ie'
REMOTEHOST=$(adb shell "echo \$HOSTNAME")
DATE=$(date +%Y-%m-%d)
FILENAME="messages_${REMOTEHOST}_${DATE}.zip"
adb shell "am stop-app $APP"
adb shell "am start $APP/.MainActivity"
input_tap '//node[@resource-id="com.github.tmo1.sms_ie:id/export_messages_button"]'
handle_documentsui_save "$FILENAME"
wait_write "/sdcard/$FILENAME"
adb shell "mkdir -p /sdcard/Backup/Messages"
adb shell "mv '/sdcard/$FILENAME' /sdcard/Backup/Messages"
adb shell "am stop-app $APP"
}
export_calendar () {
APP='org.sufficientlysecure.ical'
REMOTEHOST=$(adb shell "echo \$HOSTNAME")
DATE=$(date +%Y-%m-%d)
adb shell "am stop-app $APP"
adb shell "am start $APP/.ui.MainActivity"
adb shell "mkdir -p /sdcard/Backup/Calendar"
input_tap '//node[@resource-id="org.sufficientlysecure.ical:id/SpinnerChooseCalendar"]'
NUM_CALS=$(xpath 'count(//node[@package="org.sufficientlysecure.ical" and @class="android.widget.CheckedTextView"])')
CAL_IDX=1
while [ "$CAL_IDX" -le "$NUM_CALS" ]; do
if [ "$CAL_IDX" -gt 1 ]; then
input_tap '//node[@resource-id="org.sufficientlysecure.ical:id/SpinnerChooseCalendar"]'
fi
input_tap "//node[@package=\"org.sufficientlysecure.ical\" and @class=\"android.widget.ListView\"]/node[$CAL_IDX]"
input_tap '//node[@resource-id="org.sufficientlysecure.ical:id/SaveButton"]'
input_tap '//node[@resource-id="android:id/buttonPanel"]/node[1]/node[1]'
CAL_NAME=$(xpath '//node[@package="org.sufficientlysecure.ical" and @class="android.widget.EditText"]/@text' \
| sed 's/.*"\(.*\)".*/\1/')
input_tap '//node[@resource-id="android:id/buttonPanel"]/node[1]/node[3]'
wait_write "/sdcard/${CAL_NAME}.ics"
adb shell "mv '/sdcard/${CAL_NAME}.ics' '/sdcard/Backup/Calendar/${CAL_NAME}_${REMOTEHOST}_${DATE}.ics'"
CAL_IDX=$(($CAL_IDX + 1))
done
adb shell "am stop-app $APP"
}
case "$1" in
'dump')
dump
;;
'export-contacts')
export_contacts
;;
'export-messages')
export_messages
;;
'export-calendar')
export_calendar
;;
'export-all')
export_contacts
export_messages
export_calendar
;;
*)
dump
esac