#!/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