Skip to main content

android/emulator/
controller.rs

1//! The controller client: two HTTP/2 connections to one emulator, a deadline on
2//! every call, touches, raw screenshots and the scaled frame stream.
3
4use std::fmt;
5use std::future::Future;
6use std::time::{Duration, Instant};
7
8use tonic::metadata::{Ascii, MetadataValue};
9use tonic::service::Interceptor;
10use tonic::service::interceptor::InterceptedService;
11use tonic::transport::Channel;
12
13use super::endpoint::{Endpoint, Unavailable};
14use super::frame::{Frame, FrameError};
15use super::hardware::{Hardware, scaled_size};
16use super::proto;
17use super::proto::emulator_controller_client::EmulatorControllerClient;
18use super::proto::image_format::ImgFormat;
19
20/// Largest message the client accepts: a native RGBA8888 frame of a 1080x2400
21/// display is 10.4 MB, beyond tonic's default of 4 MB.
22pub const MAX_MESSAGE: usize = 64 << 20;
23
24/// Pressure of a finger on the glass; the touchscreen's axis maximum is 1024.
25pub const DOWN_PRESSURE: i32 = 512;
26
27/// Deadlines of the client.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Options {
30    /// Deadline of every unary call, and of opening the frame stream.
31    pub deadline: Duration,
32    /// Deadline of each of the two connections.
33    pub connect_deadline: Duration,
34}
35
36impl Default for Options {
37    fn default() -> Self {
38        Self {
39            deadline: Duration::from_secs(2),
40            connect_deadline: Duration::from_secs(2),
41        }
42    }
43}
44
45/// Why a call on a connected controller failed.
46#[derive(Debug)]
47pub enum Error {
48    /// The call did not finish within its deadline.
49    Deadline { call: &'static str, after: Duration },
50    /// The controller answered with an error status.
51    Status {
52        call: &'static str,
53        status: tonic::Status,
54    },
55    /// A frame's bytes do not match its dimensions. The stream stays usable.
56    Frame {
57        call: &'static str,
58        error: FrameError,
59    },
60    /// The emulator ended the frame stream.
61    StreamEnded,
62}
63
64impl fmt::Display for Error {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Error::Deadline { call, after } => write!(f, "{call}: no answer within {after:?}"),
68            Error::Status { call, status } => {
69                write!(f, "{call}: {:?}: {}", status.code(), status.message())
70            }
71            Error::Frame { call, error } => write!(f, "{call}: {error}"),
72            Error::StreamEnded => write!(f, "the emulator ended the frame stream"),
73        }
74    }
75}
76
77impl std::error::Error for Error {}
78
79/// One contact in a touch event.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct TouchPoint {
82    /// Tracks a contact across events; any non-negative value.
83    pub id: i32,
84    /// Display pixels from the left edge.
85    pub x: i32,
86    /// Display pixels from the top edge.
87    pub y: i32,
88    /// Non-zero while touching; zero lifts the contact.
89    pub pressure: i32,
90}
91
92impl TouchPoint {
93    /// Contact 0 pressed at (x, y).
94    pub fn down(x: i32, y: i32) -> Self {
95        Self {
96            id: 0,
97            x,
98            y,
99            pressure: DOWN_PRESSURE,
100        }
101    }
102
103    /// Contact 0 lifted at (x, y).
104    pub fn up(x: i32, y: i32) -> Self {
105        Self {
106            pressure: 0,
107            ..Self::down(x, y)
108        }
109    }
110}
111
112/// Adds `authorization: Bearer <token>` to every call when a token is known.
113#[derive(Clone)]
114struct Bearer(Option<MetadataValue<Ascii>>);
115
116impl Interceptor for Bearer {
117    fn call(
118        &mut self,
119        mut request: tonic::Request<()>,
120    ) -> Result<tonic::Request<()>, tonic::Status> {
121        if let Some(value) = &self.0 {
122            request
123                .metadata_mut()
124                .insert("authorization", value.clone());
125        }
126        Ok(request)
127    }
128}
129
130type Client = EmulatorControllerClient<InterceptedService<Channel, Bearer>>;
131
132/// Whether `status` reports a deadline: tonic's own `grpc-timeout` enforcement
133/// answers `Cancelled` with "Timeout expired", the server `DeadlineExceeded`.
134fn is_deadline(status: &tonic::Status) -> bool {
135    status.code() == tonic::Code::DeadlineExceeded
136        || (status.code() == tonic::Code::Cancelled
137            && status.message() == tonic::TimeoutExpired(()).to_string())
138}
139
140/// Awaits `response` for at most `deadline`.
141async fn within<T>(
142    call: &'static str,
143    deadline: Duration,
144    response: impl Future<Output = Result<tonic::Response<T>, tonic::Status>>,
145) -> Result<T, Error> {
146    match tokio::time::timeout(deadline, response).await {
147        Err(_) => Err(Error::Deadline {
148            call,
149            after: deadline,
150        }),
151        Ok(Err(status)) if is_deadline(&status) => Err(Error::Deadline {
152            call,
153            after: deadline,
154        }),
155        Ok(Err(status)) => Err(Error::Status { call, status }),
156        Ok(Ok(response)) => Ok(response.into_inner()),
157    }
158}
159
160/// A unary request that also tells the emulator its deadline (`grpc-timeout`).
161fn unary<T>(message: T, deadline: Duration) -> tonic::Request<T> {
162    let mut request = tonic::Request::new(message);
163    request.set_timeout(deadline);
164    request
165}
166
167/// An error and its sources on one line, since tonic's transport errors say
168/// little on their own; a cause that repeats the one before is left out.
169fn chain(error: &dyn std::error::Error) -> String {
170    let mut text = error.to_string();
171    let mut last = text.clone();
172    let mut source = error.source();
173    while let Some(cause) = source {
174        let message = cause.to_string();
175        if message != last {
176            text.push_str(": ");
177            text.push_str(&message);
178        }
179        last = message;
180        source = cause.source();
181    }
182    text
183}
184
185/// One HTTP/2 connection to `uri`, established within the connect deadline.
186async fn channel(uri: &str, options: &Options) -> Result<Channel, String> {
187    let endpoint = tonic::transport::Endpoint::from_shared(uri.to_owned())
188        .map_err(|e| format!("bad endpoint {uri}: {}", chain(&e)))?
189        .connect_timeout(options.connect_deadline)
190        .tcp_nodelay(true);
191    match tokio::time::timeout(options.connect_deadline, endpoint.connect()).await {
192        Err(_) => Err(format!(
193            "no connection to {uri} within {:?}",
194            options.connect_deadline
195        )),
196        Ok(Err(e)) => Err(format!("connecting to {uri}: {}", chain(&e))),
197        Ok(Ok(channel)) => Ok(channel),
198    }
199}
200
201/// A client on `channel` that accepts messages up to [`MAX_MESSAGE`].
202fn client(channel: Channel, bearer: Bearer) -> Client {
203    EmulatorControllerClient::with_interceptor(channel, bearer)
204        .max_decoding_message_size(MAX_MESSAGE)
205        .max_encoding_message_size(MAX_MESSAGE)
206}
207
208/// A frame from an `Image` message.
209fn frame_from(image: proto::Image, call: &'static str) -> Result<Frame, Error> {
210    let (width, height) = image
211        .format
212        .as_ref()
213        .map_or((0, 0), |format| (format.width, format.height));
214    Frame::new(width, height, image.image)
215        .map(|frame| frame.with_timing(image.seq, image.timestamp_us))
216        .map_err(|error| Error::Frame { call, error })
217}
218
219/// The request for RGB888 frames of `width` by `height`, 0 by 0 for the
220/// display size.
221fn rgb888(width: u32, height: u32) -> proto::ImageFormat {
222    proto::ImageFormat {
223        format: ImgFormat::Rgb888 as i32,
224        width,
225        height,
226        ..Default::default()
227    }
228}
229
230/// A connected emulator controller.
231///
232/// Status, screenshots and frames use one HTTP/2 connection and touches a
233/// second one, which halved drag-input p95 while native frames streamed (S7).
234/// The client sends no keys: without a keyboard device the emulator accepts
235/// and drops them, so [`Controller::keys_supported`] tells the driver whether
236/// keys may go this way at all.
237#[derive(Clone)]
238pub struct Controller {
239    serial: String,
240    frames: Client,
241    input: Client,
242    hardware: Hardware,
243    deadline: Duration,
244}
245
246impl fmt::Debug for Controller {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        f.debug_struct("Controller")
249            .field("serial", &self.serial)
250            .field("hardware", &self.hardware)
251            .field("deadline", &self.deadline)
252            .finish()
253    }
254}
255
256impl Controller {
257    /// Opens both connections and reads the hardware through `getStatus`. Any
258    /// failure, including a port that accepts connections and never answers,
259    /// makes the endpoint [`Unavailable`].
260    pub async fn connect(endpoint: &Endpoint, options: Options) -> Result<Self, Unavailable> {
261        let unavailable =
262            |reason: String| Unavailable::new(&endpoint.serial, endpoint.console_port, reason);
263        let bearer = match &endpoint.token {
264            None => Bearer(None),
265            Some(token) => Bearer(Some(
266                format!("Bearer {token}")
267                    .parse::<MetadataValue<Ascii>>()
268                    .map_err(|_| unavailable("the token is not a valid header value".into()))?,
269            )),
270        };
271        let frames = client(
272            channel(&endpoint.uri, &options)
273                .await
274                .map_err(unavailable)?,
275            bearer.clone(),
276        );
277        let input = client(
278            channel(&endpoint.uri, &options)
279                .await
280                .map_err(unavailable)?,
281            bearer,
282        );
283        let status = within(
284            "getStatus",
285            options.deadline,
286            frames.clone().get_status(unary((), options.deadline)),
287        )
288        .await
289        .map_err(|e| unavailable(e.to_string()))?;
290        let hardware = Hardware::from_status(&status).map_err(unavailable)?;
291        Ok(Self {
292            serial: endpoint.serial.clone(),
293            frames,
294            input,
295            hardware,
296            deadline: options.deadline,
297        })
298    }
299
300    pub fn serial(&self) -> &str {
301        &self.serial
302    }
303
304    /// The display size and keyboard flag read at connection time.
305    pub fn hardware(&self) -> Hardware {
306        self.hardware
307    }
308
309    /// Whether keys and text may be sent through the controller: only when the
310    /// AVD declares a keyboard (`hw.keyboard=yes`).
311    pub fn keys_supported(&self) -> bool {
312        self.hardware.keyboard
313    }
314
315    /// Sends one touch event holding `points` on the main display and returns
316    /// the round trip. The emulator acknowledges once it has scheduled the
317    /// event, not when the app received it.
318    pub async fn touch(&self, points: &[TouchPoint]) -> Result<Duration, Error> {
319        let event = proto::TouchEvent {
320            touches: points
321                .iter()
322                .map(|p| proto::Touch {
323                    x: p.x,
324                    y: p.y,
325                    identifier: p.id,
326                    pressure: p.pressure,
327                    ..Default::default()
328                })
329                .collect(),
330            display: 0,
331        };
332        let started = Instant::now();
333        within(
334            "sendTouch",
335            self.deadline,
336            self.input.clone().send_touch(unary(event, self.deadline)),
337        )
338        .await?;
339        Ok(started.elapsed())
340    }
341
342    /// Presses and lifts contact 0 at (x, y), in display pixels.
343    pub async fn tap(&self, x: i32, y: i32) -> Result<(), Error> {
344        self.touch(&[TouchPoint::down(x, y)]).await?;
345        self.touch(&[TouchPoint::up(x, y)]).await?;
346        Ok(())
347    }
348
349    /// A raw RGB888 capture at the display's full resolution.
350    pub async fn screenshot(&self) -> Result<Frame, Error> {
351        let call = "getScreenshot";
352        let image = within(
353            call,
354            self.deadline,
355            self.frames
356                .clone()
357                .get_screenshot(unary(rgb888(0, 0), self.deadline)),
358        )
359        .await?;
360        frame_from(image, call)
361    }
362
363    /// Subscribes to RGB888 frames scaled by the emulator to `working_width`
364    /// pixels (0 for the display size). The emulator sends a frame whenever the
365    /// guest posts one, changed or not, and nothing while the screen is still.
366    pub async fn frames(&self, working_width: u32) -> Result<FrameStream, Error> {
367        let size = scaled_size(self.hardware.display, working_width);
368        // No grpc-timeout here: it would end the stream.
369        let request = tonic::Request::new(rgb888(size.0, size.1));
370        let inner = within(
371            "streamScreenshot",
372            self.deadline,
373            self.frames.clone().stream_screenshot(request),
374        )
375        .await?;
376        Ok(FrameStream { inner, size })
377    }
378}
379
380/// A subscription to the emulator's frames.
381pub struct FrameStream {
382    inner: tonic::Streaming<proto::Image>,
383    size: (u32, u32),
384}
385
386impl FrameStream {
387    /// The size requested from the emulator, `(0, 0)` for the display size.
388    pub fn requested_size(&self) -> (u32, u32) {
389        self.size
390    }
391
392    /// The next frame, or `None` if none arrives within `wait`, which happens
393    /// whenever the screen is still.
394    pub async fn next(&mut self, wait: Duration) -> Result<Option<Frame>, Error> {
395        let call = "streamScreenshot";
396        match tokio::time::timeout(wait, self.inner.message()).await {
397            Err(_) => Ok(None),
398            Ok(Err(status)) => Err(Error::Status { call, status }),
399            Ok(Ok(None)) => Err(Error::StreamEnded),
400            Ok(Ok(Some(image))) => frame_from(image, call).map(Some),
401        }
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use std::sync::Arc;
409    use std::sync::atomic::{AtomicUsize, Ordering};
410    use std::time::Instant;
411    use tokio::net::TcpListener;
412
413    fn endpoint(port: u16, token: Option<&str>) -> Endpoint {
414        Endpoint {
415            serial: "emulator-5554".into(),
416            uri: format!("http://127.0.0.1:{port}"),
417            token: token.map(str::to_owned),
418            console_port: Some(5554),
419            discovery_file: None,
420        }
421    }
422
423    fn quick() -> Options {
424        Options {
425            deadline: Duration::from_millis(300),
426            connect_deadline: Duration::from_millis(300),
427        }
428    }
429
430    /// A port that accepts connections and never answers, like the emulator
431    /// ports S7 found; returns it with the count of accepted connections.
432    async fn silent_port() -> (u16, Arc<AtomicUsize>) {
433        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
434        let port = listener.local_addr().unwrap().port();
435        let accepted = Arc::new(AtomicUsize::new(0));
436        let count = accepted.clone();
437        tokio::spawn(async move {
438            let mut held = Vec::new();
439            while let Ok((socket, _)) = listener.accept().await {
440                count.fetch_add(1, Ordering::SeqCst);
441                held.push(socket);
442            }
443        });
444        (port, accepted)
445    }
446
447    #[tokio::test]
448    async fn a_silent_endpoint_fails_at_the_deadline() {
449        let (port, _) = silent_port().await;
450        let started = Instant::now();
451        let err = Controller::connect(&endpoint(port, None), quick())
452            .await
453            .unwrap_err();
454        let elapsed = started.elapsed();
455        assert!(elapsed < Duration::from_secs(2), "took {elapsed:?}");
456        assert!(err.reason.contains("getStatus"), "{}", err.reason);
457        assert!(err.reason.contains("no answer within"), "{}", err.reason);
458        assert!(err.warning().contains("-grpc 8554 -grpc-use-token"));
459    }
460
461    #[tokio::test]
462    async fn input_travels_on_a_second_connection() {
463        let (port, accepted) = silent_port().await;
464        let _ = Controller::connect(&endpoint(port, None), quick()).await;
465        assert_eq!(accepted.load(Ordering::SeqCst), 2);
466    }
467
468    #[tokio::test]
469    async fn a_closed_port_is_unavailable_with_the_fallback_warning() {
470        let port = {
471            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
472            listener.local_addr().unwrap().port()
473        };
474        let err = Controller::connect(&endpoint(port, Some("token")), quick())
475            .await
476            .unwrap_err();
477        assert!(
478            err.reason.contains(&format!("127.0.0.1:{port}")),
479            "{}",
480            err.reason
481        );
482        let warning = err.warning();
483        assert!(warning.contains("-grpc 8554 -grpc-use-token"), "{warning}");
484        assert!(warning.contains("`grpc 8554`"), "{warning}");
485    }
486
487    #[tokio::test]
488    async fn a_token_that_is_no_header_value_is_refused_before_connecting() {
489        let (port, accepted) = silent_port().await;
490        let err = Controller::connect(&endpoint(port, Some("bad\ntoken")), quick())
491            .await
492            .unwrap_err();
493        assert!(err.reason.contains("token"), "{}", err.reason);
494        assert_eq!(accepted.load(Ordering::SeqCst), 0);
495    }
496
497    #[test]
498    fn touch_points_press_and_lift_contact_zero() {
499        assert_eq!(
500            TouchPoint::down(300, 60),
501            TouchPoint {
502                id: 0,
503                x: 300,
504                y: 60,
505                pressure: DOWN_PRESSURE
506            }
507        );
508        assert_eq!(TouchPoint::up(300, 60).pressure, 0);
509    }
510}