# WhatsApp ADB Container Appium 3 + UiAutomator2-driver in Docker, voor geautomatiseerde aansturing van een fysieke Android-telefoon via ADB. Ontworpen voor de **Nexus 6P** (niet geroot) met WhatsApp. Twee containers: | Container | Functie | Poort | |-----------|---------|-------| | `appium-adb` | ADB-server + Appium | `:4723` | | `wa-appium-api` | Express REST API (fallback voor wwebjs-api) | `:3010` | --- ## Architectuur ``` ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ appium-adb │ │ wa-appium-api │ │ │ │ │ │ entrypoint.sh: ────────────┤ │ server.js: │ │ adb start-server │ │ Express REST API │ │ adb connect/usb │ │ WebDriverIO → Appium │ │ wifi_sleep_policy=2 │ │ /client/getChats/:ses │ │ svc power stayon usb │ │ /chat/fetchMessages/:ses │ │ dumpsys deviceidle off │ │ /client/sendMessage/:ses │ │ reconnect loop (30s) │ │ │ │ appium server :4723 ─────┼─────┤ localhost:4723 │ │ │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ USB │ REST (localhost:3010) ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Nexus 6P │ │ Clients │ │ (ENU7N15B…) │ │ (scripts, │ │ WhatsApp │ │ curl, etc) │ └──────────────┘ └──────────────┘ ``` **Fallback-strategie:** De primaire WhatsApp-laag is [[Whatsapp rest-api|wwebjs-api]] (`:3001`) via WhatsApp Web. Als die sessie wegvalt (geblokkeerd nummer, browser-crash), springt wa-appium-api (`:3010`) in — die de telefoon direct via USB aanstuurt. --- ## Snelstart ### USB-modus (telefoon aan de kabel) Zet USB debugging aan op de telefoon (Settings > Developer options > USB debugging). ```bash # check of de telefoon zichtbaar is adb devices # → ENU7N15B14000616 device # start beide containers docker compose up -d # smoke test node smoke-test.js # health check curl http://127.0.0.1:3010/health ``` ### TCP/IP-modus (telefoon remote) ```bash # eenmalig na elke reboot (kabel nodig) adb usb && adb tcpip 5555 # kabel los, start met PHONE_IP PHONE_IP=192.168.5.100 docker compose up -d ``` --- ## Environment variabelen ### appium-adb | Variabele | Default | Beschrijving | |------------|---------|-------------| | `PHONE_IP` | *(leeg)* | IP voor TCP/IP-modus. Leeg = USB-modus | | `ADB_PORT` | `5555` | ADB-poort (na `adb tcpip `) | ### wa-appium-api | Variabele | Default | Beschrijving | |---------------|-------------|-------------| | `APPIUM_HOST` | `127.0.0.1` | Appium server host | | `APPIUM_PORT` | `4723` | Appium server poort | | `PORT` | `3010` | REST API luisterpoort | Kopieer `.env.example` naar `.env` om defaults aan te passen. --- ## USB-passthrough De compose mount `/dev/bus/usb` in de `appium-adb` container. De container draait als root, dus USB-devices zijn direct toegankelijk. Het ADB-serverproces detecteert USB-hotplug automatisch. **Eerste keer op een nieuwe host:** 1. udev-regel aanmaken: `SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0660", GROUP="plugdev"` 2. Gebruiker toevoegen aan `plugdev`-groep 3. Kabel los/koppelen of `chown root:plugdev /dev/bus/usb/XXX/YYY` 4. RSA-vingerafdruk accepteren op de telefoon ("Altijd toestaan") --- ## wa-appium-api (fallback) REST API die wwebjs-api-endpoints spiegelt, maar via Appium de Android-telefoon aanstuurt in plaats van WhatsApp Web. Trager en layout-gevoelig, maar werkt ook als de Web-sessie geblokkeerd is. ### Endpoints #### `GET /health` ```bash curl http://127.0.0.1:3010/health {"status":"ok","sessionId":"..."} ``` #### `GET /client/getChats/:session` Haalt alle chats op van het WhatsApp-hoofdscherm. ```bash curl http://127.0.0.1:3010/client/getChats/Eddy { "chats": [ { "name": "+31 6 12345678", "lastMessage": "Foto", "timestamp": "17:28", "unread": 0 }, ... ] } ``` | Veld | Type | Beschrijving | |------|------|-------------| | `name` | string | Contactnaam of telefoonnummer | | `lastMessage` | string | Laatste bericht preview (leeg bij media) | | `timestamp` | string | Tijdstip laatste bericht | | `unread` | int | Aantal ongelezen berichten | **Limitaties:** alleen zichtbare chats op het scherm. Scrollen wordt nog niet ondersteund. Media-berichten tonen lege preview of `NaN`. #### `POST /chat/fetchMessages/:session` Haalt berichten op uit een specifieke chat. ```bash curl -X POST http://127.0.0.1:3010/chat/fetchMessages/Eddy \ -H 'Content-Type: application/json' \ -d '{"chatId": "31612345678"}' { "messages": [ { "text": "Hallo!", "timestamp": "10:00" }, ... ] } ``` | Parameter | Type | Verplicht | Beschrijving | |-----------|------|-----------|-------------| | `chatId` | string | ja | Telefoonnummer (met of zonder `@c.us`) of contactnaam | **Limitaties:** alleen tekstberichten worden opgehaald (geen media, voice, of system messages). Alleen zichtbare berichten in de weergave (scrollt niet). #### `POST /client/sendMessage/:session` Verstuurt een tekstbericht. ```bash curl -X POST http://127.0.0.1:3010/client/sendMessage/Eddy \ -H 'Content-Type: application/json' \ -d '{ "chatId": "31612345678", "content": "Hallo vanuit de API!" }' {"status":"sent","content":"Hallo vanuit de API!"} ``` | Parameter | Type | Verplicht | Beschrijving | |-----------|------|-----------|-------------| | `chatId` | string | ja | Telefoonnummer (zonder `@c.us`) of contactnaam | | `content` | string | ja | Berichttekst | **Werkt met:** individuele chats. Groepsberichten nog niet getest. ### Hoe het werkt (intern) De API gebruikt WebDriverIO om via Appium de WhatsApp Android-app aan te sturen: ``` getChats: startActivity → lees contact_row_container elementen → parse naam/bericht/tijd/ongelezen openChat: startActivity → klik search_bar_inner_layout → type in search_input → klik contact_row_container sendMessage: openChat → setValue in entry → klik send → goHome fetchMessages: openChat → lees main_layout → message_text + date ``` **Gebruikte resource-IDs** (WhatsApp v2.25+, kunnen wijzigen per versie): | Resource-ID | Functie | |-------------|---------| | `com.whatsapp:id/search_bar_inner_layout` | Zoekbalk op home | | `com.whatsapp:id/search_input` | Zoekveld | | `com.whatsapp:id/contact_row_container` | Chatrij in lijst | | `com.whatsapp:id/conversations_row_contact_name` | Contactnaam | | `com.whatsapp:id/single_msg_tv` | Bericht preview (individueel) | | `com.whatsapp:id/msg_from_tv` | Bericht preview (community/groep) | | `com.whatsapp:id/conversations_row_date` | Tijdstip | | `com.whatsapp:id/conversations_row_unread_indicator` | Ongelezen indicator | | `com.whatsapp:id/entry` | Tekstinvoerveld | | `com.whatsapp:id/send` | Verzendknop | | `com.whatsapp:id/main_layout` | Berichtcontainer in chat | | `com.whatsapp:id/message_text` | Berichttekst | | `com.whatsapp:id/date` | Datum/tijd van bericht | --- ## Containers in detail ### appium-adb **Image build:** - Base: `node:lts-bookworm-slim` - OpenJDK 17 + Android platform-tools (Google) + Appium 3.x + uiautomator2-driver - Zie `Dockerfile` **Entrypoint (`entrypoint.sh`):** 1. `adb start-server` 2. Detecteer USB (`adb -d`) of TCP (`adb connect`) 3. Pas instellingen toe (non-root): `wifi_sleep_policy=2`, `svc power stayon usb`, `deviceidle disable` 4. Start reconnect loop op achtergrond (elke 30s check of device nog zichtbaar is) 5. `exec appium --address 0.0.0.0 --port 4723 --allow-cors --relaxed-security` ### wa-appium-api **Image build:** - Base: `node:lts-bookworm-slim` - Express + WebDriverIO - Zie `wa-appium-api/Dockerfile` **Belangrijke code in `wa-appium-api/server.js`:** - `getDriver()` — maakt Appium-sessie, herconnect bij drop - `ensureWhatsApp(d)` — start WhatsApp als het niet op de voorgrond staat - `goHome(d)` — navigeer naar WhatsApp home - `openChat(d, query)` — zoek chat via search, open 'm - Finders gebruiken UiAutomator2 `new UiSelector()` via WebDriverIO --- ## Troubleshooting ### "Could not find a connected Android device" ``` docker logs appium-adb — check of adb connect/usb lukt ``` - Zit de USB-kabel er goed in? Check met `adb devices` op de **host**. - USB debugging aan op de telefoon? (Developer options) - Nieuwe RSA-key? Kabel los/koppelen en "Altijd toestaan" op telefoon. - Voor TCP/IP: `adb connect :5555` testen vanaf host. ### ADB-serverversie-mismatch ADB in de container (Google platform-tools) en op de host kunnen verschillen. Meestal geen probleem. Bij vreemd gedrag: ```bash # host ADB updaten sudo -A dnf install android-tools # Fedora # of gebruik ADB uit container docker exec appium-adb adb devices ``` ### Appium vindt uiautomator2-driver niet ``` driver uiautomator2 is not installed ``` ```bash docker exec appium-adb appium driver install uiautomator2 # of rebuild docker compose build --no-cache appium ``` ### Telefoon niet meer bereikbaar na reboot Na een reboot moet USB debugging opnieuw bevestigd worden (RSA-vingerafdruk): ```bash adb devices # zo niet: kabel los/koppelen, bevestig op telefoon # voor TCP/IP: adb usb && adb tcpip 5555 ``` ### Verbinding valt weg na verloop van tijd De entrypoint past toe: ``` settings put global wifi_sleep_policy 2 svc power stayon usb dumpsys deviceidle disable ``` Bij USB-verbinding zou dit zelden moeten gebeuren. Check: 1. `docker logs appium-adb` — zie je reconnect-pogingen? 2. **Kabel:** probeer een andere. Goedkope kabels geven drops. 3. **USB-poort:** direct op moederbord, niet via hub. 4. **Stay awake:** zet aan in Developer options op de telefoon. 5. **Settings opnieuw:** `docker restart appium-adb`. 6. **Laatste redmiddel:** rooten + `iw wlan0 set power_save off` (alleen TCP/IP). ### wa-appium-api fault ```bash docker logs wa-appium-api # als server.js is aangepast, rebuild nodig: docker compose build wa-appium-api docker compose up -d wa-appium-api ``` ### wa-appium-api: session lost De `getDriver()` functie herconnect automatisch bij een verbroken sessie. Als dat niet werkt: ```bash docker restart wa-appium-api ``` --- ## Android Notification Bridge (event-driven) Dit systeem maakt **real-time** notificatie van binnenkomende WhatsApp-berichten mogelijk zonder te pollen via Appium. Het bestaat uit twee componenten: ``` WhatsApp → Android Notification → listener-app (APK) → HTTP POST → wa-appium-api → subscribers ``` ### Architectuur 1. **Android app** (`android-notification-listener/`) luistert naar WhatsApp-notificaties 2. Bij elk bericht stuurt de app een HTTP POST naar de API-container 3. De API valideert het gedeelde secret en stuurt het bericht door naar subscribers 4. Subscribers registreren zich via een apart endpoint ### Android-app: bouwen en installeren **Builden** (vereist Android SDK lokaal of op een build-server): ```bash cd android-notification-listener ./gradlew assembleDebug adb install app/build/outputs/apk/debug/app-debug.apk ``` Als `gradlew` niet bestaat, genereer het eerst: ```bash gradle wrapper --gradle-version 8.5 ``` **Notificatietoegang verlenen** (eenmalig via ADB): ```bash adb shell settings put secure enabled_notification_listeners \ com.eddydevink.whatsapplistener/com.eddydevink.whatsapplistener.WhatsAppNotificationListener ``` Verifiëren: ```bash adb shell settings get secure enabled_notification_listeners # moet de package/service naam bevatten ``` **Configureren:** open de app op de telefoon ("WA Notification Bridge"), vul het webhook-URL en shared secret in. ### Webhook configuratie (API-zijde) Zet in `.env` of als environment variable: ```bash WEBHOOK_SHARED_SECRET=verander-mij # moet gelijk zijn aan de Android-app ``` ### Webhook endpoints #### `POST /webhooks/whatsapp/incoming` Ontvangt berichten van de Android notification-listener. ```bash curl -X POST http://127.0.0.1:3010/webhooks/whatsapp/incoming \ -H 'Content-Type: application/json' \ -H 'X-Webhook-Secret: verander-mij' \ -d '{ "sender": "+31 6 12345678", "text": "Hallo!", "chatId": "Eddy de Vink", "timestamp": 1700000000000 }' ``` | Header | Waarde | |--------|--------| | `X-Webhook-Secret` | Gedeeld geheim (moet matchen met `WEBHOOK_SHARED_SECRET`) | | Veld | Type | Verplicht | Beschrijving | |------|------|-----------|-------------| | `sender` | string | ja | Naam of nummer van afzender | | `text` | string | ja | Berichttekst | | `chatId` | string | ja | Chat-naam zoals getoond in notificatie | | `timestamp` | long | nee | Unix timestamp (ms) | #### `POST /webhooks/subscribe` Registreer een subscriber (interne service die notificaties wil ontvangen). ```bash curl -X POST http://127.0.0.1:3010/webhooks/subscribe \ -H 'Content-Type: application/json' \ -d '{"url": "http://192.168.5.252:3003/webhook"}' ``` #### `GET /webhooks/subscribers` Lijst van actieve subscribers. ```bash curl http://127.0.0.1:3010/webhooks/subscribers ``` ### Beperkingen - **Samengevoegde notificaties:** als meerdere berichten in dezelfde chat binnenkomen voordat de eerste is verwerkt, kan Android één samengevoegde notificatie sturen. De listener verwerkt dan alle `messages` uit de `MessagingStyle`, dus individuele berichten gaan niet verloren. - **Alleen push-notificaties:** berichten die binnenkomen terwijl de app open is (dus geen notificatie genereren) worden niet gevangen. Gebruik dan Appium-polling als aanvulling. - **Eén telefoon, één listener:** de app luistert naar alle chats. --- ## Onderhoud ### Rebuild en restart ```bash # alles docker compose build --no-cache docker compose up -d # alleen Appium (bv. na driver update) docker compose build --no-cache appium docker compose up -d appium # alleen API (bv. na code wijziging) docker compose build wa-appium-api docker compose up -d wa-appium-api ``` ### Logs ```bash docker logs appium-adb -f docker logs wa-appium-api -f ``` ### Phone settings resetten ```bash docker exec appium-adb adb shell settings put global wifi_sleep_policy 2 docker exec appium-adb adb shell svc power stayon usb docker exec appium-adb adb shell dumpsys deviceidle disable ``` --- ## Bestanden ``` ├── Dockerfile # Image: Node LTS + Java 17 + adb + Appium ├── docker-compose.yml # Service-definities (appium + wa-appium-api) ├── entrypoint.sh # USB/TCP-detectie + settings + reconnect-loop ├── wa-appium-api/ │ ├── Dockerfile # Image: Node LTS + Express + WebDriverIO │ ├── package.json # Dependencies │ └── server.js # Express REST API ├── smoke-test.js # Node.js smoke-test (geen dependencies) ├── .env.example # Voorbeeld env-variabelen ├── android-notification-listener/ # Android-app: notificaties → webhook │ ├── app/src/main/java/.../ # Kotlin broncode │ │ ├── SettingsActivity.kt │ │ ├── WhatsAppNotificationListener.kt │ │ └── WebhookClient.kt │ ├── build.gradle.kts │ └── settings.gradle.kts └── README.md # Dit bestand ``` ## Zie ook - [[Whatsapp rest-api]] — primaire WhatsApp API (wwebjs-api, `:3001`) - [[Android Telefoon]] — Nexus 6P met Termux - [[Whatsapp Scripts Migratie]] — overzicht WhatsApp automatisering