Chapter 22: The Qt UI¶
When you launch the Android Emulator with a display, the window you see, the rounded phone bezel, the vertical toolbar with its rotate and screenshot buttons, the "Extended controls" panel where you fake a GPS fix or drain the battery, is all drawn by a Qt 6 application that lives in external/qemu/android/android-ui/. That Qt application is not the emulator core. The core (QEMU, the virtual CPU, the virtual input devices) runs on a separate thread, and the two halves communicate through a deliberately narrow set of interfaces: a C skin_winsys_* API in one direction, a queue of SkinEvent structs and a table of agent function pointers in the other.
This chapter follows a single mouse click from the moment Qt delivers a QMouseEvent to EmulatorQtWindow all the way down to kbd_mouse_event_absolute() inside QEMU, and the screen frame back up from the host GPU to the pixels you see. Along the way we look at how the skin file describes the bezel and buttons, how the toolbar and extended-controls panels reuse the very same event queue that real input uses, and how the UI calls into the core through the typed agent interfaces collected in UiEmuAgent.
22.1 Two Threads, One Window¶
The single most important fact about the Qt UI is that it does not run on the same thread as the emulator core. Qt owns the process's main thread and runs QApplication::exec(); the QEMU machine runs in a worker thread spawned by the UI.
skin_winsys_spawn_thread() is the handoff. When the program decides to bring up the windowed UI, it asks the winsys layer to spawn the core's entry function on a new thread, then enters the Qt event loop on the original thread.
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/winsys-qt.cpp
extern void skin_winsys_spawn_thread(bool no_window,
StartFunction f,
int argc,
char** argv) {
...
EmulatorQtWindow* window = EmulatorQtWindow::getInstance();
...
window->startThread(f, argc, argv);
}
EmulatorQtWindow::startThread() constructs a MainLoopThread, a tiny QThread subclass whose entire run() body is a call to the supplied start function (external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/emulator-qt-window.h:80). That start function is QEMU's main loop. From that point the process has two long-lived loops: Qt's g->app->exec() on the main thread (winsys-qt.cpp:239) and QEMU's machine loop on the MainLoopThread.
22.1.1 Why the split exists¶
Qt insists that all widget manipulation happen on the thread that created QApplication. QEMU, meanwhile, wants to own its own loop and block in its own select/poll. Putting them on one thread would force one to drive the other's event pump, which neither library tolerates well. Splitting them keeps each loop idiomatic, at the cost of needing a thread-safe channel between them, which is what the rest of this chapter is about.
22.1.2 Crossing back to the UI thread¶
The core frequently needs the UI to do something, resize the window, repaint, show an error dialog, and those operations must run on the Qt thread. The bridge is skin_winsys_run_ui_update(), which marshals a function pointer onto the Qt thread and optionally blocks until it finishes:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/winsys-qt.cpp
void skin_winsys_run_ui_update(SkinGenericFunction f, void* data, bool wait) {
EmulatorQtWindow* const window = EmulatorQtWindow::getInstance();
...
if (wait) {
QSemaphore semaphore;
window->runOnUiThread([f, data]() { ...; f(data); }, &semaphore);
semaphore.acquire();
} else {
window->runOnUiThread([f, data]() { ...; f(data); }, nullptr);
}
}
runOnUiThread is a Qt signal connected to slot_runOnUiThread through an ordinary (non-blocking) connection (emulator-qt-window.cpp:750). Because the signal is emitted from the QEMU thread but the slot executes on the Qt thread, Qt queues the call. The optional QSemaphore is how the caller blocks: the slot releases the semaphore when the lambda returns (emulator-qt-window.cpp:3389), so the QEMU thread can acquire() and know the UI work is done. The long comment in emulator-qt-window.h (lines 156 to 172) documents this pattern: every cross-thread signal in that header carries an optional QSemaphore* for exactly this reason.
UI-thread handoff for a core-initiated update
sequenceDiagram
participant Core as QEMU thread
participant Sig as runOnUiThread signal
participant Qt as Qt main thread
Core->>Sig: emit runOnUiThread(f, semaphore)
Note over Sig: Qt queues the call
Sig->>Qt: slot_runOnUiThread(f, semaphore)
Qt->>Qt: f() runs on UI thread
Qt->>Core: semaphore.release()
Core->>Core: semaphore.acquire() returns
22.2 The Component Map¶
Before tracing data flow, it helps to name the widgets. The windowed UI is built from a handful of top-level QFrame and QWidget subclasses, all under external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/.
The major UI objects are:
EmulatorQtWindow(emulator-qt-window.h:113): the device window itself. It is aQFramethat paints the skin bezel and the guest screen, and it receives every raw mouse, key, touch, pen, and wheel event.EmulatorContainer(emulator-container.h:40): aQScrollAreathat wrapsEmulatorQtWindow, providing scroll bars for zoom mode, the window frame, and overlay surfaces (modal overlay, message center).ToolWindow(tool-window.h:61): the vertical toolbar docked beside the device. Its buttons map toQtUICommandvalues.ExtendedWindow(extended-window.h:50): the tabbed "Extended controls" panel with one pane per emulated subsystem (location, battery, cellular, ...).VirtualSceneControlWindowandTouchpadWindow: optional auxiliary windows for the virtual scene camera and trackpad input.
Top-level Qt widgets and their ownership
flowchart TD
Container["EmulatorContainer<br/>(QScrollArea)"]
Window["EmulatorQtWindow<br/>(device window)"]
Tool["ToolWindow<br/>(toolbar)"]
Ext["ExtendedWindow<br/>(extended controls)"]
VS["VirtualSceneControlWindow"]
TP["TouchpadWindow"]
Container --> Window
Window --> Tool
Tool --> Ext
Tool --> VS
Tool --> TP
EmulatorQtWindow holds its EmulatorContainer and EmulatorOverlay by value as its last members (emulator-qt-window.h:688), and it owns the ToolWindow pointer (mToolWindow). The ToolWindow in turn lazily constructs the ExtendedWindow, virtual scene window, and touchpad window through MemberOnDemandT holders (tool-window.h:212), so the heavyweight extended panel is not built until something needs it.
22.3 The Skin: Drawing the Device¶
A "skin" is the artwork and geometry that makes the window look like a particular phone: the bezel image, where the screen sits inside it, and which on-screen hardware buttons exist. The skin engine is older C code that lives in the window module (external/qemu/android/android-ui/modules/aemu-ui-window/src/android/skin/), not in the Qt module. It is deliberately toolkit-agnostic.
22.3.1 The skin file model¶
A parsed skin is a SkinFile containing parts and layouts (external/qemu/android/android-ui/modules/aemu-ui-common/include/android/skin/file.h:119). Each SkinPart carries a background image, a display rectangle, and a linked list of buttons:
// Source: external/qemu/android/android-ui/modules/aemu-ui-common/include/android/skin/file.h
typedef struct SkinPart {
struct SkinPart* next;
const char* name;
SkinBackground background[1];
SkinDisplay display[1];
SkinButton* buttons;
SkinRect rect; /* bounding box of all parts */
} SkinPart;
A SkinButton couples an image and a rectangle to a keycode (file.h:49). When you click inside a button's rectangle, the skin engine synthesizes the button's keycode as a key event, which is how an on-bezel "home" button works without any Android involvement. A SkinLayout is a named arrangement (portrait, landscape, folded, ...) that positions parts and records the framebuffer rotation (file.h:85).
22.3.2 Layout selection and the SkinUI object¶
skin_ui_create() selects the initial layout by name, then builds the keyboard, generic-event source, and window from it:
// Source: external/qemu/android/android-ui/modules/aemu-ui-window/src/android/skin/ui.c
ui->layout = skin_file_select_layout(layout_file->layouts, initial_orientation);
...
ui->keyboard = skin_keyboard_create(
ui->ui_params.keyboard_charmap, ui->layout->dpad_rotation,
ui_funcs->keyboard_flush, false);
...
ui->window = skin_window_create(
ui->layout, ui->ui_params.window_x, ui->ui_params.window_y,
ui->ui_params.enable_scale, use_emugl_subwindow,
ui->ui_funcs->window_funcs);
The SkinUI struct (ui.c:41) is the heart of the engine: it owns the layout, the keyboard translation table, the SkinWindow, an optional trackball, and the onion-skin overlay used for screen masks.
22.3.3 Painting the bezel versus the guest screen¶
The actual pixels are drawn in two passes inside EmulatorQtWindow::paintEvent. The skin bezel comes from a SkinSurface backing bitmap; the guest screen is a separate pixmap:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/emulator-qt-window.cpp
if (mBackingSurface) {
...
mScaledBackingImage = QPixmap::fromImage(mBackingSurface->bitmap->get().scaled(
r.size() * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation));
...
painter.drawPixmap(r, mScaledBackingImage);
}
if (!mGuestScreenPixmap.isNull()) {
auto r = contentsRect();
painter.drawPixmap(r, mGuestScreenPixmap);
}
A SkinSurface is a thin struct over a SkinSurfaceBitmap (emulator-qt-window.h:692 and :731) that wraps a QImage and supports lazy rotation and alpha blending. The bezel is uploaded once via the cross-thread showWindow signal (slot_showWindow, emulator-qt-window.cpp:2488), which stashes the surface in mBackingSurface. After that it only changes when the layout rotates or folds.
The guest screen, on the other hand, normally does not go through mGuestScreenPixmap at all. When the host GPU is used, the guest frames are composited by an OpenGL sub-window placed directly over the Qt window, which we cover in Section 22.7. The mGuestScreenPixmap path is used by the shared-memory/streamed renderer (SharedMemoryRenderer, emulator-qt-window.cpp:1779), where each frame arrives as a QImage over the frameReady signal.
22.4 The SkinEvent Queue: One Channel for All Input¶
Every piece of input, real or synthetic, becomes a SkinEvent and lands in a single FIFO queue on EmulatorQtWindow. This is the central design idea of the input path. A SkinEvent is a tagged union (external/qemu/android/android-ui/modules/aemu-ui-common/include/android/skin/event.h:200) whose type field is one of the SkinEventType values, kEventKeyDown, kEventMouseMotion, kEventTouchBegin, kEventRotaryInput, and so on (event.h:24), and whose u member holds the matching per-type payload struct.
22.4.1 Producing events on the Qt thread¶
Qt delivers low-level events to the overridden handlers on EmulatorQtWindow: mousePressEvent, keyPressEvent, tabletEvent, wheelEvent (emulator-qt-window.h:138 to :147). Each translates the Qt event into a SkinEvent and pushes it. For a mouse press, handleMouseEvent fills the union and enqueues:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/emulator-qt-window.cpp
SkinEvent skin_event = createSkinEvent(type);
skin_event.u.mouse.button = button;
skin_event.u.mouse.x = pos.x();
skin_event.u.mouse.y = pos.y();
skin_event.u.mouse.x_global = gPos.x();
skin_event.u.mouse.y_global = gPos.y();
skin_event.u.mouse.xrel = pos.x() - mPrevMousePosition.x();
skin_event.u.mouse.yrel = pos.y() - mPrevMousePosition.y();
...
queueSkinEvent(std::move(skin_event));
queueSkinEvent locks mSkinEventQueueMtx, pushes onto mSkinEventQueue, and, crucially, only if this is the first event in the queue, pokes the core awake:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/emulator-qt-window.cpp
const auto uiAgent = mToolWindow->getUiEmuAgent();
if (firstEvent && uiAgent && uiAgent->userEvents &&
uiAgent->userEvents->onNewUserEvent) {
uiAgent->userEvents->onNewUserEvent();
}
The comment notes the optimization: the core drains the queue completely once woken, so re-notifying for every event is wasted work (emulator-qt-window.cpp:2331). queueSkinEvent also coalesces high-frequency events: a new kEventScreenChanged, kEventScrollBarChanged, or kEventZoomedWindowResized replaces an existing one of the same type rather than piling up (emulator-qt-window.cpp:2308).
22.4.2 Consuming events on the QEMU thread¶
When the core wakes, it calls the C function skin_event_poll(), which simply forwards to the window's pollEvent:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/event-qt.cpp
extern bool skin_event_poll(SkinEvent* event) {
EmulatorQtWindow *window = EmulatorQtWindow::getInstance();
if (!window) return false;
bool retval;
window->pollEvent(event, &retval);
return retval;
}
pollEvent pops the front of the queue under the same mutex (emulator-qt-window.cpp:2281). The core loops on skin_event_poll inside skin_ui_process_events, dispatching each event by type (external/qemu/android/android-ui/modules/aemu-ui-window/src/android/skin/ui.c:272). This is the thread boundary: producers run on the Qt thread, the consumer runs on the QEMU thread, and the std::mutex plus the onNewUserEvent wakeup are the only coordination.
The unified input channel
flowchart LR
subgraph QT["Qt main thread"]
QEv["QMouseEvent /<br/>QKeyEvent /<br/>QTouchEvent"]
Handle["handleMouseEvent /<br/>handleKeyEvent /<br/>handleTouchPoints"]
Queue["mSkinEventQueue<br/>(deque + mutex)"]
end
subgraph CORE["QEMU thread"]
Poll["skin_event_poll"]
Proc["skin_ui_process_events"]
end
QEv --> Handle --> Queue
Queue -->|"onNewUserEvent wakes core"| Poll
Poll --> Proc
22.5 From SkinEvent to Guest Input¶
skin_ui_process_events is a large switch on ev.type. Each case routes the event to the right consumer inside the skin engine (ui.c:287).
The event router branches three ways:
- Key and text events go to
skin_keyboard_process_event, which applies the charmap and emits guest keycodes (ui.c:297,:303,:316). - Generic events (lid switch, etc.) go to
skin_generic_event_process_event(ui.c:310). - Mouse, wheel, rotary, and pointer-tracking events go to
skin_window_process_event; touch events go toskin_window_process_touch_event; pen events toskin_window_process_pen_event(ui.c:319to:391).
22.5.1 The window callbacks¶
The skin window does not know how to deliver input to the guest. It holds a const SkinWindowFuncs* table (external/qemu/android/android-ui/modules/aemu-ui-window/src/android/skin/window.c:1054) and calls through it. For a mouse event it calls win_funcs->mouse_event(...) (window.c:1156); for an on-bezel button it calls win_funcs->key_event(button->keycode, ...) (window.c:2385); for a rotary encoder it calls win_funcs->rotary_input_event(...) (window.c:2789).
That table is populated in emulator_window_setup (external/qemu/android/android-ui/modules/aemu-ui-window/src/android/emulator-window.c:271), and each entry is a thin wrapper around the user event agent:
// Source: external/qemu/android/android-ui/modules/aemu-ui-window/src/android/emulator-window.c
static void emulator_window_window_key_event(unsigned keycode, int down) {
user_event_agent->sendKey(keycode, down);
}
static void emulator_window_window_mouse_event(unsigned x, unsigned y,
unsigned state, int displayId,
bool absoluteCoordinates) {
enum MouseEventMode mouse_event_mode =
absoluteCoordinates ? MOUSE_EVENT_MODE_ABS : MOUSE_EVENT_MODE_REL;
user_event_agent->sendMouseEvent(x, y, 0, state, displayId,
mouse_event_mode);
}
22.5.2 The user event agent and QEMU¶
user_event_agent is a QAndroidUserEventAgent*, a struct of function pointers declared in external/qemu/android/emu/agents/include/android/emulation/control/user_event_agent.h:45. The windowed emulator's implementation lives in the QEMU glue layer:
// Source: external/qemu/android-qemu2-glue/qemu-user-event-agent-impl.c
static const QAndroidUserEventAgent sQAndroidUserEventAgent = {
.sendTouchEvents = user_event_touch,
.sendKey = user_event_key,
.sendKeyCode = user_event_keycode,
.sendKeyCodes = user_event_keycodes,
.sendMouseEvent = user_event_mouse,
...
};
sendKey ultimately constructs a QEMU InputEvent of kind INPUT_EVENT_KIND_KEY and enqueues it on the active console (qemu-user-event-agent-impl.c:32 to :53). sendMouseEvent calls kbd_mouse_event for relative motion or kbd_mouse_event_absolute for absolute, choosing based on the device driver mode and feature flags (qemu-user-event-agent-impl.c:112). Touch and pen events go through android_virtio_send_touch_as_mt / android_virtio_send_pen_as_mt into the virtio multi-touch device (qemu-user-event-agent-impl.c:103, :157). From there the events are exactly what the virtual input devices deliver to the guest kernel.
End-to-end input pipeline, click to guest
flowchart TD
A["EmulatorQtWindow::mousePressEvent"]
B["handleMouseEvent builds SkinEvent"]
C["mSkinEventQueue"]
D["skin_ui_process_events"]
E["skin_window_process_event"]
F["win_funcs.mouse_event"]
G["user_event_agent.sendMouseEvent"]
H["kbd_mouse_event_absolute (QEMU)"]
I["guest virtual input device"]
A --> B --> C --> D --> E --> F --> G --> H --> I
22.6 The Toolbar: ToolWindow and QtUICommand¶
The vertical strip of buttons (rotate, screenshot, volume, power, "...") is ToolWindow. Its buttons do not call agents directly; they resolve to a QtUICommand and route through handleUICommand.
22.6.1 Buttons carry a uiCommand property¶
In the .ui layout each button is given a Qt dynamic property named uiCommand. At construction ToolWindow walks its child buttons, reads that property, and parses it into a QtUICommand so it can attach the correct tooltip and shortcut:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/tool-window.cpp
for (auto button : childButtons) {
QVariant uiCommand = button->property("uiCommand");
if (uiCommand.isValid()) {
QtUICommand cmd;
if (parseQtUICommand(uiCommand.toString(), &cmd)) {
QVector<QKeySequence>* shortcuts = mShortcutKeyStore.reverseLookup(cmd);
...
}
}
...
}
QtUICommand is an enum class (external/qemu/android/android-ui/modules/aemu-ui-widgets/src/android/skin/qt/qt-ui-commands.h:17) with one value per toolbar action: SHOW_PANE_LOCATION, TAKE_SCREENSHOT, VOLUME_UP, POWER, HOME, and so on.
22.6.2 Two flavors of command¶
handleUICommand (tool-window.cpp:924) splits commands into two groups. Panel commands (SHOW_PANE_*) open or raise a pane of the extended window:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/tool-window.cpp
case QtUICommand::SHOW_PANE_LOCATION:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_LOCATION);
}
break;
Hardware-button commands instead synthesize a guest key. They do not call the agent directly; they re-inject a SkinEvent into the same queue that real keys use, via forwardKeyToEmulator:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/tool-window.cpp
void ToolWindow::forwardKeyToEmulator(uint32_t keycode, bool down) {
SkinEvent skin_event = createSkinEvent(down ? kEventKeyDown : kEventKeyUp);
skin_event.u.key.keycode = keycode;
skin_event.u.key.mod = 0;
mEmulatorWindow->queueSkinEvent(std::move(skin_event));
}
So clicking the toolbar's Home button (QtUICommand::HOME) maps to forwardKeyToEmulator(LINUX_KEY_HOME, down) (tool-window.cpp:1105), which enqueues a key SkinEvent indistinguishable from one produced by a physical keyboard. Volume, power, back, overview, and the foldable lid switch (forwardGenericEventToEmulator(EV_SW, SW_LID, ...), tool-window.cpp:2472) all reuse the queue this way. This is why the toolbar and a real key both end up at the same user_event_agent call.
22.6.3 Keyboard shortcuts¶
The toolbar also owns a ShortcutKeyStore<QtUICommand> seeded with defaults in the constructor, for example Ctrl+S TAKE_SCREENSHOT, Ctrl+P POWER, Ctrl+Backspace BACK (tool-window.cpp:280 to :313). handleQtKeyEvent (tool-window.cpp:1487) consults this store before the key is treated as device input, giving the virtual scene and touchpad windows first refusal, then matching a shortcut to its QtUICommand.
Toolbar command routing
flowchart TD
Btn["Toolbar button click /<br/>keyboard shortcut"]
Cmd["QtUICommand"]
H["ToolWindow::handleUICommand"]
Pane["showOrRaiseExtendedWindow(pane)"]
Key["forwardKeyToEmulator(keycode)"]
Q["queueSkinEvent"]
Btn --> Cmd --> H
H -->|"SHOW_PANE_*"| Pane
H -->|"HOME / BACK / POWER / VOLUME"| Key --> Q
22.7 Rendering the Guest Screen¶
The guest's framebuffer is almost never copied through Qt's painter when a GPU is in play. Instead the host GPU composites guest frames into an OpenGL sub-window that the windowing system stacks directly on top of the Qt device window.
22.7.1 The OpenGL sub-window¶
When the skin window needs to (re)place the GL sub-window, it builds a gles_show_data describing the position, size, framebuffer dimensions, rotation, and device-pixel ratio, then calls through the window functions:
// Source: external/qemu/android/android-ui/modules/aemu-ui-window/src/android/skin/window.c
static void skin_window_run_opengles_show(void* p) {
gles_show_data* data = p;
data->window->win_funcs->opengles_show(
skin_winsys_get_window_handle(), data->wx, data->wy, data->ww,
data->wh, data->fbw, data->fbh, data->dpr, data->rot,
data->deleteExisting);
AFREE(data);
}
The wrapper emulator_window_opengles_show_window (emulator-window.c:196) forwards to android_showOpenglesWindow, passing the native window handle obtained from skin_winsys_get_window_handle(). The renderer draws guest frames into that sub-window; Qt only paints the bezel around it. When the user scrolls a zoomed window, opengles_setTranslation shifts the sub-window (window.c:1927), and opengles_redraw requests a fresh frame (window.c:1628).
22.7.2 The streamed/shared-memory path¶
When there is no host GL sub-window, for example a headless host or the gRPC-driven fishtank shell, frames arrive as images. EmulatorQtWindow::initializeStreamer constructs a SharedStreamEmulator, the gRPC stream manager that listens for new frames; for MMAP transport it pairs it with a SharedMemoryRenderer, and otherwise its frame callback decodes the streamed PNG/raw frames. Either way the window emits frameReady(QImage), which is connected to slot_updateGuestScreen (emulator-qt-window.cpp:1789). That slot updates mGuestScreenPixmap, and the next paintEvent blits it (Section 22.3.3). Multi-display secondary windows use MultiDisplayWidget, a GLWidget subclass that paints a guest texture per extra display (multi-display-widget.h:23).
Two rendering paths for guest frames
flowchart TD
subgraph GPU["Host GPU path"]
Sub["opengles_show sub-window"]
Comp["GPU composites guest frames"]
end
subgraph STREAM["Streamed path"]
Shm["SharedMemoryRenderer /<br/>SharedStreamEmulator"]
FR["frameReady(QImage) signal"]
Slot["slot_updateGuestScreen"]
Pix["mGuestScreenPixmap"]
Paint["paintEvent drawPixmap"]
end
Sub --> Comp
Shm --> FR --> Slot --> Pix --> Paint
22.8 The Extended Window and the Agents¶
"Extended controls" is a tabbed panel, one tab per emulated subsystem. The pane order is fixed by the ExtendedWindowPane enum, which must match the tab order baked into extended.ui (external/qemu/android/emu/host-common/include/host-common/qt_ui_defs.h:36): PANE_IDX_LOCATION, PANE_IDX_MULTIDISPLAY, PANE_IDX_CELLULAR, PANE_IDX_BATTERY, and so on.
22.8.1 One agent per subsystem¶
The UI talks to the core through UiEmuAgent, a struct of typed agent pointers, one per subsystem (external/qemu/android/emu/agents/include/android/ui-emu-agent.h:17):
// Source: external/qemu/android/emu/agents/include/android/ui-emu-agent.h
typedef struct UiEmuAgent {
const struct QAndroidAutomationAgent* automation;
const struct QAndroidBatteryAgent* battery;
const struct QAndroidCellularAgent* cellular;
...
const struct QAndroidUserEventAgent* userEvents;
const struct QAndroidVirtualSceneAgent* virtualScene;
const struct QAndroidMultiDisplayAgent* multiDisplay;
const struct SettingsAgent* settings;
} UiEmuAgent;
This struct is handed to the UI once, through skin_winsys_set_ui_agent, which forwards it to ToolWindow::earlyInitialization (winsys-qt.cpp:944) and stores it in the static sUiEmuAgent (tool-window.h:252). Every pane and the toolbar reach the core only through these pointers; there is no other back channel.
22.8.2 A pane drives its agent¶
Take the battery pane. When the user drags the charge slider, the page hands a BatteryState to a BatteryController, whose legacy implementation calls straight through the QAndroidBatteryAgent:
// Source: external/qemu/android/android-ui/modules/aemu-ext-pages/battery/src/android/skin/qt/extended-pages/legacy-battery-controller.cpp
if (... && mAgent->setChargeLevel) {
...
mAgent->setChargeLevel(state.chargeLevel);
}
if (mCurrentState.charger != state.charger && mAgent->setCharger) {
...
mAgent->setCharger(state.charger);
}
The page picks its controller at init time: a grpc-battery-controller when the UI is running detached over gRPC, or the in-process legacy-battery-controller otherwise (battery-page.cpp:202). Either way the abstract BatteryController interface (battery-controller.h:16) hides whether the agent call is a local function pointer or a remote RPC, the pane code is identical.
The extended pages are split into per-subsystem CMake modules under external/qemu/android/android-ui/modules/aemu-ext-pages/ (battery, cellular, location, camera, finger, telephony, ...), each owning its .ui layout, its page class, and its controller. ExtendedPageFactory::construct (extended-page-factory.h:23) wires a pane into the tabbed ExtendedControls UI on demand.
How a control panel reaches the core
flowchart LR
Pane["BatteryPage (Qt widget)"]
Ctrl["BatteryController<br/>(legacy or gRPC)"]
Agent["QAndroidBatteryAgent<br/>(in UiEmuAgent)"]
Core["Battery model in core"]
Pane --> Ctrl --> Agent --> Core
22.9 Keyboard Translation and Mouse Grab¶
Two cross-cutting concerns deserve their own section: how Qt key codes become guest key codes, and how the window captures the host pointer.
22.9.1 Key code conversion¶
A QKeyEvent carries a Qt key constant; the guest wants a Linux/Android scan code. forwardKeyEventToEmulator converts it and tracks modifier state:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/emulator-qt-window.cpp
SkinEvent skin_event = createSkinEvent(type);
SkinEventKeyData& keyData = skin_event.u.key;
bool isModifier = false;
keyData.keycode = convertKeyCode(event.key(), isModifier);
...
if (isModifier) {
if (type == kEventKeyDown) mHeldModifiers.insert(keyData.keycode);
else if (type == kEventKeyUp) mHeldModifiers.erase(keyData.keycode);
}
keyData.mod = generateModData(event.modifiers());
queueSkinEvent(std::move(skin_event));
When the QtRawKeyboardInput feature is on, the window prefers the unmodified key so that, for example, Shift+2 reaches the guest as the physical key plus a modifier rather than as a pre-composed @ (emulator-qt-window.cpp:3103). The skin keyboard layer (skin_keyboard_process_event) then applies the AVD's charmap before the keycode is sent.
22.9.2 Mouse grab and the release shortcut¶
For pointer devices that need relative motion (the VirtioMouse feature), mousePressEvent captures the host pointer with grabMouse and posts a kEventMouseStartTracking event (emulator-qt-window.cpp:1455). While grabbed, the cursor is hidden and all motion is delivered to the guest. The user reclaims the host pointer with a release shortcut, Ctrl+Alt+7 on newer desktop AVDs, otherwise Ctrl+R (releaseMouseShortcutName, emulator-qt-window.cpp:1278). handleKeyEvent checks for that shortcut and, when matched, calls releaseMouse() and posts kEventMouseStopTracking (emulator-qt-window.cpp:3137). The first time you click into a grabbing window, a QMessageBox explains the capture (emulator-qt-window.cpp:1457).
22.9.3 Right click becomes Back¶
On devices without a virtual mouse or tablet, a right click is reinterpreted as the Android Back key rather than a pointer button:
// Source: external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/emulator-qt-window.cpp
if (button == kMouseButtonRight) {
const bool shouldTranslateMouseClickToTouch =
(!feature_is_enabled(kFeature_VirtioMouse) &&
!feature_is_enabled(kFeature_VirtioTablet));
if (shouldTranslateMouseClickToTouch) {
const bool down = (type == kEventMouseButtonDown);
SkinEvent skin_event = createSkinEvent(down ? kEventKeyDown : kEventKeyUp);
skin_event.u.key.keycode = LINUX_KEY_BACK;
...
queueSkinEvent(std::move(skin_event));
return;
}
}
This is another example of the queue being the single point through which all input flows, even a remapped right click becomes an ordinary key SkinEvent.
22.10 The winsys Interface: How the Core Steers the Window¶
The core never includes a Qt header. It manipulates the window only through the C skin_winsys_* API declared in external/qemu/android/android-ui/modules/aemu-ui-window/include/android/skin/winsys.h. That header is the formal contract between the toolkit-neutral core and whichever GUI backend is linked, Qt for the desktop, or the headless no_window backend.
The interface covers four kinds of operation:
- Geometry and monitor queries:
skin_winsys_get_monitor_rect,skin_winsys_get_device_pixel_ratio,skin_winsys_set_window_size(winsys.h:33to:55). - Lifecycle and the main loop:
skin_winsys_start,skin_winsys_spawn_thread,skin_winsys_enter_main_loop,skin_winsys_quit_request(winsys.h:109to:141). - UI-thread marshaling:
skin_winsys_run_ui_updateandskin_winsys_error_dialog(winsys.h:147,:150). - Agent and notification plumbing:
skin_winsys_set_ui_agent,skin_winsys_update_rotation,skin_winsys_show_virtual_scene_controls(winsys.h:121to:154).
The Qt implementation of every one of these is in winsys-qt.cpp. For instance, skin_winsys_enter_main_loop installs the native event filter and calls g->app->exec() for the windowed case, or blocks on a wake event / sigsuspend for the windowless case (winsys-qt.cpp:186). Because the contract is pure C, the same core links unchanged against the headless backend, which is exactly what makes server-side and CI runs possible.
The winsys contract between core and GUI backend
flowchart TD
Core["Emulator core<br/>(no Qt headers)"]
API["skin_winsys_* C API<br/>(winsys.h)"]
QtImpl["Qt backend<br/>(winsys-qt.cpp)"]
Headless["Headless backend<br/>(no_window)"]
Core --> API
API --> QtImpl
API --> Headless
22.11 Try It¶
The following commands exercise the pieces this chapter described. Run them from a shell where the emulator binary is on your PATH.
Inspect what the window does internally by turning on the verbose categories that the skin code logs under:
# "keys" logs every SkinEvent the core consumes; "surface" logs paint/blit.
emulator -avd <your_avd> -verbose -debug keys,surface,init
Watch the input pipeline end to end. With -debug keys on, click and type in the window; the >> MOUSE and >> ... KEY lines printed by qemu-user-event-agent-impl.c are the bottom of the pipeline from Section 22.5.
Open the extended controls and a specific pane directly from the toolbar shortcuts:
- Press
Ctrl+Shift+S(orF1for help) to open the Settings / Help pane, the sameSHOW_PANE_*commands wired intool-window.cpp. - Press
Ctrl+Sto take a screenshot andCtrl+Pto send a power key (on non-desktop AVDs).
Drive the UI without a window to confirm the winsys split:
# Headless: the same core runs, the Qt backend is replaced by the no-window backend.
emulator -avd <your_avd> -no-window -verbose
Find the skin file your AVD uses, then open its layout file to see the parts, buttons, and display rectangle that Section 22.3 parses:
# The skin directory is recorded in the AVD's config; layouts live alongside the bezel PNGs.
find "$ANDROID_SDK_ROOT/skins" -name layout | head
Summary¶
- The Qt UI and the QEMU core run on two threads in one process: Qt owns the main thread (
QApplication::exec), the core runs on aMainLoopThreadspawned throughskin_winsys_spawn_thread. - Core-to-UI calls cross threads via
skin_winsys_run_ui_update, which marshals a function onto the Qt thread through therunOnUiThreadsignal and an optionalQSemaphorefor blocking. - All input, real mouse/key/touch/pen, toolbar buttons, and keyboard shortcuts, converges on a single
SkinEventFIFO (mSkinEventQueue) guarded by a mutex;onNewUserEventwakes the core, which drains the queue withskin_event_poll. - The skin engine (
SkinFile,SkinLayout,SkinPart,SkinButton) is toolkit-neutral C in the window module; it dispatches events through aSkinWindowFuncstable whose entries forward to theQAndroidUserEventAgent, which finally calls QEMU input functions likekbd_mouse_event_absolute. EmulatorQtWindow::paintEventdraws only the bezel from aSkinSurface; the guest screen is normally composited by a host-GPU OpenGL sub-window stacked over the Qt window, with aSharedMemoryRenderer/ streamedQImagefallback.- The toolbar (
ToolWindow) resolves each button to aQtUICommand; hardware-button commands re-injectSkinEvents into the same queue, whileSHOW_PANE_*commands open panes of the lazily builtExtendedWindow. - Extended-control panes reach the core only through the typed agents collected in
UiEmuAgent, set once viaskin_winsys_set_ui_agent; a controller abstraction lets the same pane call either an in-process agent or a gRPC remote.
Key Source Files¶
| File | Purpose |
|---|---|
external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/emulator-qt-window.cpp |
Device window: Qt event handlers, the SkinEvent queue, painting |
external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/emulator-qt-window.h |
Cross-thread signal/slot contract and window state |
external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/tool-window.cpp |
Toolbar, QtUICommand routing, shortcuts |
external/qemu/android/android-ui/modules/aemu-ui-qt/src/android/skin/qt/winsys-qt.cpp |
Qt implementation of the skin_winsys_* C API and the main loop |
external/qemu/android/android-ui/modules/aemu-ui-window/src/android/skin/ui.c |
Toolkit-neutral event router (skin_ui_process_events) |
external/qemu/android/android-ui/modules/aemu-ui-window/src/android/skin/window.c |
Skin window, button hit-testing, GL sub-window placement |
external/qemu/android/android-ui/modules/aemu-ui-window/src/android/emulator-window.c |
SkinWindowFuncs table bridging the skin engine to the agents |
external/qemu/android/android-ui/modules/aemu-ui-common/include/android/skin/event.h |
SkinEvent union and SkinEventType enum |
external/qemu/android/emu/agents/include/android/ui-emu-agent.h |
UiEmuAgent aggregate of per-subsystem agents |
external/qemu/android-qemu2-glue/qemu-user-event-agent-impl.c |
QAndroidUserEventAgent implementation into QEMU input |