Skip to content

Commit f3f4bfd

Browse files
committed
Fix Android display rotation handling
1 parent 58ab9fa commit f3f4bfd

4 files changed

Lines changed: 213 additions & 23 deletions

File tree

client/src/app/AppShell.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,14 +1845,25 @@ export function AppShell({
18451845
if (!selectedSimulator) {
18461846
return;
18471847
}
1848+
const androidViewport = isAndroidSimulator(selectedSimulator);
18481849
beginZoomAnimation();
18491850
if (sendControl(selectedSimulator.udid, { type: "rotateRight" })) {
1850-
setRotationQuarterTurns((current) => (current + 1) % 4);
1851+
if (androidViewport) {
1852+
setRotationQuarterTurns(0);
1853+
window.setTimeout(() => void refresh(), 300);
1854+
} else {
1855+
setRotationQuarterTurns((current) => (current + 1) % 4);
1856+
}
18511857
return;
18521858
}
18531859
void runAction(async () => {
18541860
await rotateRight(selectedSimulator.udid);
1855-
setRotationQuarterTurns((current) => (current + 1) % 4);
1861+
if (androidViewport) {
1862+
setRotationQuarterTurns(0);
1863+
await refresh();
1864+
} else {
1865+
setRotationQuarterTurns((current) => (current + 1) % 4);
1866+
}
18561867
}, false);
18571868
}}
18581869
onStreamEncoderChange={updateStreamEncoder}

server/src/android.rs

Lines changed: 194 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ const MODIFIER_COMMAND: u32 = 1 << 3;
2828
const MODIFIER_CAPS_LOCK: u32 = 1 << 4;
2929

3030
type TimedMap<T> = Option<(Instant, HashMap<String, T>)>;
31-
type ScreenSizeCache = HashMap<String, (Instant, (f64, f64))>;
31+
type DisplayMetricsCache = HashMap<String, (Instant, AndroidDisplayMetrics)>;
32+
33+
#[derive(Clone, Copy, Debug, PartialEq)]
34+
struct AndroidDisplayMetrics {
35+
width: f64,
36+
height: f64,
37+
rotation_quarter_turns: u16,
38+
}
3239

3340
#[derive(Clone, Default)]
3441
pub struct AndroidBridge;
@@ -411,8 +418,32 @@ impl AndroidBridge {
411418
}
412419

413420
pub fn rotate_right(&self, id: &str) -> Result<(), AppError> {
421+
self.rotate_by_quarter_turns(id, 1)
422+
}
423+
424+
pub fn rotate_left(&self, id: &str) -> Result<(), AppError> {
425+
self.rotate_by_quarter_turns(id, -1)
426+
}
427+
428+
fn rotate_by_quarter_turns(&self, id: &str, delta: i16) -> Result<(), AppError> {
414429
let serial = self.serial_for_id(id)?;
415-
self.run_adb(["-s", &serial, "emu", "rotate"])?;
430+
let current = self
431+
.display_metrics_for_serial(&serial)
432+
.map(|metrics| metrics.rotation_quarter_turns)
433+
.unwrap_or(0);
434+
let next = (i16::try_from(current).unwrap_or(0) + delta).rem_euclid(4) as u16;
435+
let rotation = next.to_string();
436+
self.run_adb([
437+
"-s",
438+
&serial,
439+
"shell",
440+
"cmd",
441+
"window",
442+
"user-rotation",
443+
"lock",
444+
&rotation,
445+
])?;
446+
self.invalidate_display_metrics_for_serial(&serial);
416447
Ok(())
417448
}
418449

@@ -689,12 +720,18 @@ impl AndroidBridge {
689720
fn device_value(&self, device: AndroidDevice) -> Value {
690721
let id = id_for_avd(&device.avd_name);
691722
let private_display = if let Some(serial) = device.serial.as_deref() {
692-
let (width, height) = self.screen_size_for_serial(serial).unwrap_or((0.0, 0.0));
723+
let metrics =
724+
self.display_metrics_for_serial(serial)
725+
.unwrap_or(AndroidDisplayMetrics {
726+
width: 0.0,
727+
height: 0.0,
728+
rotation_quarter_turns: 0,
729+
});
693730
json!({
694-
"displayReady": width > 0.0 && height > 0.0,
731+
"displayReady": metrics.width > 0.0 && metrics.height > 0.0,
695732
"displayStatus": "Ready",
696-
"displayWidth": width,
697-
"displayHeight": height,
733+
"displayWidth": metrics.width,
734+
"displayHeight": metrics.height,
698735
"frameSequence": 0,
699736
"rotationQuarterTurns": 0,
700737
})
@@ -804,13 +841,32 @@ impl AndroidBridge {
804841
}
805842

806843
fn screen_size_for_serial(&self, serial: &str) -> Result<(f64, f64), AppError> {
807-
static CACHE: OnceLock<Mutex<ScreenSizeCache>> = OnceLock::new();
808-
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
809-
if let Some((updated_at, size)) = cache.lock().unwrap().get(serial) {
844+
let metrics = self.display_metrics_for_serial(serial)?;
845+
Ok((metrics.width, metrics.height))
846+
}
847+
848+
fn display_metrics_for_serial(&self, serial: &str) -> Result<AndroidDisplayMetrics, AppError> {
849+
let cache = android_display_metrics_cache();
850+
if let Some((updated_at, metrics)) = cache.lock().unwrap().get(serial) {
810851
if updated_at.elapsed() < SCREEN_SIZE_CACHE_TTL {
811-
return Ok(*size);
852+
return Ok(*metrics);
812853
}
813854
}
855+
let output = self.run_adb(["-s", serial, "shell", "dumpsys", "display"])?;
856+
let metrics = parse_android_display_metrics(&output)
857+
.or_else(|| self.wm_display_metrics_for_serial(serial).ok())
858+
.ok_or_else(|| AppError::native("Android emulator did not report display metrics."))?;
859+
cache
860+
.lock()
861+
.unwrap()
862+
.insert(serial.to_owned(), (Instant::now(), metrics));
863+
Ok(metrics)
864+
}
865+
866+
fn wm_display_metrics_for_serial(
867+
&self,
868+
serial: &str,
869+
) -> Result<AndroidDisplayMetrics, AppError> {
814870
let output = self.run_adb(["-s", serial, "shell", "wm", "size"])?;
815871
let size = output
816872
.split_whitespace()
@@ -825,11 +881,18 @@ impl AndroidBridge {
825881
let height = height
826882
.parse::<f64>()
827883
.map_err(|_| AppError::native("Android emulator reported an invalid height."))?;
828-
cache
884+
Ok(AndroidDisplayMetrics {
885+
width,
886+
height,
887+
rotation_quarter_turns: 0,
888+
})
889+
}
890+
891+
fn invalidate_display_metrics_for_serial(&self, serial: &str) {
892+
android_display_metrics_cache()
829893
.lock()
830894
.unwrap()
831-
.insert(serial.to_owned(), (Instant::now(), (width, height)));
832-
Ok((width, height))
895+
.remove(serial);
833896
}
834897

835898
fn run_adb_shell(&self, serial: &str, script: &str) -> Result<String, AppError> {
@@ -1173,6 +1236,89 @@ fn android_type(short_class: &str, class_name: &str) -> String {
11731236
}
11741237
}
11751238

1239+
fn android_display_metrics_cache() -> &'static Mutex<DisplayMetricsCache> {
1240+
static CACHE: OnceLock<Mutex<DisplayMetricsCache>> = OnceLock::new();
1241+
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
1242+
}
1243+
1244+
fn parse_android_display_metrics(output: &str) -> Option<AndroidDisplayMetrics> {
1245+
let rotation = parse_android_display_rotation(output).unwrap_or(0);
1246+
if let Some(line) = output
1247+
.lines()
1248+
.find(|line| line.contains("mOverrideDisplayInfo=DisplayInfo"))
1249+
{
1250+
if let Some((width, height)) = parse_display_info_app_size(line) {
1251+
return Some(AndroidDisplayMetrics {
1252+
width,
1253+
height,
1254+
rotation_quarter_turns: rotation,
1255+
});
1256+
}
1257+
}
1258+
if let Some((width, height)) = output.lines().find_map(parse_logical_frame_size) {
1259+
return Some(AndroidDisplayMetrics {
1260+
width,
1261+
height,
1262+
rotation_quarter_turns: rotation,
1263+
});
1264+
}
1265+
None
1266+
}
1267+
1268+
fn parse_android_display_rotation(output: &str) -> Option<u16> {
1269+
output
1270+
.lines()
1271+
.find(|line| line.contains("mOverrideDisplayInfo=DisplayInfo"))
1272+
.and_then(parse_display_info_rotation)
1273+
.or_else(|| {
1274+
output
1275+
.lines()
1276+
.find_map(|line| {
1277+
line.split_once("mCurrentOrientation=")
1278+
.map(|(_, value)| value)
1279+
})
1280+
.and_then(parse_rotation_token)
1281+
})
1282+
}
1283+
1284+
fn parse_display_info_app_size(line: &str) -> Option<(f64, f64)> {
1285+
let (_, value) = line.rsplit_once(", app ")?;
1286+
parse_size_prefix(value)
1287+
}
1288+
1289+
fn parse_display_info_rotation(line: &str) -> Option<u16> {
1290+
let (_, value) = line.rsplit_once(", rotation ")?;
1291+
parse_rotation_token(value)
1292+
}
1293+
1294+
fn parse_rotation_token(value: &str) -> Option<u16> {
1295+
let digits = value
1296+
.trim()
1297+
.chars()
1298+
.take_while(|character| character.is_ascii_digit())
1299+
.collect::<String>();
1300+
let rotation = digits.parse::<u16>().ok()?;
1301+
Some(rotation % 4)
1302+
}
1303+
1304+
fn parse_size_prefix(value: &str) -> Option<(f64, f64)> {
1305+
let mut parts = value.split_whitespace();
1306+
let width = parts.next()?.trim_end_matches(',').parse::<f64>().ok()?;
1307+
if parts.next()? != "x" {
1308+
return None;
1309+
}
1310+
let height = parts.next()?.trim_end_matches(',').parse::<f64>().ok()?;
1311+
Some((width, height))
1312+
}
1313+
1314+
fn parse_logical_frame_size(line: &str) -> Option<(f64, f64)> {
1315+
let (_, value) = line.split_once("logicalFrame=Rect(")?;
1316+
let (frame, _) = value.split_once(')')?;
1317+
let (_, max_values) = frame.split_once(" - ")?;
1318+
let (width, height) = max_values.split_once(',')?;
1319+
Some((width.trim().parse().ok()?, height.trim().parse().ok()?))
1320+
}
1321+
11761322
fn android_role(node: roxmltree::Node<'_, '_>, class_name: &str) -> &'static str {
11771323
let clickable = bool_attr(node, "clickable");
11781324
let scrollable = bool_attr(node, "scrollable");
@@ -1446,6 +1592,41 @@ mod tests {
14461592
vec![113, 59]
14471593
);
14481594
}
1595+
1596+
#[test]
1597+
fn parse_android_display_metrics_prefers_current_app_size() {
1598+
let output = r#"
1599+
mViewports=[DisplayViewport{type=INTERNAL, logicalFrame=Rect(0, 0 - 2400, 1080), physicalFrame=Rect(0, 0 - 2400, 1080)}]
1600+
mCurrentOrientation=3
1601+
mOverrideDisplayInfo=DisplayInfo{"Built-in Screen", real 2400 x 1080, largest app 2400 x 2400, smallest app 1080 x 1080, rotation 3, state ON, app 2400 x 1080, density 420}
1602+
"#;
1603+
1604+
assert_eq!(
1605+
parse_android_display_metrics(output),
1606+
Some(AndroidDisplayMetrics {
1607+
width: 2400.0,
1608+
height: 1080.0,
1609+
rotation_quarter_turns: 3,
1610+
})
1611+
);
1612+
}
1613+
1614+
#[test]
1615+
fn parse_android_display_metrics_falls_back_to_logical_frame() {
1616+
let output = r#"
1617+
mViewports=[DisplayViewport{type=INTERNAL, logicalFrame=Rect(0, 0 - 1080, 2400), physicalFrame=Rect(0, 0 - 1080, 2400)}]
1618+
mCurrentOrientation=0
1619+
"#;
1620+
1621+
assert_eq!(
1622+
parse_android_display_metrics(output),
1623+
Some(AndroidDisplayMetrics {
1624+
width: 1080.0,
1625+
height: 2400.0,
1626+
rotation_quarter_turns: 0,
1627+
})
1628+
);
1629+
}
14491630
}
14501631

14511632
#[allow(dead_code)]

server/src/api/routes.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,9 +1859,8 @@ async fn run_android_control_message(
18591859
ControlMessage::DismissKeyboard => android.dismiss_keyboard(&udid),
18601860
ControlMessage::Home => android.press_home(&udid),
18611861
ControlMessage::AppSwitcher => android.open_app_switcher(&udid),
1862-
ControlMessage::RotateLeft | ControlMessage::RotateRight => {
1863-
android.rotate_right(&udid)
1864-
}
1862+
ControlMessage::RotateLeft => android.rotate_left(&udid),
1863+
ControlMessage::RotateRight => android.rotate_right(&udid),
18651864
ControlMessage::ToggleAppearance => android.toggle_appearance(&udid),
18661865
ControlMessage::Touch { .. }
18671866
| ControlMessage::EdgeTouch { .. }
@@ -2694,7 +2693,7 @@ async fn rotate_left(
26942693
Path(udid): Path<String>,
26952694
) -> Result<Json<Value>, AppError> {
26962695
if android::is_android_id(&udid) {
2697-
run_android_action(state, move |android| android.rotate_right(&udid)).await?;
2696+
run_android_action(state, move |android| android.rotate_left(&udid)).await?;
26982697
return Ok(json(json_value!({ "ok": true })));
26992698
}
27002699
run_bridge_action(state, move |bridge| bridge.rotate_left(&udid)).await?;
@@ -3500,7 +3499,7 @@ async fn run_batch_step(state: AppState, udid: String, step: BatchStep) -> Resul
35003499
}
35013500
BatchStep::RotateLeft => {
35023501
if android::is_android_id(&udid) {
3503-
run_android_action(state, move |android| android.rotate_right(&udid)).await?;
3502+
run_android_action(state, move |android| android.rotate_left(&udid)).await?;
35043503
return Ok(json_value!({ "action": "rotateLeft" }));
35053504
}
35063505
run_bridge_action(state, move |bridge| bridge.rotate_left(&udid)).await?;

server/src/transport/webrtc.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -860,9 +860,8 @@ async fn run_android_webrtc_control_message(
860860
ControlMessage::DismissKeyboard => state.android.dismiss_keyboard(&udid),
861861
ControlMessage::Home => state.android.press_home(&udid),
862862
ControlMessage::AppSwitcher => state.android.open_app_switcher(&udid),
863-
ControlMessage::RotateLeft | ControlMessage::RotateRight => {
864-
state.android.rotate_right(&udid)
865-
}
863+
ControlMessage::RotateLeft => state.android.rotate_left(&udid),
864+
ControlMessage::RotateRight => state.android.rotate_right(&udid),
866865
ControlMessage::ToggleAppearance => state.android.toggle_appearance(&udid),
867866
})
868867
.await

0 commit comments

Comments
 (0)