Introduction and concepts
1.1 · What Phonestra is
Phonestra brings the apps of an Android phone to a Linux PC, each in its own window, over Wi-Fi and without installing anything on the phone. SPECIFICATION.md says what it does; this manual explains how.
Each app window is a virtual display on the phone, as large as the window; the drawer, the main window, also shows the phone's real screen. Video, audio, touches, keys and clipboard go through a component written from scratch, copied to the phone at every connection and deleted at the end.
After reading this manual you should know where to work in every file, how to build the program and the phone component, build the AppImage, run the tests, and add a message or a channel without breaking the rest.
The manual assumes you know Rust and a little Java. Android and ADB concepts are explained where they are needed; the ch. 19 “Glossary” sums them up. The reason behind every choice is in the notes/ folder: here you will find the right reference when it matters (for example, “prove §49” is section 49 of notes/connection-tests.md).
| Item | Value |
|---|---|
| Version | 1.0.0 |
| Languages | Rust 2024 on the PC, Java on the phone |
| Size | 16,462 lines of Rust, 7,058 of Java |
| Target | Linux x86_64 (AppImage) · Android 14 and later |
1.2 · Basic concepts
A few concepts recur throughout the manual. This table introduces each of them in one line, with the chapter that covers it in depth.
| Concept | What it is | Where | More |
|---|---|---|---|
| Connection | The active phone and its ADB connection, kept up as long as Phonestra is open; it starts over at every drop | collegamento.rs | 3.2 “Life of a connection” |
| ADB client | The ADB protocol written by us: all channels over a single encrypted TCP connection | src/adb/ | ch. 4 “The ADB client” |
| Component | The Java jar copied to the phone; the service is its long-running process, one per connection | android/helper/, componente.rs | ch. 5 “The on-phone component” |
| Video session | A virtual display for an app window, or the mirror of the main screen for the drawer, with its video:<id> channel | video_nostro/ | 6.1 “Sessions: virtual display and mirror” |
| Guardian | A shell process that restores the phone when whatever it is tied to ends | collegamento.rs, Custode.java | 5.6 “The two guardians” |
| Panel “in hand” | The physical screen turns off while the phone is used from the PC and turns back on when the user picks the phone up | Pannello.java, collegamento.rs | 7.3 “The phone in hand” |
| Drawer | The main window: apps, notifications, phones, tools and the drawn phone screen | cassetto.rs | 10.2 “The drawer” |
| Helper | The same jar, used for short commands that print and exit (app list, wallpaper, thumbnails) | app.rs, Aiuto.java | 5.1 “What the component is” |
1.3 · Subsystems at a glance
The code is split into cohesive parts. This is the map that the manual explores chapter by chapter; the line counts of every file are in ch. 17 “Appendix B — File map”.
src/bin/phonestra.rs,src/collegamento.rsmain, the life of the connection, the 3-second round, shutdown, data on the PC (ch. 3 “Startup and life cycle”).src/adb/,src/rete.rs- The ADB client: messages, TLS, channels,
shell,v2,sync:, pairing with the code, mDNS discovery (ch. 4 “The ADB client”). android/helper/src/phonestra/,src/componente.rs- The component: service, channels, command channel, heartbeat, guardians, self-test, and its PC side (ch. 5 “The on-phone component”).
src/video_nostro/,SessioneVideo.java,Codifica.java- Virtual displays, mirror, H.264 encoder, resizing, app events (ch. 6 “Video”).
Pannello.java- The physical screen turned off during use from the PC, calls, drops (ch. 7 “The phone's panel”).
src/audio_nostro.rs,CanaleAudio.java- Loopback capture, AAC, playback and recording (ch. 8 “Audio”).
src/input_nostro.rs,src/appunti.rs,Input.java,Appunti.java- Touches, keys, text and clipboard in both directions (ch. 9 “Input and clipboard”).
src/cassetto.rs,src/finestra.rs,src/prepara.rs,src/procedura.rs,src/avvisi.rs- Drawer, app windows, preferences, first connection, notifications (ch. 10 “The user interface”).
src/azioni.rs,src/ricevi.rs- Installing apps, sending and receiving files (ch. 11 “Apps and files”).
src/bin/prova.rs,tests/- Tests on the PC and on the phone (ch. 12 “Testing and diagnostics”).
packaging/,android/helper/build.sh- The compiled component, the AppImage, releases (ch. 13 “Build and release”).
1.4 · The repository
The nic-fio/PHONESTRA repository is the only complete copy of the project: code, compiled component, documents, mockups and measurements.
PHONESTRA/ ├── Cargo.toml # the phonestra package: one library and two executables ├── Cargo.lock # the exact versions of the Rust crates ├── Makefile # make, make test, make clippy, make docs, make docs-check, make dist, make helper, make clean ├── src/ # the PC program │ ├── adb/ # the ADB client │ ├── video_nostro/ # PC side of the component's video │ └── bin/ # phonestra.rs (the program) and prova.rs (phonestra-prova) ├── android/ │ ├── helper/src/phonestra/ # the phone component, in Java │ ├── helper/stub/ # fake Android classes, for the compiler only │ ├── helper/build.sh # rebuilds the jar (make helper) │ ├── phonestra-helper.jar # the compiled component, embedded in the executable │ └── README.md # what runs on the phone and how it is built ├── data/instructions.toml # per-brand instructions for the cable procedure ├── data/en/ # English translations of the interface, one table per module ├── vendor/rusb/ # rusb without the built-in libusb (dynamic linking, vendor/README.md) ├── packaging/ # container, scripts and AppRun of the AppImage, third-party licenses ├── docs/ # the two manuals and their sources (docs/sources/), index.html, README.md ├── notes/ # decisions, studies, measurements, issue log ├── tests/ # integration tests (tests/manual.rs) ├── tools/ # setup-dev.sh (what a fresh clone needs), backup.sh (the whole project as a git bundle) ├── logos/ # the Phonestra logo: icons/, icons-with-text/ ├── mockup/ # design proposals, icons and the sources of the interface canvas ├── site/ # the website phonestra.nicfio.it: landing page, publish.sh ├── experiments/ # small test scripts outside the program ├── .github/workflows/ci.yml # CI at every push: make all, make test, make docs-check ├── README.md # the project in brief ├── CLAUDE.md # working rules for Claude Code ├── SPECIFICATION.md # what Phonestra does; the code's “§” references point here ├── NOTICE.md # copyright and third-party components with their licenses ├── LICENSE.md # Phonestra Freeware Licence, from the version after 1.0.0-rc.8 └── LICENSE-1.0.0-rc.8-and-earlier.md # free personal use, up to 1.0.0-rc.8
| Path | Contents |
|---|---|
src/ | The PC program: interface, connection, ADB client (src/adb/), PC side of the component (componente.rs, audio_nostro.rs, video_nostro/, input_nostro.rs). |
android/helper/ | The phone component: Java sources in src/phonestra/, fake Android classes for the compiler in stub/, build.sh. |
android/phonestra-helper.jar | The compiled component (dex inside a jar). It is in the repository and is embedded in the executable. |
data/instructions.toml | Instructions for each brand family for the cable procedure, embedded at build time (format in 10.5 “The first connection”). |
docs/ | Technical Manual.html and User Manual.html (generated, never by hand) and their sources in docs/sources/: build.py, style.css, manual.js, one file per chapter in technical/ and user/. |
packaging/ | Containerfile, build.sh, collect.sh, rust-licenses.py, test-distributions.sh and AppRun: the AppImage and the third-party licenses it carries (ch. 13 “Build and release”). |
notes/ | Decisions, studies, measurements, issue log: the reasons behind the project. |
tests/ | Integration tests; tests/manual.rs checks the manuals. |
tools/ | setup-dev.sh says what a fresh clone lacks (packages, Rust, git identity) and, with --install, installs the packages; backup.sh saves the repository as a git bundle. |
logos/ | The Phonestra logo in several versions, with the icons (icons/) and the icons with the name (icons-with-text/). |
mockup/ | Interface proposals, icons and the sources of the design canvas. |
Makefile | The usual commands: make, make test, make clippy, make docs, make docs-check, make dist (AppImage), make helper (jar), make clean. |
.github/workflows/ci.yml | The CI on GitHub: at every push and pull request it builds, runs make test and make docs-check, without a phone. |
SPECIFICATION.md | What Phonestra does, section by section; the code's “§” numbers point here. |
NOTICE.md | Copyright, the third-party components (Rust crates, the AppImage's libraries) and their licenses, where their texts are in the AppImage and where to get their sources. |
LICENSE.md | The Phonestra Freeware Licence, from the first version after 1.0.0-rc.8: free use, also at work, and free redistribution of the unchanged AppImage; no sale, no inclusion in commercial products, no modification. The licence of the versions up to 1.0.0-rc.8 (free personal use) stays in LICENSE-1.0.0-rc.8-and-earlier.md. |
site/ | The website, https://phonestra.nicfio.it: the landing page (landing/), publish.sh that builds it with the manuals, the licence page and the download, and copies it to the server. |
1.5 · The project in numbers
How big Phonestra is, part by part. The lines are counted from the sources every time the manual is generated.
| Part | Where | Lines | Contents |
|---|---|---|---|
| Tests and measurements (PC) | src/bin/prova*, prova_input.rs, misura_audio.rs, video_nostro/prova.rs, adb/misura.rs, adb/prove.rs, tests/ | 2,968 | the phonestra-prova tool, the study's measurements, the integration tests |
| Tests and measurements (phone) | VideoProva.java, InputProva.java, Codificatori.java | 1,478 | measurement tools and test commands of the component |
| Interface | cassetto.rs, finestra.rs, ricevi.rs, prepara.rs, procedura.rs, avvisi.rs, foto.rs, bin/phonestra.rs | 6,513 | drawer, app windows, receiving files, first connection, alerts |
| ADB client | src/adb/ | 1,491 | messages, TLS, channels, shell,v2, sync:, pairing |
| Component, PC side | componente.rs, audio_nostro.rs, video_nostro/, input_nostro.rs, appunti.rs | 3,123 | service, dispatching, audio, video, input, clipboard |
| Connection and data | collegamento.rs, rete.rs, configurazione.rs, telefono.rs, usb.rs, notifiche.rs, azioni.rs, app.rs, lib.rs | 2,367 | connection, panel, mDNS, configuration, cable, notifications, actions, app list |
| On-phone component | android/helper/src/phonestra/ | 5,580 | service, guardian, audio, video, input, clipboard, panel, thumbnails |
| Build and tools | packaging/, android/helper/build.sh, docs/sources/ | 4,820 | container, AppImage, jar, manual generator |
| Total | 28,340 |
The numbers in this table and in ch. 17 “Appendix B — File map” are not written by hand, and cargo test fails if the published manual no longer matches the sources (12.5 “The manuals and their checks”).
Overall architecture
2.1 · Architectural principles
Phonestra grew out of a few rules, written before the code and followed in every piece. They explain choices that would otherwise look odd, such as an ADB client written from scratch or a guardian for every changed setting.
| Principle | What it means |
|---|---|
| Nothing on the phone | No app installed. The component is copied to /data/local/tmp at every connection and deleted at the end. |
| The phone goes back to how it was | Every changed setting has a guardian that restores it, even if the PC disappears or the service dies suddenly (5.6 “The two guardians”). |
| Our own ADB | No adb to install: TLS, pairing with the code, mDNS and multiple channels are written here (ch. 4 “The ADB client”). |
| Our own code | The phone component is written from scratch: no scrcpy, no third-party code to depend on. |
| Measure before writing | Study, then measurements on the real phone, then code one piece at a time. The measurements stay in notes/. |
| One file, no traces | One AppImage that contains everything; on the PC only ~/.config/Phonestra and ~/.cache/Phonestra (3.6 “Configuration and data on the PC”). |
2.2 · The two sides and the layers
Phonestra has two sides: the program on the PC, in Rust, and the component on the phone, in Java. They talk to each other only through ADB, over a single encrypted Wi-Fi connection.
On the PC each layer calls only the ones below it: the interface does not talk to ADB, and the ADB client knows nothing about video or audio. On the phone there is a single process per connection, the service, which serves all windows, the drawer's screen, audio and clipboard. The guardian is a separate shell process that stays alive even when the service dies; the dashed line is the pipe through which the service passes it the restore actions.
| Layer | Responsibility | Main modules |
|---|---|---|
| Interface | The GTK windows: drawer, app windows, first connection, system alerts | cassetto.rs, finestra.rs, ricevi.rs, prepara.rs, procedura.rs, avvisi.rs |
| Connection | Finding the phone, keeping it connected, the 3-second round, the connection guardian, shutdown | collegamento.rs, rete.rs, notifiche.rs, configurazione.rs |
| PC-side pieces | Audio, video, input and clipboard over the service's channels | audio_nostro.rs, video_nostro/, input_nostro.rs, appunti.rs |
| Component | Service startup, command channel, message dispatching | componente.rs |
| ADB client | Messages, TLS, channels, flow control | src/adb/ |
| Phone | Service, guardian, classes of the pieces | android/helper/src/phonestra/ |
Collegamento, in watch channels: stato(), guasto(), info(), notifiche(), adb(), componente(). Drawer and windows subscribe and restart on their own when a value changes (3.2 “Life of a connection”).2.3 · Threads and tasks
Three worlds run together in the Phonestra process: the GTK thread, the tokio runtime and GStreamer. Each has its own job, and they talk to each other only through channels.
| Where | What runs | How they talk |
|---|---|---|
| GTK main thread | The whole interface; glib::spawn_future_local for tasks that touch widgets | Reads the watch channels of the Collegamento, sends commands over mpsc channels |
tokio runtime (phonestra::esecutore()) | Connection, ADB client, component, audio, video, input | watch for states, mpsc for messages, Notify for wake-ups |
| GStreamer | Video and audio decoding, playback, recording | appsrc elements fed by tokio tasks |
Widget never leaves the GTK thread; an Adb, a Condiviso or a Mittente is cloned and goes wherever it is needed.Startup and life cycle
3.1 · Program startup
The program starts from main, in src/bin/phonestra.rs: it initializes GStreamer, creates the GTK application and hands the configured phone over to a Collegamento, which lives in a tokio task of its own.
A second launch does not open a second connection: adw::Application is unique per session and connect_activate brings the drawer back to the foreground (reopening it if it was closed). With a package name as an argument (phonestra com.android.chrome) that app opens right away too, but only on the first launch: the instance already running does not receive the argument.
Ctrl+C and SIGTERM close the windows just as the user would, so the phone is restored. When the drawer asks to switch phones (3.5 “Multiple phones”), main relaunches the program at the end: $APPIMAGE if set, otherwise the current executable.
3.2 · Life of a connection
Collegamento::mantieni is the heart of the program. It runs in a tokio task as long as Phonestra stays open and starts over at every drop.
- Finds the phone: first the last address that worked, then the mDNS search for the
_adb-tls-connect._tcpservice with the saved serial number (rete::indirizzo_attivo). The port changes every time Wireless debugging is turned on, so it must always be read again. - Opens the connection:
Adb::wifi(TCP, STLS, TLS with Phonestra's certificate), with a 30 s timeout. - Reads and saves the user's values (screen timeout and media volume), also in
telefoni.toml. They are read only once for all reconnections; at every reconnection Phonestra checks whether the user has changed the screen timeout in the meantime. - Reads the screen's density and shape (
wm density; wm size, first the “Override” values chosen by the user, then the “Physical” ones): the density is needed by the virtual displays, the aspect ratio by the column of portrait-only apps. - Starts the connection guardian: a shell
exec:that raises screen timeout and volume to the maximum and restores them when the channel closes (5.6 “The two guardians”). - Publishes the
Adb(watch) and theCollegatostate: drawer and windows restart on their own. - Starts the component in a task (
gira_componente): service on the phone, thenCondivisopublished for windows and drawer, then audio (after the mirror and 5 s, 8.4 “The startup order”). - Listens to the clipboard of the phone (
appunti::ascolta). - 3-second round as long as the connection holds (next section).
- Shutdown when the user closes Phonestra (3.4 “Shutdown”).
The state is published as Stato::{Cerco, Collegato, Bloccato, Perso, Chiuso}. Between one attempt and the next the wait grows from 2 to 10 s; “Reconnect now” (Riconnetti ora, riconnetti_ora) interrupts it.
3.3 · The 3-second round
A single exec: every 3 s fetches lock state, calls and notifications, and also serves as a check that the phone is responding: if it does not respond within 5 s, the connection is considered dropped. Battery and network and, with the phone “in hand”, lastUserActivityTime are separate commands, with a 5 s timeout each: if they fail, the connection does not drop.
| Command | When | Used for |
|---|---|---|
dumpsys window | grep -m1 -o 'isKeyguardShowing=[a-z]*' | every round | knowing whether the phone is locked (Bloccato state) and whether the user unlocked it by hand |
dumpsys telephony.registry | grep -o 'mCallState=[12]' | every round | incoming (1) and ongoing (2) calls; with two SIMs there is one line per SIM (7.4 “Calls”) |
notifiche::COMANDO_NOTIFICHE | every round | the drawer's notifications (10.6 “Notifications and alerts”) |
notifiche::COMANDO_INFO | on the first round and then every 10 (30 s) | battery and network for the drawn phone |
dumpsys power | grep -m1 lastUserActivityTime= | only with the phone “in hand” | turning the panel off after the screen timeout with no touches (7.3 “The phone in hand”) |
Protected screens do not go through here: the component reports them, session by session (6.6 “App events”).
3.4 · Shutdown
When the user closes Phonestra, usa leaves the round and restores the phone to how it was, one step at a time, with a timeout for each.
- Music or video that is playing is paused (
cmd media_session dispatch pause, at most 3 s): otherwise, once the capture is removed, it would resume from the phone's speaker. - The
Adbis withdrawn; the windows close their sessions and remove their apps from recents (waiting up to 5 s). - The component receives
FINEand shuts down (at most 12 s); its guardian turns the panel back on. - The connection guardian is closed, and it restores volume and screen timeout.
- For 3 s Phonestra checks that the screen timeout is no longer Phonestra's; if the guardian has not restored it, Phonestra restores it directly. If even that fails, it stays recorded in
telefoni.tomland is restored at the next connection.
3.5 · Multiple phones
Phonestra uses one phone at a time (the option of several phones active together was discarded). The other configured phones appear in the sidebar as “not active” (non attivo); to switch to one of them the program closes and restarts.
- Clicking an inactive phone asks for confirmation if apps are open.
Telefoni::metti_primomoves it to the top oftelefoni.toml: it is the one opened at startup.- The drawer sets
cassetto::RIAVVIAand closes all windows, like a normal shutdown: the previous phone is restored. mainrelaunches the program ($APPIMAGEor the current executable), which connects to the new phone.
Forget this phone… (Dimentica questo telefono…) does the same restart if other phones remain; if none are left, Phonestra closes, and at the next start “Add a phone” (Aggiungi un telefono) opens.
3.6 · Configuration and data on the PC
Phonestra keeps its data in two folders on the PC: the configuration in ~/.config/Phonestra, things that can be recreated in ~/.cache/Phonestra. telefoni.toml and preferenze.toml are read and written in configurazione.rs (Telefoni, Preferenze).
| File | Contents |
|---|---|
~/.config/Phonestra/adbkey | Phonestra's private RSA key (permissions 600), different from that of adb: the phone authorizes Phonestra as a separate computer |
~/.config/Phonestra/telefoni.toml | Per phone: seriale, nome (the device name, not shown), modello, nome_scelto (from Rename…, Rinomina…), modello_commerciale, tablet (Telefono::nome_mostrato builds the displayed name from them), android, ultimo_indirizzo, spegnimento_originale, volume_originale, preferiti; the first one is opened at startup |
~/.config/Phonestra/preferenze.toml | esc_indietro, avvisi, solo_nome_app, app_silenziate, cartella_file, cartella_ricevuti (if missing: the Downloads (Scaricati) folder), lingua (if missing: the system's language); 10.4 “Preferences” |
~/.config/Phonestra/icone/ | App icons for system alerts |
~/.cache/Phonestra/ | GStreamer plugin registry, image loaders and fallback libraries of the AppImage, the logo for the “About” (Informazioni) window (icone/phonestra.png): it can be deleted |
Deleting ~/.config/Phonestra brings Phonestra back to its initial state. The configuration folder follows $XDG_CONFIG_HOME; screenshots, recordings and received files go to the XDG user folders (Pictures, Videos, Downloads).
The ADB client
4.1 · Why our own client
Phonestra speaks the ADB protocol on its own, in src/adb/: no adb server, no programs to install. All channels (commands, audio, the video of every window, shell) travel over a single encrypted TCP connection.
The adb_client library reads all channels from the same connection without dispatching the messages: it handles one command at a time, not video, audio and commands together. The client in src/adb/ has a reader task that dispatches each message to the right channel by local ID, and every channel honors ADB's flow control.
adb_client remains for short commands outside the actual connection: the USB cable (telefono.rs) in the fallback procedure and in phonestra-prova prepara, and the first Wi-Fi connection of “Add a phone” (Aggiungi un telefono), which after pairing reads model and version and removes the expiry from the authorization (telefono::Collegamento::wifi, 10.5 “The first connection”).
4.2 · ADB messages
ADB is made of a few messages, all with the same shape: a 24-byte little-endian header (command, arg0, arg1, data length, byte sum, command xor 0xffffffff) followed by the data (messaggio.rs).
| Command | Meaning | Use in Phonestra |
|---|---|---|
CNXN | Handshake: version, max_payload, features | Ours announces host::features=shell_v2,cmd,stat_v2 (and delayed_ack if enabled) |
STLS | Switch to TLS | Always, on Wireless debugging |
OPEN | Opens a channel to a service (shell,v2,raw:…, sync:, localabstract:…) | Adb::apri, with a 10 s timeout for the reply |
OKAY | Channel accepted, or data acknowledged | Flow control |
WRTE | Data on a channel | Canale::scrivi / leggi |
CLSE | Closing a channel | Canale::chiudi, Chiusore |
4.3 · Wi-Fi connection and TLS
On Wireless debugging the connection starts in clear and switches to TLS right away. Adb::wifi opens it in four steps; the phone recognizes the PC by its authorized public key.
- TCP to the address found via mDNS (5 s timeout,
TCP_NODELAY). CNXNin clear: the phone reads our features and themax_payloadfrom here, not from theCNXNafter TLS.- The phone replies
STLS; we replySTLSand TLS starts (tls.rs). The client certificate is self-signed with Phonestra's RSA key (~/.config/Phonestra/adbkey): the phone recognizes the PC by the authorized public key. The phone's certificate is not checked against an authority (it is self-signed): pairing guarantees the identity. - After TLS comes the phone's
CNXN: itsarg1is the minimum of the twomax_payloadvalues, and its text contains the phone's features.
4.4 · Channels and flow control
All channels of a connection share a single underlying connection. Each channel has its own local ID; the reader dispatches, writers wait their turn.
An Adb is cloned and passed around everywhere. One task reads from the socket and dispatches (leggi_sempre); writes go directly to the socket, one at a time under a Mutex (Adb::invia). A second task, the “posta” (mailbox), sends only the acknowledgments that cannot wait for the writers (the OKAY messages acknowledging reads).
Adb::apri(servizio) returns a Canale with scrivi, leggi (cancelable), leggi_esatti, leggi_tutto and chiudi; Canale::chiusore() returns an object that closes the channel from another task. Adb::esegui is the shortcut for a short command (the exec: service) that returns the output.
Without delayed ack each channel has only one WRTE in flight: the next one leaves after the OKAY. The transport parameters are in Trasporto:
| Value | Default | Test variable | Why |
|---|---|---|---|
delayed_ack | off | PHONESTRA_ADB_DELAYED_ACK=1 | On 28 Sep 2026 adbd rejected every OPEN when it was announced (ch. 18 “Appendix C — Known issues”). |
max_payload | 64 KiB | PHONESTRA_ADB_PAYLOAD=1m | A single connection for all channels: a 1 MiB WRTE holds the wire for ~200 ms and audio waits behind video; a 64 KiB one for ~13 ms. With 64 KiB audio stays in sync (misure §50). |
| window | 256 KiB | PHONESTRA_ADB_FINESTRA=512k | Matters only with delayed ack: bytes in flight per channel. |
The details of delayed ack (balance, 4-byte OKAY, adbd's behavior) are in notes/adb.md and in the pure parts of flusso.rs, tested by adb/prove.rs against a fake in-memory adbd.
4.5 · The shell,v2 service
shell.rs starts a process without a terminal, with input, output, errors and exit code kept separate. Packets id u8 · lunghezza u32 LE · dati travel on the channel.
| id | Direction | Content |
|---|---|---|
| 0 | PC → phone | Process input |
| 1 | phone → PC | Output |
| 2 | phone → PC | Error output |
| 3 | phone → PC | Exit code (1 byte) |
| 4 | PC → phone | Closing of the input |
shell,v2This is how the Phonestra service starts: the output carries the ready line without mixing with the logs, the input carries the secret, the exit code tells why the service ended, and if the channel drops adbd sends SIGHUP to the process. For short commands Adb::esegui is enough (the exec: service).
4.6 · Copying files: sync:
sync.rs implements the sync: protocol in both directions.
| Function | Requests | Use |
|---|---|---|
invia, invia_a_blocchi | SEND with path and permissions, DATA blocks, DONE | copying the component to /data/local/tmp, the files the user sends to the phone and the .apk files to install (with progress and cancellation) |
elenca | STA2, then LIS2 | listing a phone folder for “Receive files…” (Ricevi file…): 72-byte DNT2 entries after the ID, 64-bit sizes |
e_cartella | STA2 | finding out whether a path exists and is a folder (the places of “Receive files…”, Ricevi file…) |
ricevi | RECV | copying a file to the PC by writing the DATA blocks straight into the file, without holding it in memory |
sync.rsSTA2 is asked first.4.7 · Pairing with the code
abbina.rs reimplements Android 11+'s adb pair from the Android and BoringSSL sources: the phone's “Pair device with pairing code” (Associa dispositivo con codice di associazione) screen shows 6 digits, and the user types them into Phonestra.
- TLS directly on the pairing port, with Phonestra's certificate.
- Password = the 6 digits + 64 bytes exported from TLS (label
adb-label\0): nobody can get in the middle. - SPAKE2 over Ed25519 (
curve25519-dalek), with Phonestra in the “alice” role. - From the shared key, with HKDF-SHA256, an AES-128-GCM key (
ring). - Encrypted exchange of the
PeerInfo: we send Phonestra's ADB public key, the phone sends its ID.
From then on the phone accepts Phonestra's key in Wi-Fi connections, as after an “Always allow” (Consenti sempre) over the cable.
4.8 · Network discovery: mDNS
rete.rs builds and parses DNS packets by hand: a PTR query for _adb-tls-connect._tcp.local (connection) or _adb-tls-pairing._tcp.local (the code screen) with the QU bit, i.e. reply sent directly to our socket. It is the method that found the phone when the discovery of adb saw nothing.
| Rule | Why |
|---|---|
The instance is named adb-<seriale>-<suffisso> | The name identifies the saved phone. |
| The port comes from the SRV record | It changes every time Wireless debugging restarts. |
| The query is repeated every second | Some phones only answer the second one. |
| An SRV with TTL 0 removes the found phone | It is the “goodbye” of a service that is shutting down. |
indirizzo_attivo: last good address (800 ms), then up to three 3 s searches | The common case is fast; an address is accepted only if the port really answers. |
The on-phone component
5.1 · What the component is
Everything Phonestra does on the phone goes through a Java service written from scratch, started like the ADB shell and deleted at the end. This chapter describes its infrastructure; the following chapters describe the pieces: video, panel, audio, input.
android/phonestra-helper.jar contains a classes.dex. The PC embeds it (app::AIUTO, include_bytes!), copies it to /data/local/tmp and starts it with app_process: it runs with the shell's uid (2000) and its permissions (capturing the screen and audio, injecting events, reading the clipboard). It is not an app, it is not installed, it does not appear in the settings.
The same jar has two modes of use, chosen by the first argument of phonestra.Aiuto: the service, the long-running process, one per connection, which handles audio, video, input, clipboard and panel; and the short helper commands, which print and exit. For each short command the PC copies the jar under a name of its own (phonestra-aiuto.jar.<8 cifre esadecimali>, because several requests may arrive together), runs it and deletes it (app.rs).
| Command | What it prints | Who uses it |
|---|---|---|
app [lato] | the list of launcher apps with their PNG icons in base64, closed by a fine\t<n> line; with no arguments it means app 96 | drawer (app::elenco) |
sfondo [larghezza] | the phone's wallpaper as PNG (default 540) | phone drawn in the drawer |
miniature <lato> <percorsi in base64> | one line indice\tJPEG in base64 (quality 80) or indice\t- for each file | “Receive files…” (Ricevi file…; 11.2 “Receiving files from the phone”) |
pannello [0|1] | schermi=<quanti> after turning the panel on (1, default) or off | the guardian (5.6 “The two guardians”) |
codificatori | the phone's audio and video encoders | phonestra-prova codificatori |
audio … | captured audio, for study (sorgente=submix|loopback|render, formato, priorita, voce) | phonestra-prova audio-nostro |
video-prova … | the video measurement tool of the study | phonestra-prova video-prova |
servizio | the ready line, then stays alive | Componente::avvia |
Java only where Android requires it: the APIs needed (MediaCodec, VirtualDisplay, AudioPolicy, InputManager) exist only in Java. Audio and video encoding is done by the phone's encoders anyway.
5.2 · Starting the service
The PC starts the service on a shell,v2 channel: it copies the jar, launches it with app_process, passes it the secret on the input and waits for the ready line. Then it opens the command channel and receives the CIAO.
The ready line is phonestra-servizio pronto protocollo=1 socket=phonestra_<32 hex> pid=<pid>; if startup fails, phonestra-servizio errore <causa> and code 1. The PC waits 20 s for the ready line and 10 s for the CIAO; if the service does not start, the PC deletes the jar itself. exec makes the service take the place of sh, so adbd's SIGHUP reaches it directly; --nice-name makes it appear in ps as phonestra-servizio.
5.3 · Channels and preamble
Each channel is a localabstract:phonestra_<32 hex> opened by the PC. The first bytes are the preamble: segreto (16 byte) · lunghezza del tipo u8 · tipo ASCII. The service reads it with a 3 s timeout and chooses the handler from the part of the type before the colon (Servizio.TIPI).
| Type | Handler | Content |
|---|---|---|
comandi | Servizio.comandi | Messages in both directions. Only one per service: a second comandi channel is rejected |
audio, audio:aac, audio:pcm | CanaleAudio.gestisci | Audio packets to the PC (8.1 “The recipe”); an unknown format receives the text errore formato sconosciuto |
video:<id> | Video.canale | Video packets of a session (6.3 “The video:<id> channel”) |
Wrong secret, uid other than 2000 or unknown type: the socket is closed without a reply.
5.4 · The command channel
The command channel carries all the short messages between PC and service, in both directions: requests with a reply, spontaneous events, the heartbeat.
Messages with an 8-byte big-endian header, tipo u8 · bandiere u8 · id u16 · lunghezza u32, and the content (16 MB at most: beyond that, the stream is corrupted and the channel is closed). id ties the reply to the request (0 = spontaneous message); the 0x01 flag marks it as a reply. The format is the same in Protocollo.java and componente.rs, checked by a test with the same bytes.
| Range | Who | Where |
|---|---|---|
0x01–0x0f | Infrastructure | componente::tipo, Protocollo.java |
0x10–0x1f | Testing and diagnostics | PROVA_CUSTODE |
0x40–0x4f | Video and panel | 6.2 “Video messages” |
0x50–0x5f | Input and clipboard | 9.1 “Input and clipboard messages” |
| Type | Name | Direction | Content |
|---|---|---|---|
0x01 | CIAO | service → PC, first message | Lines chiave=valore: protocol, android, sdk, manufacturer, model, pid, avvio_ms, autotest_ms, autotest.<voce>=… (ok or the reason; for the guardian ok setsid, morto setsid, ok senza-setsid) |
0x02 | BATTITO | both directions, every second | empty |
0x03 | FINE | PC → service; same reply | empty; after the reply the service exits with 0 |
0x04 | ERRORE | service → PC, as a reply | text, for example “tipo sconosciuto 0x2a” |
0x10 | PROVA_CUSTODE | PC → service | creates a file that the guardian removes at the end (tests only) |
A type the service does not know receives ERRORE: the PC understands that the jar is old. PROTOCOLLO (1 today) changes only if an existing message changes meaning.
5.5 · Heartbeat and exit codes
Any message counts as a sign of life. After 5 s of silence each side considers the other gone: the PC closes the command channel (ricevi returns None; it checks every 200 ms), the service exits. This is needed because adbd on Wireless debugging does not notice by itself that a PC is gone (Wi-Fi lost, PC off).
| Code | When |
|---|---|
| 0 | FINE requested by the PC |
| 1 | Startup error or unhandled exception |
| 3 | No message from the PC for 5 s |
| 4 | Command channel closed by the PC, or write impossible |
| 5 | Secret not received or no command channel within 10 s |
| 128+n | Killed by signal n (129 = SIGHUP: startup channel closed; 137 = kill -9) |
componente::descrivi_uscita)The service exits with System.exit and, after 2 s, Runtime.halt if something hangs. Restoring the phone is the guardian's job, which is there both at an orderly end and at a sudden one. The service restores by itself only what it can restore right away: the audio policy (the audio-fine shutdown hook), the panel when it turns it back on, and min_refresh_rate as soon as the panel is off; in those cases it also removes the corresponding action from the guardian.
5.6 · The two guardians
Phonestra changes a few things on the phone and must restore them even when something goes wrong. This is done by two shell processes, each tied to the life of something else:
| Connection guardian | Service guardian | |
|---|---|---|
| Who starts it | The PC, collegamento.rs, with exec: | The service, Custode.java, with setsid sh -c |
| Tied to | The ADB connection: it ends when adbd closes its input (cat >/dev/null) | The service process: it reads a pipe that only the service keeps open |
| What it restores | Screen timeout, media volume | Physical panel, minimum display refresh rate, test tasks, jar copies |
| Survives with | trap '' HUP TERM PIPE | setsid and trap '' HUP INT TERM PIPE |
The screen timeout is set to the maximum (SPEGNIMENTO_LUNGO, i.e. never) while Phonestra is open: with the phone asleep, apps on virtual displays receive no input, and taps from the PC do not count as phone activity (with 30 minutes it fell asleep during use, prove §55). The volume is set to the maximum because with the volume at 0 the Facebook app does not start the audio of reels; audio comes out only from the PC anyway.
Both values are also saved in telefoni.toml: if Phonestra crashes without restoring them, the next connection restores the saved ones. The screen timeout is read from the phone at startup and restored at the end only if it is still Phonestra's value: if the user changes it during the connection, the user's value stays (prove §56). A value under 5 s is considered invalid; 30 minutes (SPEGNIMENTO_LUNGO_VECCHIO, the value of releases up to rc.3) is treated as a leftover of a crashed Phonestra.
The service guardian receives the whole list of actions at every change, between a #inizio line and a #fine line; a half list does not replace the previous one. When the service dies it runs them in ascending order (for equal order, the last added first), one per line with sh -c (one that fails does not stop the others), then deletes the service's jar and forgotten copies older than one minute, and terminates.
| Order | Action | Who sets it |
|---|---|---|
| 400 | Turn the panel back on: app_process … phonestra.Aiuto pannello 1 with a copy of the jar kept for the guardian (phonestra-custode-<pid>.jar); if the copy is missing, the old fallback KEYCODE_SLEEP/WAKEUP | Pannello.java |
| 410 | Restore min_refresh_rate to what it was (only while waiting, 1 s at most, before the panel is turned off) | Pannello.java |
| 500 | Remove the tasks started by the input tests | InputProva.java |
| 900 | Remove the PROVA_CUSTODE file | Servizio.java |
KEYCODE_SLEEP/WAKEUP). On Samsung phones this locked the phone and made Wireless debugging drop at every Phonestra restart. Now the guardian calls setDisplayPowerMode like the service (misure §51).Virtual displays need no action: Android closes them when the process dies. Apps are not removed from recents on a drop, on purpose: when the connection comes back they return to their window with their state. Android removes the audio policy (8.5 “Who removes the capture”).
5.7 · Context, hidden APIs and self-test
Many of the APIs needed are hidden (IDisplayManager, IWindowManager, InputManagerGlobal, IClipboard, AudioPolicy). In app_process the hidden API policy is off: they are called via reflection.
| Part | How |
|---|---|
| Hidden | Services from ServiceManager + Stub.asInterface, methods looked up by name and number of parameters among the known variants. Rule: look at what is there, not at the version. |
| Context | app_process has no Android context. One step at a time, it sets up the main Looper, a system ActivityThread, its ConfigurationController (without it, on Samsung phones DisplayManagerGlobal fails) and the context of the com.android.shell package: since Android 16 the shell's permissions apply only with the right package. |
| Self-test | At startup it checks that every hidden API is there and with which signature, without using it. The result goes to the PC in the CIAO (Ciao::mancanti()). |
| System | Sistema.java gathers the common pieces: the display manager for each virtual display, app launching (startActivityAsUser with 11 parameters, with am start as fallback), togliTask when a window is closed, the internal shell commands. |
| Item | What it checks |
|---|---|
contesto | Shell context ready, com.android.shell package |
permessi | The shell permissions the pieces need |
display_manager | createVirtualDisplay and DisplayManagerGlobal |
capture_display | IWindowManager.captureDisplay and the class of its arguments |
task_stack_listener | TaskStackListener and its registration |
inject_input_event | injectInputEvent (2 or 3 parameters), InputEvent.setDisplayId |
audio_policy | AudioPolicy, AudioMix, createAudioRecordSink, registration |
appunti | Methods of IClipboard; presence of semclipboard |
custode | Guardian alive, with or without setsid |
5.8 · Service security
The service runs with the shell's permissions: nobody else must be able to control it, and at the end nothing must be left on the phone.
- Abstract socket with a random 128-bit name; every channel must come from uid 2000 (
getPeerCredentials) and start with the secret (constant-time comparison). No TCP port open on the phone. - The secret is passed on the process input, not on the command line:
/proc/<pid>/cmdlineis readable by other processes with the same uid. - No generic command: the PC cannot make the service run arbitrary shell commands. The guardian's actions are decided by the service; test commands accept only packages and actions made of letters, digits, dots and underscores.
- Nothing is left: jar deleted at startup and by the guardian, service that exits after 5 s without a PC, guardian that terminates after restoring.
5.9 · PC side: Componente and Condiviso
Componente (in componente.rs) is a started service: avvia, apri_canale, manda, richiesta, ricevi, chiudi. In Phonestra it is always used through Condiviso, which keeps it in a task (smista) and can be cloned: one service per connection, many windows. When the last copy of Condiviso goes away, the service is closed.
Condiviso method | What it is for |
|---|---|
domanda(tipo, dati) | A request with a reply (5 s timeout; expired requests are checked every 500 ms) |
manda(tipo, dati) | A message without a reply |
apri_sessione | VIDEO_APRI: reply and events of the session; events that arrive before the reply are kept for 2 s |
dimentica(sessione) | Stop dispatching the events of a closed session |
iscrivi(tipo) | Receive the spontaneous messages of a type |
mittente() | A cloneable Mittente: taps and keys go straight into the command channel's queue, without going through the task |
apritore() | An Apritore to open the audio and video:<id> channels |
nome_dispositivo() | The model from the CIAO (“telefono”, phone, if missing) |
finito(), vivo() | Find out whether the service is still alive |
chiudi() | FINE, waiting for the exit, closing the channels |
CondivisoCollegamento::gira_componente restarts the service if it dies while the phone is still connected, after 2 s. After CADUTE_MASSIME (3) failed starts or drops it stops trying and publishes the reason (Collegamento::guasto): drawer and windows show “Phonestra won't start on the phone” (Phonestra non parte sul telefono) with “Reconnect now” (Riconnetti ora).
Video
6.1 · Sessions: virtual display and mirror
Every app window is a virtual display on the phone, as large as the window, with the app launched on it. The phone screen drawn in the drawer is instead the mirror of the main screen. The phone encodes in H.264 in hardware, the PC decodes with GStreamer.
| Virtual display (an app's window) | Mirror (drawer) | |
|---|---|---|
| How it is created | createVirtualDisplay(nome, l, a, dpi, null, flag) with the flags of Sistema.FLAG_PROPOSTI: PUBLIC, PRESENTATION, OWN_CONTENT_ONLY, SUPPORTS_TOUCH, ROTATES_WITH_CONTENT, DESTROY_CONTENT_ON_REMOVAL, TRUSTED, OWN_DISPLAY_GROUP, OWN_FOCUS, TOUCH_FEEDBACK_DISABLED | Hidden static DisplayManager.createVirtualDisplay (permission CAPTURE_VIDEO_OUTPUT) |
| Size | The one requested by the PC (720×1280 at 320 dpi if missing), aligned to 8 and to the encoder's alignment before creating it | That of the main screen, reduced to 1920 per side |
| Orientation | Locked: cmd window set-ignore-orientation-request and user-rotation lock 0 on the display | Follows the phone: the size is re-read every 500 ms; if it rotates, new encoder and new mirror |
| Resizable | Yes (VirtualDisplay.resize) | No: the window scales the image; VIDEO_RIDIMENSIONA replies «specchio: misura dello schermo del telefono» |
| Display number | In the open reply | 0 (the main screen); no orientation events, but protected-screen events yes |
On the PC the size in pixels comes from finestra::pixel: the window size in points (1 PC point = 1 phone dp) times the phone's density, within 2560 per side and in multiples of 8. The display has the same density as the phone, because some apps (Facebook) draw certain elements with the density of the real screen, and with different densities they would come out huge. For portrait-only apps in a wide window the display is a column shaped like the phone (finestra::misura).
6.2 · Video messages
Video messages sit in the 0x40–0x4f range of the command channel, with a content made of chiave=valore lines. Requests are executed in order on a video thread of the service: the command channel (heartbeat, input) never waits for video.
| Type | Name | Request (PC → service) | Reply |
|---|---|---|---|
0x40 | VIDEO_APRI | larghezza altezza dpi codec or specchio=1 lato_massimo codec; optional app=, informazioni=; test switches, valid for the whole service: max_fps=, priorita=, protetta= | id display codec larghezza altezza (size already aligned), plus avvio=<esito> if there was app= or informazioni= |
0x41 | VIDEO_CHIUDI | id [togli_task=1] | empty, once closed |
0x42 | VIDEO_AVVIA_APP | id app=<pacchetto> or id informazioni=<pacchetto> | outcome of the launch |
0x43 | VIDEO_RIDIMENSIONA | id larghezza altezza | size LxA or unchanged size |
0x44 | VIDEO_CHIAVE | id | empty |
0x45 | VIDEO_PANNELLO | acceso=0|1 | schermi=<quanti> |
0x46 | VIDEO_EVENTO | — | spontaneous: evento=<nome> id=<sessione> … |
The PC waits for the reply only for APRI and CHIUDI; the other commands do not wait for it and an error ends up in the log. informazioni=<pacchetto> opens the “App info” (Informazioni sull'app) (App info) page of Settings instead of the app. The default codec is h264; the component also accepts h265, used only by the tests.
6.3 · The video:<id> channel
Each session has its own channel, video:<id>, which carries the encoder's packets from the phone to the PC.
The PC opens it right after VIDEO_APRI (within 10 s). The encoder starts when the channel is open, so the first packet is the size, then the parameters, then the first keyframe, and nothing is lost. The PC does not write to it; if it closes it, the session closes (without removing the app from recents). Each packet has a 12-byte big-endian header:
| Packet | Header | Then |
|---|---|---|
| New size | 0x80000000 · larghezza u32 · altezza u32 | nothing |
| Data | pts u64 (µs since the first frame; bit 62 = codec parameters, bit 61 = keyframe) · lunghezza u32 | the data in Annex B, as it comes out of MediaCodec |
On the PC side video_nostro::flusso::leggi_pacchetto reads them in a dedicated task: a read interrupted halfway inside a select! would lose bytes. The phone's reader thread writes each whole packet with a single write.
6.4 · Encoder and keyframe
Codifica.java takes the first hardware encoder (not an alias) for the type, with the measured values (misure §43): 8 Mbit/s, 60 frames per second declared, keyframe every 10 s, repeat after 100 ms, real-time priority, limited range, plus prepend-sps-pps-to-idr-frames so that every keyframe carries the parameters in front; max-fps-to-encoder only with the test switch max_fps.
If configure rejects the format, it retries in this order: hardware without prepend-sps-pps-to-idr-frames, then Android's default encoder with and without it.
The keyframe is needed when a window restarts or a recording begins (ricomincia_video → VIDEO_CHIAVE). It is requested with REQUEST_SYNC_FRAME, without recreating anything: about 0.1 s instead of scrcpy's 1–2 s restarts.
c2.qti.avc.encoder) ignores repeat-previous-frame-after: on a still screen nothing comes out, and the keyframe request would wait for the next change (a freshly opened window would stay black). If the keyframe does not come out within 80 ms, SessioneVideo detaches and reattaches the encoder's Surface (VirtualDisplay.setSurface(null) and then its own again), which makes a frame get composed at once; if still nothing, a second time after another 160 ms.6.5 · Resizing
When the window changes size, the virtual display follows it: it is resized if that is enough, recreated if the scale changes too much.
- The window changes size. A function tied to GTK's redraw sends the new size to the session on every change, without waiting.
- The PC decides. During a recording the size does not change (8.6 “Recording”); for the mirror nothing is done. If, with the 2560-pixel cap, the right scale drifts more than 15% from that of the display, resizing the display is not enough: the session ends with
FineSessione::Ricreaand is recreated after 300 ms, once the window has stopped changing size. OtherwiseVIDEO_RIDIMENSIONA. - The phone resizes. One size = one encoder. If the aligned size does not change, nothing happens. If it changes: the new encoder is prepared, the size is sent to the PC,
VirtualDisplay.resize+setSurface, then the old one is closed; its packets still in flight are discarded.
An input event computed on the old size is discarded by the phone (9.2 “Injection”).
6.6 · App events
EventiApp.java registers a TaskStackListener as long as there is at least one session. Each event schedules a check 150 ms later (events arrive in bursts); in addition, a check every 3 s, because a protected window can appear without task events.
| Event | Pairs | When | What the PC does |
|---|---|---|---|
orientamento | display verticale=0|1 valore=N | At the first check, then when the app switches from portrait to landscape or vice versa (not on every value change); never for the mirror | Fixed-size 9:16 window or column |
protetta | display protetta=0|1 | At the first check, then when it changes | Message in place of the black image |
spostata | task display | A task moves to another screen (app opened on the phone) | Nothing (diagnostics only) |
rimosso | task | A task of the display closes | Nothing (diagnostics only) |
fine | motivo | The phone closes the session by itself | The session restarts as after a drop |
The protected screen is recognized without dumpsys (Protetta.java): captureDisplay shrunk to 5 % and containsSecureLayers(), with a 2 s timeout. Phonestra does not bypass protections: it shows a message.
6.7 · PC side: from session to window
On the PC a session is a SessioneNostra: the video channel, the commands and the events. finestra::vista connects it to a GStreamer pipeline that draws in the window.
let SessioneNostra { display, video: mut flusso, mut comandi, mut eventi, .. } =
SessioneNostra::avvia(&servizio, &Opzioni { display: (l, a, dpi), ..Opzioni::default() }).await?;
comandi.avvia_app("com.android.chrome").await?;
while let Ok(p) = leggi_pacchetto(&mut flusso).await { /* Pacchetto::Dimensione or Pacchetto::Dati */ }
comandi.ridimensiona(l, a).await?;
comandi.ricomincia_video().await?;
while let Some(e) = eventi.recv().await { /* Evento::Orientamento, Protetta, Spostata, Rimosso, Fine */ }
comandi.chiudi(true).await?; // true = remove from recents (the user closed the window)finestra::vista holds the pipeline and redoes the session when needed: connection dropped, component restarted, display to be recreated, orientation changed (FineSessione::{Chiusa, Ricrea, Caduta}). The codec parameters (SPS/PPS) must be merged with the following frame before the appsrc.
The phone's panel
7.1 · Turning the panel off
While apps are used from the PC the phone's screen turns off, but the phone stays awake and unlocked: that way the apps on the virtual displays keep running and receiving touches. When the user picks the phone back up, the panel turns back on; when they leave it there, it turns off again.
Pannello.java calls SurfaceControl.setDisplayPowerMode(token, 0|2) on every physical display: it acts only on the compositor, Android believes the screen is on and any change of state (power button) turns it back on. Since Android 14 the tokens live in DisplayControl, inside services.jar, loaded with a class loader on the SYSTEMSERVERCLASSPATH and the library android_servers.
The PC controls it with VIDEO_PANNELLO (6.2 “Video messages”): video_nostro::pannello sends it without waiting for the reply, ComandiVideo::pannello sends it from a session. There is one panel per phone, not one per window: the one that decides whether to turn it off is the Collegamento, with the phone “in hand” rule described below. When the service dies, the guardian's action 400 turns it back on (5.6 “The two guardians”).
7.2 · Display refresh rate when the panel is off
Samsung phones change the display refresh rate by themselves (10–120 Hz): after a moment of calm it is at 24 Hz. When the panel is turned off SurfaceFlinger switches to 60 Hz, but confirms the change only with the panel's vsyncs, which no longer arrive: its model stays at 24 Hz, takes the frames of the virtual displays at that pace, and apps that draw faster remain stuck waiting for it (Facebook, with AV1 reels decoded in software, starves the audio). Prove §59.
- Before turning off,
Pannello.javareadsmDisplayModePtrandmPeriodConfirmationInProgressfromdumpsys SurfaceFlinger. - If the model is not confirmed at 60 Hz or more, it sets
min_refresh_rate=60and registers with the guardian action 410, which restores the previous value. - It waits for the confirmation, at most 1 s (
ATTESA_60_MS), then turns off. - It immediately restores
min_refresh_rateas it was and removes the guardian's action.
7.3 · The phone in hand
The state is a single value in the Collegamento: a_mano, read with Collegamento::pannello_a_mano. With a_mano false, sessions that open turn the panel off; with a_mano true they leave it on. It lasts as long as the process, across reconnections: a drop does not reset it.
| Event | Where | Effect |
|---|---|---|
| The phone, locked during use, is unlocked | Collegamento::sbloccato | “in hand”: the panel stays on |
| First check after a connection, phone unlocked | Collegamento::sbloccato | “in hand” only if there had been a lock before, or a drop with the phone asleep (7.5 “Drops and reconnections”) |
| Incoming call | 3-second round | panel on, “in hand” (7.4 “Calls”) |
| End of a call with the panel off | 3-second round | panel on, “in hand” |
| Closing of the last session | finestra.rs, pannello_acceso_senza_finestre | panel on, “in hand” |
| Touch, key, scroll, text, paste, zoom, long press or Back from a window | Collegamento::usa_dal_pc | if it was “in hand”: panel off, no longer “in hand” |
| Phone “in hand” idle for the user's screen timeout, with no calls during that time | 3-second round | panel off, no longer “in hand” |
Turning off without touches exists because the phone's screen timeout, during the connection, is at the maximum (5.6 “The two guardians”): a phone unlocked by hand and then left on the table would never turn off. The 3-second round, as long as the phone is “in hand”, asks dumpsys power | grep -m1 lastUserActivityTime= and reads “(N ms ago)” (fermo_da): once the time chosen by the user (the one saved at the start of the connection) has passed, it turns the panel off. This also applies with no windows open: the phone stays awake and unlocked (prove §58 and §60).
Collegamento::sessioni) also counts the drawer's mirror. “Last session” therefore means the last window and the drawer: with the drawer open, closing an app's last window does not turn the panel back on. Opening the mirror also turns the panel off, just like opening a window.7.4 · Calls
The 3-second round reads the call state with dumpsys telephony.registry | grep -o 'mCallState=[12]': 1 means it is ringing, 2 that it is in progress. With two SIMs there is one line per SIM, and one line is enough to count.
| Moment | Rule | Why |
|---|---|---|
| It starts ringing | If the phone is not already “in hand”, panel on and “in hand” | To answer with the phone in hand (prove §58). |
| During the call | The time without touches is counted from the last round with a call (chiamata_alle): the panel does not turn off | With the phone at your ear there are no touches. |
| It ends, and the phone was not “in hand” | Panel on and “in hand”; then turning off without touches applies | Answered with a click from the PC: during the call Android turns the panel back on by itself (proximity sensor), and Phonestra would believe it off; it stayed on forever (prove §60). |
squillava, in_chiamata, chiamata_alle) start from zero at every connection. Before restarting Phonestra for a test, look at all the mCallState lines: a restart during a call leaves the mirror black (prove §61).7.5 · Drops and reconnections
On Samsung phones locking the phone drops Wireless debugging: the connection reopens on unlock. But the connection also drops because of the network, with the phone still unlocked on the table. In both cases the panel is on upon return (the guardian or the user turned it back on), and Collegamento::sbloccato must figure out who did it.
- On the drop,
mantieninotes the time (caduto), only the first time and only if the connection was really open. - At the first lock check after the reconnection, if the phone is unlocked: if before the drop the round had seen the lock (
bloccato_durante_uso), the user unlocked it: “in hand”. - Otherwise
dumpsys power | grep -m1 mLastSleepTime=tells how long ago the phone fell asleep (dormito_da). If that happened no longer ago than the time elapsed since the drop plusMARGINE_CADUTA(15 s, generous compared with the 8 s within which the PC notices the drop: 5 s of waiting for the reply plus 3 s between checks), the phone has slept and the user unlocked it: “in hand”. - If it slept earlier, the drop was a network one: the panel turns off again as the sessions restart. If the line is missing or the command does not reply, when in doubt “in hand”.
“Unlocked by hand” is decided from the first lock check after the connection, not from the reconnection alone (prove §55 and §57).
7.6 · The change log
Every panel change leaves a line in Phonestra's log, which ends up in the PC's journal:
$ journalctl --user --since today | grep -i phonestra| Line | When |
|---|---|
[collegamento] sbloccato a mano: il pannello resta acceso | manual unlock, even after a drop (“unlocked by hand: the panel stays on”) |
[collegamento] caduta senza blocco: il pannello si rispegne | reconnection after a network drop (“drop without lock: the panel turns off again”) |
[collegamento] chiamata in arrivo: pannello acceso | it starts ringing (“incoming call: panel on”) |
[collegamento] fine della chiamata: pannello acceso, si rispegne senza tocchi | end of a call with the panel off (“end of the call: panel on, turns off again without touches”) |
[collegamento] telefono in mano non toccato da N s: pannello spento | turning off without touches (“phone in hand not touched for N s: panel off”) |
[finestra] usato dal PC: pannello spento | first touch from the PC with the phone “in hand” (“used from the PC: panel off”) |
[finestra] ultima finestra chiusa: pannello acceso | closing of the last session (“last window closed: panel on”) |
[servizio]: when the display needs to be brought to 60 Hz (7.2 “Display refresh rate when the panel is off”), phonestra-servizio: video: frequenza del display a 60 Hz prima dello spegnimento appears.Audio
8.1 · The recipe
The phone's audio plays from the PC's speakers, without interruptions and in sync with the video. The recipe came from measurements: loopback capture, AAC, timestamps from the sample count and a precise startup order.
| Choice | Why |
|---|---|
Loopback capture: AudioPolicy with ROUTE_FLAG_LOOP_BACK on the sound usages (media, games, assistant, navigation, system sounds…: Audio.USI) | Meanwhile the phone stays silent; once the policy is removed, it plays again by itself. No setting to restore. |
| AAC-LC 192 kbit/s, 48 kHz stereo (Android's software encoder) | As clean as PCM (misure §42) and much lighter on Wi-Fi. PCM as a test fallback. |
| Timestamps from the sample count (samples × 10⁶ / 48000) | Regular: the encoder's output time comes in bursts (1–3 ms and 30–40 ms instead of 21). |
| Reading at priority −19, and reading, encoding and sending on separate threads | If Wi-Fi or the encoder slow down, reading does not stop and no samples are lost. |
On the phone (CanaleAudio.java, which uses the classes of the measuring tool Audio.java) there are four threads: audio-lettura, audio-codifica, audio-spedizione (queue of 256 packets, about 5 s, which drops the oldest ones, counted in persi) and audio-sentinella, which reads from the socket only to notice the close. Each thread catches its own errors: an audio problem closes the channel with an errore line, never the service. Only one audio channel at a time: a new one stops the old one and waits (at most 3 s) for it to have removed its policy.
8.2 · Audio packets
The audio channel goes only from the phone to the PC: orario u64 BE · lunghezza u32 BE · dati. The PC sends nothing; closing the channel stops the capture.
| Flag in the timestamp | Content |
|---|---|
| bit 61 | UTF-8 text of the form chiave=valore …. The first packet is always inizio (formato=aac frequenza=48000 canali=2 bitrate=192000 sorgente=loopback buffer_ms=… registrazione=istanza|statica) or errore …; then lettura tid=… nice=…, misura (one per second), avviso, errore. |
| bit 62 | Codec configuration (AudioSpecificConfig, 2 bytes 11 90), before any data. |
| none | Data: an AAC frame of 1024 samples (21.333 ms), or 1024 PCM samples. |
8.3 · Playback on the PC
On the PC three pieces give each packet its timestamp and decide when to play it; then GStreamer decodes and plays it.
| Piece | What it does |
|---|---|
Durate | The duration of each packet from the sample count (21,333 or 21,334 µs, with no accumulated error). |
Orari | Keeps the timestamps regular and realigns only beyond a 60 ms deviation. |
Margine | Decides when to play each packet: phone timestamp + an offset fixed from the first packet. It starts at 80 ms; a late packet (less than 10 ms before now) moves everything later (a moment of silence, then no gaps) and widens the margin by 40 ms, up to 300. A phone more than 200 ms ahead beyond the margin triggers a realignment. |
Margine::scendi | After 10 s of calm it reduces the margin by skipping a “silence” packet (less than 40 % of the average bytes). With AAC the packet size is almost constant and it never triggers (ch. 18 “Appendix C — Known issues”). |
audio_nostro::riproduci(apritore) is the function the connection calls; it ends if the channel closes and, when cancelled, closes it. PHONESTRA_AUDIO_CODEC=pcm (or raw) uses PCM instead of AAC.
8.4 · The startup order
Audio capture does not start together with the connection: it waits for the drawer's mirror. It is a rule found through measurements.
ATTESA_SPECCHIO 10 s at most, ASSESTAMENTO 5 s, in collegamento.rs). If the mirror is recreated, the capture restarts after 300 ms. With the capture started before the connection's initial sessions had started, the Facebook reels player in the windows starved and the audio had micro-interruptions; verified with alternating tests (misure §48–49). Android's internal mechanism is not yet understood: do not change this order without redoing those tests.The drawer signals the opening of the mirror with Collegamento::specchio_aperto(), which increments a watch counter followed by the audio task. The task only looks at the mirrors opened after the current service started: after a reconnection it waits for the new mirror (prove §53).
8.5 · Who removes the capture
The capture removes itself in every way the service can end, even the most abrupt.
| How it ends | Who removes the audio policy |
|---|---|
| The PC closes the channel, or a new one is opened | CanaleAudio: stops the recorder and calls unregisterAudioPolicy |
The service exits with System.exit | A shutdown hook (audio-fine) |
The service dies suddenly (kill -9) | Android: the policy is tied with linkToDeath to our process |
The guardian has no action for the audio: no shell command removes another process's policy, and none is needed. When Phonestra closes, before detaching the audio, the connection pauses the media that are playing, otherwise they would resume from the phone's speaker (3.4 “Shutdown”).
8.6 · Recording
The “Record the screen” (Registra lo schermo) button in an app's window writes an MP4: the H.264 video as it is (h264parse ! mp4mux) and the AAC audio as it is, without re-encoding. audio_nostro::ascolta() gives a copy of the packets (broadcast), caps_registrazione() the caps with the codec_data of the current audio. It starts from the first keyframe; with no AAC audio in progress the file has no audio.
During the recording the app's display does not change size and is not recreated for orientation (the window scales the image), and the button shows the elapsed time (● m:ss). The file goes to <Video di XDG>/Phonestra/<app> AAAA-MM-GG HH.MM.SS.mp4.
Input and clipboard
9.1 · Input and clipboard messages
The PC's mouse, touchpad and keyboard become fingers and keys on the app's screen; the clipboard travels in both directions, text only, never passwords.
Range 0x50–0x5f, big-endian. Events have id 0 and no response: the PC does not wait for the phone. larghezza/altezza are the size of the image on which the PC computed the coordinates.
| Type | Name | Content |
|---|---|---|
0x50 | TOCCHI | display i32 · larghezza u16 · altezza u16 · n u8 · n × (dito i64 · azione u8 · x i32 · y i32 · pressione f32) |
0x51 | ROTELLINA | display i32 · x i32 · y i32 · larghezza u16 · altezza u16 · orizzontale f32 · verticale f32 |
0x52 | TASTO | display i32 · azione u8 · codice u32 · ripetizione u32 · meta u32 |
0x53 | TESTO | display i32 · testo UTF-8 |
0x54 | INDIETRO | display i32 · azione u8 |
0x55 | APPUNTI_SCRIVI | display i32 · incolla u8 · testo UTF-8; responds if id is not 0 |
0x56 | APPUNTI_LEGGI | empty request; response stato u8 · testo |
0x57 | APPUNTI_ASCOLTA | attivo u8; responds if id is not 0 |
0x58 | APPUNTI_CAMBIATI | service → PC, unsolicited: stato u8 · testo |
0x5c | CONTEGGI | diagnostics: injected, failed, discarded, clipboard notices, ascolto_appunti, last error |
0x5d | PROVA | test commands (InputProva.java), on their own thread (input-prova): apri <l> <a> <dpi>, avvia <display> <pacchetto>, azione <display> <azione> [pacchetto], firma <display> (32×56 luminance), chiudi, salva-appunti, ripristina-appunti, esterno <0|1> <testo> |
A Rust test in input_nostro.rs reads Input.java and Video.java and checks that no message number is used twice.
9.2 · Injection
On the phone, input becomes Android events injected into the right display, in order, by a single thread.
| Step | How |
|---|---|
| Queue | The thread that reads commands does not inject; it queues the messages for the “input” thread, a single one, so order is preserved. |
| Event | InputEvent.setDisplayId always, then injectInputEvent asynchronously. A rejection is counted (falliti); the log records the first error and then one every 100. |
| Fingers | Each PC identifier (−1 mouse, −2 generic finger, 10 and 11 for pinch) becomes a finger with a local number 0–9, at most 10. Clicks and drags are fingers (SOURCE_TOUCHSCREEN): dragging scrolls, a long press opens menus. Every event carries all the fingers that are down. |
| Scaling | Coordinates × (display size / PC size). An event computed on a size different from the one declared by the video is discarded: during a resize a click would land in the wrong place. For this reason the PC rounds sizes to multiples of 8, like the phone. |
| Wheel | ACTION_SCROLL, SOURCE_MOUSE, fractional values, within ±16. |
| Keys | KeyEvent from a virtual keyboard. Text: the virtual key map, one character at a time, in practice ASCII only; the PC sends the rest (accented letters, symbols) with “paste”. |
| Back | KEYCODE_BACK; on the main display when it is off, POWER to turn it back on. |
| Paste | Phone clipboard + KEYCODE_PASTE. |
9.3 · Keyboard and mouse on the PC
finestra.rs translates GTK events (finestra::tastiera for keys). Modifiers follow the values of KeyEvent.META_* (META_SHIFT, META_CTRL).
| On the PC | On the phone |
|---|---|
| Click, drag | Finger |
| Right click | Long press (selects and opens the Android menu) |
| Wheel, two fingers on the touchpad | Scrolling at the pointer position |
| Ctrl + wheel, Ctrl++ / Ctrl+−, pinch on the touchpad | Two-finger pinch (zoom; with the keys, at the center of the window) |
| Esc, mouse “back” button, Back button | Back (Esc can be turned off in the preferences) |
| ↑ / ↓ | One scroll step; real keys while typing (after a letter, Backspace or Delete) |
| Page Up / Page Down | Scrolling by 80% of the window height |
| Enter, Backspace, Delete, Tab, ← →, Home, End | The corresponding Android keys (also with Alt: Alt+← is an arrow, not Back) |
| ASCII letters | TESTO; the rest via paste |
| Ctrl + letter | The Android shortcut (Ctrl+C, Ctrl+A…), pressed and released |
| Ctrl+V, Shift+Insert | The PC clipboard goes into the app |
| Ctrl+R, Ctrl+W, Ctrl+Shift+C | Stay with the PC: Rotate, Close app, Copy screenshot (10.3 “App windows”) |
| Alt + other | Stays with the desktop (Alt+F4…) |
9.4 · Clipboard
The service talks directly to IClipboard, as the package com.android.shell: no ClipboardManager, hence no Looper to run and no detour through the Samsung service semclipboard, which rejects the write with the wrong context. Signatures change between versions: the longest variant is chosen whose parameters, after the fixed ones, are only strings and integers.
| Direction | How | What does not pass |
|---|---|---|
| Phone → PC | APPUNTI_ASCOLTA 1 at startup; on every copy APPUNTI_CAMBIATI (appunti::ascolta) → Collegamento::appunti → GTK clipboard | Copies marked as sensitive (state 2) or of unknown sensitivity (3); texts over 200,000 bytes; echoes |
| PC → phone | Only with Ctrl+V in a window: APPUNTI_SCRIVI with incolla=1 | Texts from password managers (x-kde-passwordManagerHint): alert “Password non inviata al telefono” (Password not sent to the phone); texts that are too long: “Testo troppo lungo: usa il trasferimento file” (Text too long: use file transfer) |
Echoes. What Phonestra puts into the phone's clipboard must not come back to the PC. The service ignores its own writes (even an identical text within 3 s, because the notice may arrive later) and Samsung sends every notice twice (discarded within 0.5 s); the PC remembers the texts it sent (Collegamento::e_un_rimbalzo). The service rereads the clipboard only if the current clip is its own: reading another app's clip would make the “ha incollato dagli appunti” (pasted from the clipboard) notice appear.
main sets the copy immediately and, to be safe, sets it again the next time a Phonestra window is activated; if something else is copied on the PC in the meantime, the pending one is forgotten.The user interface
10.1 · Interface architecture
The interface uses GTK4 and libadwaita, with a style faithful to libadwaita (GNOME title bar, boxed lists, pill buttons). Phonestra follows the system's light or dark theme: segui_tema applies to all windows, the SCURO palette of cassetto.rs to the drawer, “Add a phone” (Aggiungi un telefono) and the wizard (the app windows have rules of their own); the mockups are light only. No .desktop file is installed. The design proposals every screen comes from are in mockup/; the rules in notes/interface.md.
| Window | File | What it is | More detail |
|---|---|---|---|
| Drawer | cassetto.rs | The main window: apps, notifications, phones, tools, the phone's screen | 10.2 “The drawer” |
| App window | finestra.rs | One per app, with its own virtual display | 10.3 “App windows” |
| “Receive files…” (Ricevi file…) | ricevi.rs | An adw::Dialog for choosing files on the phone | 11.2 “Receiving files from the phone” |
| “Add a phone” (Aggiungi un telefono) | prepara.rs | The first connection without a cable | 10.5 “The first connection” |
| Cable wizard | procedura.rs | The USB cable fallback | 10.5 “The first connection” |
| System alerts | avvisi.rs | The phone's notifications on the desktop, via D-Bus | 10.6 “Notifications and alerts” |
10.2 · The drawer
cassetto.rs is the main window, the drawer. It subscribes to four watch of the Collegamento: stato(), guasto(), info() (battery and network) and notifiche(); it reaches the component through finestra::vista.
| Part | What it contains |
|---|---|
| Title bar | The Phonestra symbol, the name and the phone pill with the connection state. When the component has failed, the pill turns red: “Phonestra won't start on the phone” (Phonestra non parte sul telefono). |
| Pill menu | Header with name, “model · Android N”, Wi-Fi and battery; Reconnect (Riconnetti), Rename… (Rinomina…), Turn off Wireless debugging on exit (Spegni il Debug wireless alla chiusura; disabled, “Coming soon”), Forget this phone… (Dimentica questo telefono…). |
| Sidebar | Apps (App) and Notifications (Notifiche; with the counter); section My phones (I miei telefoni) with the active phone, the other configured phones (“not active”, non attivo) and Add phone (Aggiungi telefono); section Tools (Strumenti) with Install app… (Installa app…), Send files… (Invia file…), Receive files… (Ricevi file…); at the bottom Preferences (Preferenze) and About (Informazioni; the libadwaita “About” window with the logo). |
| Apps page | Search, FAVORITES (PREFERITI) and ALL APPS (TUTTE LE APP). Typing in the drawer searches; Enter opens the first app found. |
| Notifications page | The phone's notifications grouped by app, at most 2 per app and then “N more notifications from … ›” (altre N notifiche di … ›); Hide (it stays on the phone) (Nascondi (sul telefono resta)) and Hide all (Nascondi tutte) act only in Phonestra, and a hidden notification reappears if it is updated. |
| Drawn phone | On the right: time, Wi-Fi, battery and the phone's real screen (the mirror, interactive, with finestra::vista(…, SCHERMO, …); it receives keys only after a click). Files can be dragged onto it (11.1 “Installing and sending”). |
| Veil | When the phone cannot be used, a veil explains why; with the connection lost or the component failed it offers “Reconnect now” (Riconnetti ora). |
The app list comes from the helper (app::elenco, command app <lato>) and is valid only if it ends with the line fine\t<n>: a list interrupted by a drop is not used and is reread on reconnection (elenco_intero). The app menu (right click) has Open (Apri; or Bring to front, Porta in primo piano, if it is already open), Close app (Chiudi app) if it is open, favorites, App info (Informazioni sull'app) and Uninstall… (Disinstalla…; disabled for system apps).
10.3 · App windows
finestra.rs: one window per app, with finestra::vista in the center (video, mouse, keyboard) and around it the bar with Back (Indietro), Screenshot, Record the screen (Registra lo schermo) and the More commands (Altri comandi; ⋮) menu.
| Command | Shortcut | What it does |
|---|---|---|
| Screenshot | Saves to <Immagini di XDG>/Phonestra/<app> AAAA-MM-GG HH.MM.SS.png and copies to the clipboard | |
| Copy screenshot (Copia screenshot; ⋮ menu) | Ctrl+Shift+C | Copies only |
| Record the screen | MP4 in <Video di XDG>/Phonestra; the button becomes ● m:ss (8.6 “Recording”) | |
| Rotate (Ruota; ⋮ menu) | Ctrl+R | Swaps the window's sides; nothing if it is maximized or recording |
| Close app (⋮ menu) | Ctrl+W | Closes the window and removes the app from recents |
- The virtual display follows the window (6.5 “Resizing”).
- Portrait-only apps (the
orientamentoevent): fixed-size 9:16 window, no maximizing and no draggable edges; in full screen the app sits in a column shaped like the phone. - Connection lost or component failed: the last image stays, blurred, with “Reconnect now” and “Close” (Chiudi); the session restarts on its own when the connection returns and the app reappears where it was. With the phone locked only the subtitle changes.
- Protected screen: a message instead of the black image.
- When the window is closed, the app is removed from the phone's recents (
ComandiVideo::chiudi(true)); if it was the one controlling playback (YouTube, Facebook), it is paused. When the last session closes, the panel turns back on (7.3 “The phone in hand”).
10.4 · Preferences
The drawer's Preferences page saves the user's choices in preferenze.toml (3.6 “Configuration and data on the PC”). Each item has its own value.
| Tab | Item | Value in preferenze.toml |
|---|---|---|
| App windows | Esc goes back | esc_indietro |
| Notifications | Pop-up alert | avvisi |
| Notifications | App name only | solo_nome_app |
| Notifications | Apps allowed to alert (one switch per app) | app_silenziate |
| Files | Files sent to the phone: one of the 6 folders of azioni::CARTELLE (Download, Documents, Pictures, Camera, Music, Movies; in Italian Download, Documenti, Immagini, Fotocamera, Musica, Video) | cartella_file |
| Files | Files received from the phone: a folder on the PC; choosing the Downloads (Scaricati) folder saves “nothing”, so it follows the system folder | cartella_ricevuti |
| Phone apps | App list: Update now (Aggiorna ora) | — |
| Language | Interface language: Automatic (system) (Automatica (del sistema)), Italiano or English; it takes effect at the next start (lingua::attuale) | lingua |
configurazione::Preferenze)10.5 · The first connection
prepara.rs is “Add a phone” (Aggiungi un telefono) without a cable: the list of settings to enable on the phone (same Wi-Fi network, Developer options, any protections, Wireless debugging with “Pair device with pairing code”). For each item, the word to search for in Settings and “Ask Google ↗” (Chiedi a Google ↗), which opens Google's AI Mode with the question already written.
Items visible on the network tick themselves: Wireless debugging turned on (_adb-tls-connect) and the pairing code screen open (_adb-tls-pairing), which enables the 6-digit field. With the code, Phonestra pairs (adb::abbina), connects with adb_client, removes the expiry of the authorization (settings put global adb_allowed_connection_time 0) and saves the phone.
procedura.rs is the USB cable fallback, for Android 10 or earlier (it opens from “Android 10 or earlier? Connect with the cable” (Android 10 o precedente? Collega col cavo)). It advances on its own by checking the cable every second (usb.rs reads /sys/bus/usb/devices without opening the device) and recognizes these cases:
Cable state (usb.rs) | How it is recognized | Step shown |
|---|---|---|
SoloRicarica | no useful interface, known Android manufacturer (PRODUTTORI_ANDROID) | choose “File transfer” (Trasferimento file) from the USB notification |
DebugSpento | MTP interface (06/01/01) without ADB | enable Developer options and USB debugging |
DebugAttivoSoloRicarica | ADB (ff/42/01) without MTP | in “Charging only” (Solo ricarica) the systemd permission (uaccess) is missing and access may be denied: choose “File transfer” (Trasferimento file) |
DebugAttivo | MTP and ADB | “Always allow” (Consenti sempre), then the switch to Wi-Fi |
After the cable, telefono.rs retries 3 times with a 1 s pause (the error “got AUTH” means consent is still missing), sets adb_allowed_connection_time 0 and adb_wifi_enabled 1 and waits up to 30 s for the user's confirmation. After 20 s stuck at the first step, the “What the PC sees” (Cosa vede il PC) box appears with the list of USB devices, to photograph for whoever is helping remotely.
The per-brand instructions come from data/instructions.toml, embedded at build time. The families are chosen by searching for the words in marche in the manufacturer read from the cable; the last one, “Other phones” (Altri telefoni), has an empty marche and acts as the fallback. The paths are the phone's Settings names as they appear on the phone, in Italian here and in English in data/en/instructions.toml (the one used with the English interface); in the example: “Impostazioni › Informazioni sul telefono › Informazioni sul software” (Settings › About phone › Software information), “Numero build” (Build number), “Impostazioni › Opzioni sviluppatore” (Settings › Developer options) and “Impostazioni › Sicurezza e privacy › Blocco automatico” (Settings › Security and privacy › Auto Blocker).
[[famiglia]]
nome = "Samsung"
marche = ["samsung"] # words searched in the manufacturer
verificata = true # paths tested on a real phone
percorso_build = ["Impostazioni", "Informazioni sul telefono", "Informazioni sul software"]
voce_build = "Numero build"
percorso_debug = ["Impostazioni", "Opzioni sviluppatore"]
grigio = "…" # what to do if the USB debugging item is grayed out
sicurezza = "…" # extra step (Xiaomi: security settings)
[famiglia.prima] # a setting to change before anything else
percorso = ["Impostazioni", "Sicurezza e privacy", "Blocco automatico"]
voce = "Blocco automatico"PHONESTRA_PROVA_PASSO both windows show a specific step without a phone, for interface tests: acceso, codice, fatto for prepara.rs; debug, consenti, xiaomi, wifi, fatto for procedura.rs.10.6 · Notifications and alerts
notifiche.rs reads dumpsys notification --noredact (title and text of private notifications too) in the connection's 3-second round, and battery (dumpsys battery, “charging” with status 2 or 5) and network (cmd wifi status) every 30 s. It discards ongoing notifications (ONGOING_EVENT, FOREGROUND_SERVICE), group summaries (GROUP_SUMMARY) and those with neither title nor text; it sorts them newest first.
avvisi.rs turns new notifications into system notifications with the D-Bus service org.freedesktop.Notifications (GNOME, KDE, Xfce): GTK notifications on GNOME work only for programs with a .desktop file, which Phonestra does not install. “Already seen” notifications are fixed at the first read; today, however, the first read happens with the drawer just opened, when the list is still empty, so at the first real read the notifications already on the phone raise an alert (ch. 18 “Appendix C — Known issues”). Clicking an alert opens the app. The preferences allow turning them off, showing only the app name (the text becomes “New notification”, Nuova notifica) and muting individual apps. The app icons for the alerts are written to ~/.config/Phonestra/icone/<pacchetto>.png.
Apps and files
11.1 · Installing and sending
azioni.rs uses only the Android shell: no app on the phone, no extra permissions.
| Action | From where | How |
|---|---|---|
| Installing an app | Install app… (Installa app…), or an .apk dragged onto the drawn phone (with confirmation) | Copy with sync: to /data/local/tmp, pm install -r; the reasons for a rejection are translated into plain words (azioni::spiega_installazione) |
| Uninstalling | Uninstall… (Disinstalla…) in the app menu (user apps only, pm list packages -3), with confirmation | pm uninstall; the app leaves the favorites and its window closes |
| Sending files | Send files… (Invia file…; several files too), or files dragged onto the drawn phone | Copy to /sdcard/<cartella> (the folder from the preferences, Download if not chosen) with a free name, without overwriting, and notification to the media scanner: the file appears in Gallery and Files |
- One transfer at a time: a second one is rejected with the alert “Wait for the current transfer to finish” (Aspetta la fine del trasferimento in corso). The transfer card shows the progress and is canceled with ×.
- After an installation or uninstallation, the app list is reread.
11.2 · Receiving files from the phone
ricevi.rs is the “Receive files…” (Ricevi file…) window (adw::Dialog): you browse the phone and choose the files to copy to the PC (SPECIFICATION §11.1, mockup receive-files.html).
| Place | Phone folder | Opens as |
|---|---|---|
| Recent (Recenti) | the last 7 days of Camera (Fotocamera), Screenshot, Download, Documents (Documenti) and WhatsApp (images, videos, documents, audio), in groups Today, Yesterday, This week (Oggi, Ieri, Questa settimana) | list |
| Camera (Fotocamera) | /sdcard/DCIM/Camera | thumbnails |
| Screenshot | /sdcard/DCIM/Screenshots or /sdcard/Pictures/Screenshots | thumbnails |
| Download | /sdcard/Download | list |
/sdcard/Android/media/com.whatsapp/WhatsApp/Media (Android 11+) or /sdcard/WhatsApp/Media | list | |
| Documents (Documenti) | /sdcard/Documents | list |
| Phone storage (Memoria del telefono) | /sdcard | list |
| SD card (Scheda SD) | the cards found in /storage (more than one: “SD card 1”, “SD card 2”…) | list |
STA2, the first candidate folder that exists wins); SD cards are found by listing /storage (minus emulated and self)- Clickable path and “up” button, up to Phone storage or SD card; search by name; sort by date, newest or oldest first; thumbnails or list.
- Folders are read with
sync.rs(4.6 “Copying files: sync:”): they are read in full and shown 200 at a time (“Show N more”, Mostra altri N); hidden files (starting with a dot) do not appear. - Checkmarks persist when changing folder; “Select all” / “Deselect all” (Scegli tutti / Togli tutti); a double click receives the file immediately.
- The thumbnails are made by the helper (
miniature <lato> <percorsi in base64>,Miniature.java) in groups of 40, at 192 pixels: each launch costs about one second. Photos are downscaled byBitmapFactoryand straightened using EXIF; videos give a frame withMediaMetadataRetrieverthrough aMediaDataSource.
The copy is done by the drawer, with the same transfer card as sending: each file arrives with sync::ricevi in the folder of Preferenze::cartella_ricevuti (the Downloads (Scaricati) folder if not chosen), with a free name and the phone's modification date. At the end an alert says how many files arrived, with Open folder (Apri la cartella; with a single file, the folder opens with the file already highlighted).
PHONESTRA_PROVA_RICEVI=<posto> opens “Receive files…” (Ricevi file…) by itself on connection, at the given place (for example Fotocamera), for interface tests; phonestra-prova file tests listing, receiving and thumbnails from the command line.Testing and diagnostics
12.1 · Tests on the PC
Two levels: the tests on the PC, which need no phone and run at every commit, and the tests on the real phone with phonestra-prova.
cargo test tests the pure parts: the formats of the chunked messages (commands, audio, video, input), the preamble, the ready line, CIAO, the heartbeat, shell,v2 packets, dispatching without a network, audio timing and margin, real AAC decoded and written to MP4, flow control against a fake adbd, mDNS, parsing of dumpsys, message numbers matching between Java and Rust, and these manuals (tests/manual.rs, 12.5 “The manuals and their checks”).
12.2 · The phonestra-prova tool
A separate executable, without a user interface, that uses the same code as the program. The component tests are run with the phone unlocked and Phonestra closed; each one cleans up after itself and checks that nothing is left behind on the phone.
| Command | What it does |
|---|---|
| Connection | |
cerca, collega, banner | mDNS discovery, connection to the saved phone, features announced by adbd |
abbina <codice> [ip:porta] | Pairing with the 6-digit code; without an address it finds the pairing-code screen by itself with mDNS |
usb, prepara, shell-usb <comando>, procedura | The cable: status, Wi-Fi preparation, shell, the fallback procedure |
shell <comando> | A shell command over Wi-Fi (instead of adb shell) |
throughput [MB] [--senza-delayed-ack] [--payload N] [--finestra N] [--latenza] [--exec] [--alla-lettura] | Throughput and latency of the ADB transport |
canali | Several commands at once on the same connection and a file copy with sync: (old test) |
| Helper and drawer | |
app, sfondo, notifiche, codificatori | Helper commands and drawer reads |
file <cartella>, file ricevi <percorso> <destinazione>, file miniature <percorsi> | “Receive files…” (Ricevi file…) from the command line: listing, copy to the PC, thumbnails |
| Component | |
servizio [secondi] [--sparisci] | The skeleton of the component: startup, CIAO, heartbeat, guardian, exit; --sparisci simulates a PC that disappears |
custode [abbandona] | A test guardian like the connection's one (screen timeout at 1,234 s, without the volume): it closes the channel and checks that the value is restored; with abbandona it exits without closing it, and the restore is checked by hand (shell settings get system screen_off_timeout) |
audio-componente <secondi> [aac|pcm] [--ascolta] [--uccidi] | Audio from the component into a file (phonestra-prova.aac/.wav), check of timing and policies |
video-componente app|schermo [--app P] [--secondi N] [--codec C] [--senza-pannello] | Virtual display or mirror, keyframes, resizing, panel, events; saves the stream for ffprobe |
input-componente appunti|tocchi|testo|tutte [--misura LxA] [--dpi D] [--app P] [--azione A[:P]] [--tocca X,Y] | Clipboard, taps, scroll wheel, pinch, text, verified with the “signature” of the test screen |
| Study | |
video-prova schermo|chiave|istanze|protetto|task|permessi|codificatori | The video measurement tool of the study |
audio-nostro <secondi> [submix|loopback|render] [pcm|aac] [senza-priorita] [voce] | The audio measurements of the study |
phonestra-prova$ ./target/debug/phonestra-prova servizio 10
$ ./target/debug/phonestra-prova video-componente app --secondi 15
$ PHONESTRA_DEBUG=1 ./target/debug/phonestra-prova input-componente tutte12.3 · Rules for tests on the phone
Tests on the real phone follow a few rules: the phone must go back to how it was, and every measurement must stay on record.
phonestra-provafirst; the systemadbonly for diagnostics the tool cannot do (it uses a different key: the phone asks for a new authorization).- At the end of a test: shut down any
adbserver that was started and any open sessions; do not leave phone settings changed. - Before restarting Phonestra for a test, check all the
mCallStatelines (one per SIM): never during a call (7.4 “Calls”). - Measurements and results go in
notes/connection-tests.md, with the section number (§).
12.4 · Diagnostic variables
The PHONESTRA_* variables turn on diagnostics or change a parameter, for tests and measurements.
| Variable | Effect |
|---|---|
PHONESTRA_DEBUG=1 | Diagnostics on the terminal (works with any value): audio measurements, frames drawn and dropped, clipboard, window scaling |
PHONESTRA_FOTO=<cartella> | Every window saves itself as PNG 6 s after opening and then every 6 s (foto.rs) |
PHONESTRA_PROVA_PASSO=… | Shows a step of the first connection without a phone (10.5 “The first connection”) |
PHONESTRA_PROVA_RICEVI=<posto> | Opens “Receive files…” (Ricevi file…) by itself on connection, at the given place |
PHONESTRA_AUDIO_CODEC=pcm | PCM audio instead of AAC (also raw) |
PHONESTRA_VIDEO_FPS, PHONESTRA_VIDEO_PRIORITA, PHONESTRA_VIDEO_PROTETTA=0 | Test switches for the encoder and for the protected-screen check (they become max_fps=, priorita=, protetta= of VIDEO_APRI) |
PHONESTRA_ADB_DELAYED_ACK, PHONESTRA_ADB_PAYLOAD, PHONESTRA_ADB_FINESTRA | ADB transport parameters (4.4 “Channels and flow control”) |
The log of Phonestra started from the AppImage is in the PC's journal: journalctl --user --since today | grep -i phonestra (7.6 “The change log”).
12.5 · The manuals and their checks
Both manuals are generated: the sources are in docs/sources/, one per chapter in technical/ and user/, and build.py assembles them into docs/Technical Manual.html and docs/User Manual.html, two self-contained files, readable even when downloaded on their own. Their style is the common style of the manuals of the seven projects, identical in all of them and embedded in each page: style.css and manual.js (the search box at the top of the sidebar and the Copy button on command blocks) are copied from it as they are, and build.py only adds the few rules for elements found only here. The cover shows the official logo (logos/phonestra-logo-horizontal-light.png, embedded in the page).
tests/manual.rs runs python3 docs/sources/build.py --controlla, which fails if:
- the published file does not match the sources (the manuals are not edited by hand);
- the page does not embed the common
style.cssandmanual.jsunchanged; - a source file is missing from ch. 17 “Appendix B — File map” or the map lists a file that no longer exists;
- a cited Rust symbol (such as
Collegamento::mantieni) does not exist in the sources, or a cited Java method (such asServizio.comandi) is not in the class's file; - a cited file does not exist in the repository;
- a
PHONESTRA_*variable in the code is not documented, or the manuals cite one that does not exist; - a command of
phonestra-provaor of the helper is not documented; - an internal link leads to a section that does not exist, or the version from
Cargo.tomldoes not appear; or the running text of either manual contains Italian sentences (outside code, interface labels with the Italian label in parentheses after them, and program output).
Build and release
13.1 · Required tools
Building Phonestra, its component and the manuals requires these tools. The reference system is Debian 13 “trixie”.
| Tool | What for | Notes |
|---|---|---|
| Stable Rust (2024 edition) | Everything on the PC | cargo lives in ~/.cargo/bin: add it to the PATH in commands. |
GTK 4.12+, libadwaita 1.5+, GStreamer 1.x (with -dev) | Building and running on the development PC | Required plugins: base, good, bad, gstreamer1.0-libav (for avdec_aac) and gst-plugin-gtk4 (gtk4paintablesink). |
JDK (javac) | Building the component | Compiled with --release 11; any recent JDK will do. |
| D8 from R8 9.4.26 | From Java classes to dex | In strumenti/r8.jar (not in the repository: URL and SHA-256 fingerprint in android/helper/build.sh). |
podman | Building the AppImage | Container phonestra-appimage (13.4 “The AppImage container”). |
Python 3 with pygments and Pillow | Generating the two manuals | python3 docs/sources/build.py writes docs/Technical Manual.html and docs/User Manual.html; packages python3-pygments and python3-pil (Pillow is needed for the official logo on the cover). It is also used by cargo test. |
Testing requires a phone with Android 14 or later, Wireless debugging turned on and the PC on the same Wi-Fi network.
strumenti/r8.jar is downloaded with the command written in android/helper/build.sh. The image of the phonestra-appimage container is rebuilt with podman build. The developer's configuration (~/.config/Phonestra: ADB key and paired phones) stays on the PC: on a new PC the phone must be paired again. The AppImages published on the site are rebuilt from the code.13.2 · Everyday commands
A few commands cover everyday work; cargo test also checks that the manuals are in step with the code.
$ export PATH=$HOME/.cargo/bin:$PATH
$ cargo build # program and test tool
$ cargo test # tests on the PC, without a phone; also checks the manuals
$ cargo clippy --all-targets # no warnings allowed
$ cargo run --bin phonestra # runs Phonestra from the sources
$ android/helper/build.sh # rebuilds the phone component
$ python3 docs/sources/build.py # regenerates the two manualscargo test (seconds) → if you touched the component, android/helper/build.sh and the phonestra-prova test of that piece → the real Phonestra on the phone. Before every commit: cargo build, cargo test and cargo clippy with no warnings.docs/sources/, never in the HTML files in docs/: the next generation would wipe out the change, and cargo test fails until the published files match the sources (12.5 “The manuals and their checks”).13.3 · Building the component
The phone component is built outside cargo, with a script: javac, then D8. The resulting jar goes into the repository.
$ curl -L -o strumenti/r8.jar https://dl.google.com/android/maven2/com/android/tools/r8/9.4.26/r8-9.4.26.jar
$ android/helper/build.sh
creato android/phonestra-helper.jarThe script compiles with javac --release 11 the fake Android classes in stub/ (signatures only: they serve the compiler and do not end up in the jar; the phone has the real ones) and the sources in src/phonestra/, then D8 converts them to dex with --min-api 34 and --lib pointing at the JDK.
When you use a new public Android class, add its stub with only the methods used. Hidden APIs are called through reflection and usually have no stub; the exceptions are the hidden classes we extend, which the compiler must know: android.app.TaskStackListener (EventiApp.java) and android.content.IOnPrimaryClipChangedListener (Input.java).
include_bytes!, and an old jar makes the phone run the previous code.13.4 · The AppImage container
A single file that starts on all common distributions: it is built on an old glibc and carries GTK, libadwaita and GStreamer with it, but uses the system's graphics drivers.
packaging/Containerfile starts from Ubuntu 22.04 (glibc 2.35), so the executable requires at most glibc 2.34 and starts even on 2022 systems. The GTK and libadwaita of Ubuntu 22.04 are too old: the container builds into /opt/phonestra wayland 1.22, wayland-protocols 1.36, glib 2.80, graphene 1.10, GTK 4.14 and libadwaita 1.5 (packaging/build.sh, meson), plus gst-plugin-gtk4 0.13.5 from gst-plugins-rs.
$ podman build -t phonestra-appimage packaging
$ podman run --rm -v .:/phonestra:Z phonestra-appimage sh -c 'CARGO_TARGET_DIR=target/appimage cargo build --release && packaging/collect.sh'collect.sh on its own packages whatever executable it finds in target/appimage/release, even an old one: after every code change, cargo build --release must be run again in the container first (on 28 Sep 2026 an old version was packaged).The result is target/appimage/Phonestra-<versione>-x86_64.AppImage; the version comes from Cargo.toml.
13.5 · What collect.sh does
collect.sh takes the executable built in the container and turns it into an AppImage, in five steps.
- Copies the executable and, recursively with
ldd, the libraries it uses, except those that must come from the system: glibc,libstdc++,libgcc_s, graphics drivers and the libraries that load them (GL, EGL, DRM, gbm, Vulkan),libX11andlibxcb, fontconfig, freetype,libexpat, PipeWire, ALSA, udev. - Copies the GStreamer plugins that are needed (
coreelements,app,typefindfunctions,playback,videoparsersbad,videoconvert,videoscale,libav,opus,audioconvert,audioresample,autodetect,pulseaudio,isomp4,vaapi,va) andgtk4, withgst-plugin-scanner. - Copies the image loaders (PNG, JPEG, SVG), the GSettings schemas, the fallback Adwaita icons and the dconf GIO module: without it, GTK does not read the desktop settings (the window title bar was left with only the X). Then the licenses, in
usr/share/doc/phonestra/:LICENSE.md,NOTICE.mdandthird-party/, with the Ubuntu copyright file of every package a copied file comes from, the license texts they point to, those of the libraries built in the container (saved bybuild.sh), the crates' ones (rust-licenses.py) and the AppImage runtime's. - Moves into
usr/lib/riservathe libraries that the system's drivers also use (wayland, zlib, zstd, libxml2, libffi, libelf, libva, thelibxcb-*ones…): they are used only if the system lacks them or has versions that are too old. - Fixes the paths with
patchelf($ORIGIN), addsAppRun, the icon (logos/icons/phonestra-256.png) and a.desktopfile withNoDisplay=true(needed byappimagetool, not installed), and creates the AppImage with the static runtime (nolibfuse2).
13.6 · AppRun
packaging/AppRun prepares the environment and launches usr/bin/phonestra: the system's libraries and the bundled ones must not mix.
| Setting | Why |
|---|---|
Only the bundled GStreamer plugins, registry in ~/.cache/Phonestra | The system's plugins are built for a different GStreamer. |
| Image loaders with the paths of this run | The loaders file has absolute paths (@APPDIR@ replaced at startup). |
| No system GIO modules | Built for a different glib, they break programs. |
GSK_RENDERER=gl | The GTK 4.14 default gets gradients wrong with older Mesa; a value set by the user wins. |
| System icons before ours | Ours are only a fallback. |
Fallback libraries linked into ~/.cache/Phonestra/riserva only when needed | For example a libwayland-client older than 1.21. |
13.7 · Testing on distributions
packaging/test-distributions.sh starts the AppImage in Ubuntu 22.04, Debian 12, Fedora 43 and Arch containers, connected to the PC's Wayland display and graphics card, with an empty configuration: Phonestra must open “Add a phone” (Aggiungi un telefono). With PHONESTRA_FOTO the window is saved as PNG and the script reports the errors in the log.
13.8 · Publishing a release
A new release is published in four steps, always in the same order.
- Version in
Cargo.toml(for release candidates:1.0.0-rc.N),python3 docs/sources/build.py(the version appears in the manuals), commit and push. - AppImage from the container, startup and connection test, SHA-256 fingerprint.
- Publication on
https://phonestra.nicfio.itwithsite/publish.sh, which takes the AppImage just built or the published one (see the script's header; first--buildalone, to look atsite/public/; before publishing, the search-engine check: description, canonical, robots, icons,og.png, structured data, sitemap), then the annotated git tagv<versione>. - Copy of the AppImage into the user's home (
~/Phonestra-<versione>-x86_64.AppImage): that is the one the user runs. Since 1.0.0 the site replaces the GitHub Releases (the repository is private).
Extending Phonestra
14.1 · A new message on the command channel
A new message touches both sides: the Java class of the part and the Rust module that uses it. These are the steps, in order.
- Choose the number in the part's range (video
0x40–0x4f, input0x50–0x5f) or open a new range of 16 for a new part. - Phone: the constant in the part's class (
static final int NOME = 0x47;inVideo.java), thecasein theswitchofServizio.comandi(orInput.nostrofor input), the handler. If it is a request, respond with the sameidand theRISPOSTAflag, or withERRORE. - Rebuild the jar with
android/helper/build.sh. - PC: the constant in
componente::tipo(orinput_nostro::tipo), the encoding of the content as a pure function, the method that uses it (Condiviso::domandafor a request,Mittente::mandafor an event with no response). - Add the name to the list of the
nessun_tipo_usato_da_due_modulitest: it checks that the number is unique, in the right range and the same in Java and in Rust. - An encoding test with the expected bytes, then a test with
phonestra-prova. - Describe the message in the table of its chapter in
docs/sources/technical/.
ERRORE “tipo sconosciuto” (unknown type) to a new message: the PC must know how to handle it. Change PROTOCOLLO only if an existing message changes meaning.14.2 · A new channel type
A part with a stream of its own, like audio and video, needs its own channel, opened with the preamble.
- Phone: a handler
static void gestisci(LocalSocket s, String tipo)and its entry inServizio.TIPI. The handler runs on its own thread: it catches all errors (including theErrorof hidden APIs) and closes the socket withshutdownInput/shutdownOutputbeforeclose, otherwise a read in progress on another thread keeps the socket open. - PC:
Condiviso::apritore().apri("tipo")gives aCanalewith the preamble already sent. - If the part changes something on the phone, register the inverse action with the guardian.
14.3 · A new action for the guardian
Everything a part changes on the phone must have its inverse action registered with the service's guardian (5.6 “The two guardians”).
A new action for the guardian// When you change something on the phone:
Servizio.custode().imposta("mia-azione", 450, "settings put system qualcosa 1");
// When you have put it back yourself:
Servizio.custode().togli("mia-azione");The command is a shell line run with sh -c after the service dies, without an Android context. Choose the order so that the actions happen in the right sequence (panel 400, refresh rate 410, task 500) and test it with kill -9 on the service (phonestra-prova shell 'kill -9 <pid>').
Conventions
15.1 · Language and style
Phonestra's code reads like its project documents: in Italian, with the why next to the how. The two manuals are the exception: they are written in English.
- Italian everywhere else: names, comments, program messages (the English interface takes its texts from
data/en/), the documents in the repository (README, SPECIFICATION, notes/). Only the text of the two manuals is in English: the pages in docs/ and the chapters they are generated from in docs/sources/. Plain words, short sentences. The manuals quote the interface labels as the English interface shows them, with the Italian label in parentheses. - Comments explain the why and point to the source:
SPECIFICATION §7.3,prove §49(notes/connection-tests.md),notes/component.md. - Rust:
anyhowfor errors, withcontextsaying what was being done; nounwrapwhere a different phone might answer something else. - Java: one class per piece, reflection only in
Nascosteand in the pieces that use it, errors caught in the thread that produces them. - Third-party code: never copied. scrcpy and AOSP are read as documentation.
15.2 · Before every commit
Every commit leaves the project built, tested and documented.
$ cargo build && cargo test && cargo clippy --all-targetsNo clippy warnings. If you changed the component, the rebuilt jar goes into the same commit. If you changed a behavior, update the chapter that describes it in docs/sources/technical/ (and, if the user sees it, in docs/sources/user/) and regenerate the manuals (python3 docs/sources/build.py); if you added a source file, give it a row in the map (ch17_map.py, ch. 17 “Appendix B — File map”): cargo test requires it.
15.3 · Personal data
The repository is public: nothing that identifies people, phones or networks.
R5CT0000000); the files produced by the tests (phonestra-prova.*) are ignored by git.15.4 · Recording decisions
The project's why lives in notes/, one file per topic. Decisions are written there, together with their why.
| File | What goes in it |
|---|---|
notes/next-session.md | Where to pick up again: read at the start of every work session |
notes/user-decisions.md | The decisions and their why, including the rejected alternatives |
notes/issue-log.md | Problem → cause → solution → status |
notes/connection-tests.md | The measurements on the phone, in numbered sections (§) |
notes/component.md, adb.md, api-android.md, study/ | Design and study of the component and of the transport |
notes/interface.md | User interface rules |
Before proposing an alternative, check that it has not already been rejected (for example Flatpak, icons in the system menu, dark themes as a style, several phones active at the same time).
Appendix A — Data structures and messages
16.1 · Index of data structures
This appendix collects the central data structures of the PC program, with the file that defines them and the section that covers them in depth. They are the contracts that run through Phonestra; the Java classes of the component are listed in ch. 17 “Appendix B — File map”.
| Structure | Defined in | Role | Section |
|---|---|---|---|
| Connection and data | |||
Collegamento | collegamento.rs | The active phone: state, Adb, component, “in hand” panel, notifications | 3.2 “Life of a connection” |
Stato | collegamento.rs | Cerco, Collegato, Bloccato, Perso, Chiuso | 3.2 “Life of a connection” |
Telefoni, Telefono | configurazione.rs | The configured phones, in telefoni.toml | 3.6 “Configuration and data on the PC” |
Preferenze | configurazione.rs | The user's choices, in preferenze.toml | 10.4 “Preferences” |
Notifica, Info | notifiche.rs | Notifications, battery and network read from dumpsys | 10.6 “Notifications and alerts” |
App | app.rs | A launcher app: package, activity, name, icon | 10.2 “The drawer” |
| ADB client | |||
Adb | adb/mod.rs | The connection: can be cloned, opens channels, runs short commands | 4.4 “Channels and flow control” |
Canale, Chiusore | adb/mod.rs | A channel to a service; closing it from another task | 4.4 “Channels and flow control” |
Trasporto | adb/mod.rs | delayed_ack, max_payload, window | 4.4 “Channels and flow control” |
Messaggio | adb/messaggio.rs | An ADB message: 24-byte header and data | 4.2 “ADB messages” |
ShellV2 | adb/shell.rs | A process on the shell,v2 channel | 4.5 “The shell,v2 service” |
Voce | adb/sync.rs | An entry in a phone folder: name, folder or file, size, modification date | 4.6 “Copying files: sync:” |
| Component, PC side | |||
Componente | componente.rs | The started service: command channel, requests, shutdown | 5.9 “PC side: Componente and Condiviso” |
Condiviso | componente.rs | The component held in a task and cloneable: one service, many windows | 5.9 “PC side: Componente and Condiviso” |
Mittente, Apritore | componente.rs | Messages without a reply queued on the command channel; opening of the audio and video:<id> channels | 5.9 “PC side: Componente and Condiviso” |
Pronto, Ciao | componente.rs | The service's ready line; its CIAO | 5.2 “Starting the service” |
Messaggio | componente.rs | A command-channel message: type, flags, id, content | 5.4 “The command channel” |
SessioneNostra, ComandiVideo | video_nostro/mod.rs | A video session and its commands | 6.7 “PC side: from session to window” |
Evento | video_nostro/mod.rs | The events of a session: orientation, protected, moved, removed, end | 6.6 “App events” |
Opzioni, Pacchetto | video_nostro/flusso.rs | The display options; the packets of the video channel | 6.3 “The video:<id> channel” |
InputNostro | input_nostro.rs | Taps, scroll wheel, keys, text and clipboard encoded for the Mittente | 9.1 “Input and clipboard messages” |
Durate, Orari, Margine | audio_nostro.rs | Duration, timestamp and playback time of each audio packet | 8.3 “Playback on the PC” |
Vista | finestra.rs | Video, mouse and keyboard: the heart of an app window, also used for the screen in the drawer | 10.3 “App windows” |
16.2 · Index of messages
All the command-channel messages in one table, by number. The format of each is described in the section indicated.
| Type | Name | Direction | Section |
|---|---|---|---|
| Infrastructure (0x01–0x0f) and tests (0x10–0x1f) | |||
0x01 | CIAO | service → PC | 5.4 “The command channel” |
0x02 | BATTITO | both directions | 5.5 “Heartbeat and exit codes” |
0x03 | FINE | PC → service | 5.4 “The command channel” |
0x04 | ERRORE | service → PC, as a reply | 5.4 “The command channel” |
0x10 | PROVA_CUSTODE | PC → service | 5.4 “The command channel” |
| Video and panel (0x40–0x4f) | |||
0x40–0x45 | VIDEO_APRI, VIDEO_CHIUDI, VIDEO_AVVIA_APP, VIDEO_RIDIMENSIONA, VIDEO_CHIAVE, VIDEO_PANNELLO | PC → service | 6.2 “Video messages” |
0x46 | VIDEO_EVENTO | service → PC, unsolicited | 6.6 “App events” |
| Input and clipboard (0x50–0x5f) | |||
0x50–0x54 | TOCCHI, ROTELLINA, TASTO, TESTO, INDIETRO | PC → service, no reply | 9.1 “Input and clipboard messages” |
0x55–0x57 | APPUNTI_SCRIVI, APPUNTI_LEGGI, APPUNTI_ASCOLTA | PC → service | 9.4 “Clipboard” |
0x58 | APPUNTI_CAMBIATI | service → PC, unsolicited | 9.4 “Clipboard” |
0x5c, 0x5d | CONTEGGI, PROVA | PC → service (diagnostics and tests) | 9.1 “Input and clipboard messages” |
nessun_tipo_usato_da_due_moduli checks that every number is unique, in the right range and the same in Java and in Rust (14.1 “A new message on the command channel”).16.3 · Stream headers
Every byte stream in Phonestra starts with a fixed header. Here they are side by side.
| Stream | Header | Byte order | Section |
|---|---|---|---|
| ADB message | 24 bytes: command, arg0, arg1, length, checksum, command xor 0xffffffff | little-endian | 4.2 “ADB messages” |
shell,v2 packet | id u8 · lunghezza u32 | little-endian | 4.5 “The shell,v2 service” |
| Preamble of a service channel | segreto (16 byte) · lunghezza del tipo u8 · tipo ASCII | — | 5.3 “Channels and preamble” |
| Command channel | 8 bytes: tipo u8 · bandiere u8 · id u16 · lunghezza u32 | big-endian | 5.4 “The command channel” |
video:<id> channel | 12 bytes: pts u64 · lunghezza u32, or 0x80000000 and the new size | big-endian | 6.3 “The video:<id> channel” |
| Audio channel | 12 bytes: orario u64 · lunghezza u32 | big-endian | 8.2 “Audio packets” |
Appendix B — File map
17.1 · The project's files
Every source file with its line count, recounted each time the manual is generated. If a file is missing from the map, or the map lists a file that no longer exists, generation stops and cargo test fails.
| File | Lines | Role |
|---|---|---|
| User interface | ||
src/avvisi.rs | 93 | Phone notifications as system notifications (D-Bus), app icons |
src/bin/phonestra.rs | 171 | main: one process for all windows, connection, drawer, PC clipboard, Ctrl+C and SIGTERM, shutdown and restart to switch phones |
src/cassetto.rs | 2,162 | The drawer: phone pill, Apps, Notifications and Preferences pages, phones, tools, phone drawn with the mirror, transfers |
src/finestra.rs | 1,637 | An app's window: view (video, mouse, keyboard, zoom), sessions, panel, recording, screenshot |
src/foto.rs | 36 | PHONESTRA_FOTO: window images for user interface tests |
src/prepara.rs | 557 | “Add a phone” (Aggiungi un telefono) without a cable: settings list, checkmarks from mDNS, 6-digit code |
src/procedura.rs | 734 | The fallback cable procedure, by brand family |
src/ricevi.rs | 1,123 | “Receive files…” (Ricevi file…): places, Recents, listing and thumbnails of the phone's folders, file selection |
| Connection and data | ||
src/app.rs | 177 | The embedded jar (AIUTO), the helper, app list with the fine row, wallpaper, thumbnails, encoders |
src/azioni.rs | 138 | Installing, uninstalling, sending files |
src/collegamento.rs | 816 | The active phone: discovery, connection, connection guardian, component, 3-second round, panel “in hand”, calls, shutdown |
src/configurazione.rs | 354 | XDG folders, ADB key, telefoni.toml, preferenze.toml |
src/lib.rs | 60 | The library modules, esecutore(), diagnostics |
src/notifiche.rs | 177 | Reading notifications, battery and network from dumpsys |
src/rete.rs | 191 | Hand-written mDNS: phones with Wireless debugging, the code screen |
src/telefono.rs | 144 | Cable or Wi-Fi connection with adb_client, for the fallback procedure and the first connection |
src/usb.rs | 149 | Cable-connected phones read from sysfs, without permissions |
| ADB client | ||
src/adb/abbina.rs | 317 | Pairing with the code: TLS, SPAKE2, HKDF, AES-GCM |
src/adb/flusso.rs | 210 | Pure parts of flow control: banner, OPEN, OKAY, delayed ack balance |
src/adb/messaggio.rs | 88 | ADB messages: 24-byte header, reading and writing |
src/adb/mod.rs | 479 | Adb, Canale, Trasporto: Wi-Fi connection, dispatching, flow control |
src/adb/shell.rs | 125 | The shell,v2 service: input, output, errors, exit code |
src/adb/sync.rs | 202 | The sync: protocol: copying files to the phone, listing its folders, receiving files |
src/adb/tls.rs | 70 | Wireless debugging TLS: client certificate from Phonestra's key |
| Component, PC side | ||
src/appunti.rs | 61 | Copies made on the phone → PC clipboard, with limits and bounce-backs |
src/audio_nostro.rs | 880 | Audio channel, packets, timestamps, margin, GStreamer playback, copies for recording |
src/componente.rs | 1,290 | Componente and Condiviso: startup, preamble, CIAO, heartbeat, dispatching, leftovers |
src/lingua.rs | 161 | Interface language, Italian or English: t!, the English tables of data/en/ |
src/input_nostro.rs | 430 | InputNostro: encoding of touches, scroll wheel, keys, text, clipboard |
src/video_nostro/flusso.rs | 87 | Screen options and reading of video packets |
src/video_nostro/mod.rs | 375 | SessioneNostra, ComandiVideo, events, panel |
| Component on the phone | ||
android/helper/src/phonestra/Aiuto.java | 193 | Jar entry point: short helper commands and the service |
android/helper/src/phonestra/Appunti.java | 173 | Direct IClipboard: reading, writing, sensitive items, listener |
android/helper/src/phonestra/Audio.java | 670 | Audio capture, reading and encoding; measurement tool |
android/helper/src/phonestra/Autotest.java | 176 | Startup check of the hidden APIs, without using them |
android/helper/src/phonestra/CanaleAudio.java | 311 | The audio channel: loopback capture, AAC, four threads |
android/helper/src/phonestra/Codifica.java | 243 | Hardware MediaCodec from a Surface, keyframe on demand |
android/helper/src/phonestra/Contesto.java | 89 | Android context for app_process, package com.android.shell |
android/helper/src/phonestra/Custode.java | 164 | The service guardian: sh script, list of actions |
android/helper/src/phonestra/EventiApp.java | 275 | TaskStackListener: orientation, protected screen, tasks moved or closed |
android/helper/src/phonestra/Input.java | 738 | Input messages, queue, injection, fingers, scaling |
android/helper/src/phonestra/Miniature.java | 154 | Photo and video thumbnails for “Receive files…” (downsampled BitmapFactory, EXIF, video frame) |
android/helper/src/phonestra/Nascoste.java | 220 | Adapters for the hidden APIs, via reflection |
android/helper/src/phonestra/Pannello.java | 241 | Physical panel on or off, refresh rate at 60 Hz before turning off, restore via the guardian |
android/helper/src/phonestra/Protetta.java | 121 | Protected screen with captureDisplay and containsSecureLayers |
android/helper/src/phonestra/Protocollo.java | 128 | Message format of the command channel and the preamble |
android/helper/src/phonestra/Pulizia.java | 79 | Teardown in reverse order at the end of a test |
android/helper/src/phonestra/Servizio.java | 406 | The service: secret, socket, channels, heartbeat, exit codes, TIPI |
android/helper/src/phonestra/SessioneVideo.java | 531 | Virtual display or mirror, encoder, resizing, forced redraw |
android/helper/src/phonestra/Sistema.java | 437 | Shared pieces: display manager for virtual displays, app launching, togliTask, internal commands, images |
android/helper/src/phonestra/Video.java | 231 | Video messages, session registry, video:<id> channel |
| Tests and measurements (PC) | ||
src/adb/misura.rs | 368 | phonestra-prova throughput: transport speed and latency |
src/adb/prove.rs | 165 | The ADB client against a fake in-memory adbd |
src/bin/prova/audio_componente.rs | 301 | phonestra-prova audio-componente |
src/bin/prova.rs | 793 | phonestra-prova: the command-line test commands |
src/misura_audio.rs | 253 | Analysis of the studio audio: measurement lines, levels, WAV |
src/prova_input.rs | 545 | phonestra-prova input-componente: clipboard, touches, text |
src/video_nostro/prova.rs | 402 | phonestra-prova video-componente |
tests/lingua.rs | 115 | Every t! text has its English translation, with the same placeholders |
tests/manual.rs | 26 | This manual kept in step with the sources (runs build.py --controlla) |
| Tests and measurements (phone) | ||
android/helper/src/phonestra/Codificatori.java | 129 | List of the phone's audio and video encoders |
android/helper/src/phonestra/InputProva.java | 300 | Input test commands: test screen, signature, clipboard |
android/helper/src/phonestra/VideoProva.java | 1,049 | Measurement tool for the studio video |
| Build and tools | ||
packaging/AppRun | 57 | AppImage launcher: GTK and GStreamer variables, fallback libraries |
packaging/Containerfile | 70 | Ubuntu 22.04 container with GTK 4.14, libadwaita 1.5, gst-plugin-gtk4 |
packaging/build.sh | 18 | Builds a library from a tarball with meson, inside the container, and keeps its license files |
packaging/test-distributions.sh | 41 | The AppImage on Ubuntu, Debian, Fedora and Arch in containers |
packaging/collect.sh | 226 | Gathers the executable and libraries into the AppDir and creates the AppImage |
packaging/rust-licenses.py | 129 | License texts of the Rust crates built into an executable, from cargo tree and the crates' sources |
android/helper/build.sh | 20 | Builds the component: javac, D8, phonestra-helper.jar |
docs/sources/build.py | 633 | Generates the two manuals: functions for text, tables and SVG figures, numbering, checks |
docs/sources/technical/ch01_introduction.py | 207 | Technical Manual, chapter 1: Introduction and concepts |
docs/sources/technical/ch02_architecture.py | 84 | Technical Manual, chapter 2: Overall architecture |
docs/sources/technical/ch03_startup.py | 140 | Technical Manual, chapter 3: Startup and life cycle |
docs/sources/technical/ch04_adb.py | 152 | Technical Manual, chapter 4: The ADB client |
docs/sources/technical/ch05_component.py | 246 | Technical Manual, chapter 5: The on-phone component |
docs/sources/technical/ch06_video.py | 140 | Technical Manual, chapter 6: Video |
docs/sources/technical/ch07_panel.py | 132 | Technical Manual, chapter 7: The phone's panel |
docs/sources/technical/ch08_audio.py | 109 | Technical Manual, chapter 8: Audio |
docs/sources/technical/ch09_input.py | 106 | Technical Manual, chapter 9: Input and clipboard |
docs/sources/technical/ch10_interface.py | 166 | Technical Manual, chapter 10: The user interface |
docs/sources/technical/ch11_files.py | 57 | Technical Manual, chapter 11: Apps and files |
docs/sources/technical/ch12_testing.py | 114 | Technical Manual, chapter 12: Testing and diagnostics |
docs/sources/technical/ch13_build.py | 154 | Technical Manual, chapter 13: Build and release |
docs/sources/technical/ch14_extending.py | 50 | Technical Manual, chapter 14: Extending Phonestra |
docs/sources/technical/ch15_conventions.py | 50 | Technical Manual, chapter 15: Conventions |
docs/sources/technical/ch16_structures.py | 100 | Technical Manual, chapter 16: Appendix A — Data structures and messages |
docs/sources/technical/ch17_map.py | 126 | Technical Manual, chapter 17: Appendix B — File map |
docs/sources/technical/ch18_problems.py | 26 | Technical Manual, chapter 18: Appendix C — Known issues |
docs/sources/technical/ch19_glossary.py | 72 | Technical Manual, chapter 19: Glossary |
docs/sources/user/ch01_welcome.py | 68 | User Manual, chapter 1: Welcome |
docs/sources/user/ch02_installation.py | 74 | User Manual, chapter 2: Installation and first start |
docs/sources/user/ch03_connecting.py | 141 | User Manual, chapter 3: Connecting the phone |
docs/sources/user/ch04_drawer.py | 166 | User Manual, chapter 4: The drawer at a glance |
docs/sources/user/ch05_first_steps.py | 52 | User Manual, chapter 5: First steps |
docs/sources/user/ch06_windows.py | 138 | User Manual, chapter 6: App windows |
docs/sources/user/ch07_mouse_keyboard.py | 77 | User Manual, chapter 7: Mouse, keyboard and clipboard |
docs/sources/user/ch08_audio.py | 47 | User Manual, chapter 8: Audio, calls and camera |
docs/sources/user/ch09_notifications.py | 57 | User Manual, chapter 9: Notifications |
docs/sources/user/ch10_apps_files.py | 123 | User Manual, chapter 10: Apps and files |
docs/sources/user/ch11_screenshot.py | 36 | User Manual, chapter 11: Screenshots and recording |
docs/sources/user/ch12_screen.py | 67 | User Manual, chapter 12: The phone's screen |
docs/sources/user/ch13_phones.py | 51 | User Manual, chapter 13: Multiple phones |
docs/sources/user/ch14_preferences.py | 42 | User Manual, chapter 14: Preferences |
docs/sources/user/ch15_privacy.py | 99 | User Manual, chapter 15: Phone, PC and privacy |
docs/sources/user/ch16_problems.py | 123 | User Manual, chapter 16: Troubleshooting |
docs/sources/user/ch17_glossary.py | 34 | User Manual, chapter 17: Glossary |
Appendix C — Known issues
18.1 · Open issues
Open as of version 1.0.0; the up-to-date status is in notes/issue-log.md.
| Issue | Status |
|---|---|
| Original volume saved as 15 instead of the user's value: on shutdown the phone may be left at maximum | accepted by the user: the volume is turned down by hand (Collegamento::volume_originale) |
The audio margin grows but does not shrink: Margine::scendi does not trigger because AAC packets are of almost constant size | to do silence marked by the phone |
With delayed ack announced, adbd rejects every channel | off to be investigated (notes/adb.md) |
| Service memory about 145 MB (ART startup cost) | to measure |
| Why the audio startup order matters | rule found through measurements; the Android mechanism is still to be understood |
| PC Wi-Fi drop, recording with AAC audio on different phones | to test |
| Real call after rc.7: panel on for the incoming call and off again after it ends | to test |
| “Receive files…” (Ricevi file…): SD card, cancellation, large folders, dark theme | to test |
System alerts: on the first read after the drawer opens, the notifications already present on the phone raise an alert, because the “already seen” set is fixed while the list is still empty (avvisa_nuove in cassetto.rs) | to verify probable bug, found by rereading the code |
| Locked phone: on Samsung phones, locking drops Wireless debugging | worked around the connection reopens on unlock and the apps go back to where they were |
Glossary
19.1 · Terms A–L
Definitions of the technical terms used in the manual. The code of Phonestra is written in Italian, so terms that are also Italian names in the code are listed under the Italian word, followed by the English term this manual uses. The cross-references point to the section with the details.
- adbd
- The phone's ADB daemon: it accepts connections from the PC and starts the services (
shell,sync:,localabstract:). See ch. 4 “The ADB client”. - Aiutante (helper)
- The component's jar when used for short commands (app list, wallpaper, thumbnails, measurements). See 5.1 “What the component is”.
- Annex B
- The H.264/H.265 stream format with the start code
00 00 00 01in front of every unit. See 6.3 “The video:<id> channel”. - app_process
- The Android program that starts Java code outside an app; the shell uses it too. See 5.1 “What the component is”.
- ART
- Android Runtime: the virtual machine that runs the dex.
- AudioPolicy
- Hidden API for routing audio; with loopback it sends the apps' sound to a recorder instead of the loudspeaker. See 8.1 “The recipe”.
- Battito (heartbeat)
- The
BATTITOmessage, sent every second in both directions: after 5 s of silence each side considers the other gone. See 5.5 “Heartbeat and exit codes”. - Canale (channel)
- A logical connection inside the ADB connection, to a service on the phone; all channels share the same connection. See 4.4 “Channels and flow control”.
- Collegamento (connection)
- The active phone and its ADB connection (
collegamento.rs), kept alive as long as Phonestra stays open. See 3.2 “Life of a connection”. - Componente (component)
- Everything Phonestra runs on the phone: the jar
phonestra-helper.jar. See ch. 5 “The on-phone component”. - Custode (guardian)
- A shell process that puts the phone back in order when whatever it is tied to ends. See 5.6 “The two guardians”.
- Delayed ack
- ADB extension with more data in flight per channel and acknowledgements that carry the number of bytes received. See 4.4 “Channels and flow control”.
- dex
- The format of compiled Java code for Android (
classes.dex), produced by D8. See 13.3 “Building the component”. - Drawer
- Phonestra's main window with the phone's apps (
cassetto.rs). See 10.2 “The drawer”. - Giro dei 3 s (3-second round)
- The periodic check of the connection: lock, calls and notifications in a single command. See 3.3 “The 3-second round”.
- In mano (in hand)
- Panel state: the user is using the phone with their hands, so the panel stays on. See 7.3 “The phone in hand”.
- Loopback
- Capture of the audio coming out of the apps, while the phone itself stays silent. See 8.1 “The recipe”.
19.2 · Terms M–Z
- mDNS
- DNS on the local network without a server: this is how the phone announces Wireless debugging. See 4.8 “Network discovery: mDNS”.
- Pannello (panel)
- The phone's physical screen, turned off while the apps are used from the PC. See 7.1 “Turning the panel off”.
- Preambolo (preamble)
- The first bytes of every channel of the service: the secret and the channel type. See 5.3 “Channels and preamble”.
- Schermo virtuale (virtual display)
- An extra display, created by the component, where the app of a window runs. See 6.1 “Sessions: virtual display and mirror”.
- Servizio (service)
- The component's long-running process, one per connection (
phonestra-servizio). See 5.2 “Starting the service”. - Sessione (session)
- A virtual display or the mirror, with its
video:<id>channel. See 6.1 “Sessions: virtual display and mirror”. - Specchio (mirror)
- The copy of the phone's main screen, drawn in the drawer. See 6.1 “Sessions: virtual display and mirror”.
- Surface
- The graphics buffer the virtual display draws into and the encoder reads from. See 6.4 “Encoder and keyframe”.
- uid 2000
- The ADB shell user: its permissions are the component's permissions. See 5.8 “Service security”.
- Wireless debugging (Debug wireless)
- ADB over Wi-Fi in Android 11+, with TLS and pairing by code. See 4.3 “Wi-Fi connection and TLS”.