@@ -28,7 +28,14 @@ const MODIFIER_COMMAND: u32 = 1 << 3;
2828const MODIFIER_CAPS_LOCK : u32 = 1 << 4 ;
2929
3030type 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 ) ]
3441pub 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+
11761322fn 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) ]
0 commit comments