diff --git a/DEPS b/DEPS index 22c4c6c1def73..1317887d99652 100644 --- a/DEPS +++ b/DEPS @@ -570,7 +570,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/embedding_bundle', - 'version': 'last_updated:2021-10-28T23:34:47-0700' + 'version': 'last_updated:2021-11-03T10:29:50-0700' } ], 'condition': 'download_android_deps', diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 3aaa72d575bf3..37977c60e9b52 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -835,6 +835,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Rende FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/TransparencyMode.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/WindowInfoRepositoryCallbackAdapterWrapper.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java diff --git a/lib/ui/fixtures/ui_test.dart b/lib/ui/fixtures/ui_test.dart index b5eac66aed490..ad560db96ba30 100644 --- a/lib/ui/fixtures/ui_test.dart +++ b/lib/ui/fixtures/ui_test.dart @@ -339,7 +339,7 @@ void hooksTests() { window.onMetricsChanged!(); _callHook( '_updateWindowMetrics', - 17, + 20, 0, // window Id 0.1234, // device pixel ratio 0.0, // width @@ -355,8 +355,11 @@ void hooksTests() { 0.0, // system gesture inset top 0.0, // system gesture inset right 0.0, // system gesture inset bottom - 0.0, // system gesture inset left, + 0.0, // system gesture inset left 22.0, // physicalTouchSlop + [], // display features bounds + [], // display features types + [], // display features states ); expectIdentical(originalZone, callbackZone); @@ -403,7 +406,7 @@ void hooksTests() { test('Window padding/insets/viewPadding/systemGestureInsets', () { _callHook( '_updateWindowMetrics', - 17, + 20, 0, // window Id 1.0, // devicePixelRatio 800.0, // width @@ -421,6 +424,9 @@ void hooksTests() { 0.0, // systemGestureInsetBottom 0.0, // systemGestureInsetLeft 22.0, // physicalTouchSlop + [], // display features bounds + [], // display features types + [], // display features states ); expectEquals(window.viewInsets.bottom, 0.0); @@ -430,7 +436,7 @@ void hooksTests() { _callHook( '_updateWindowMetrics', - 17, + 20, 0, // window Id 1.0, // devicePixelRatio 800.0, // width @@ -448,6 +454,9 @@ void hooksTests() { 44.0, // systemGestureInsetBottom 0.0, // systemGestureInsetLeft 22.0, // physicalTouchSlop + [], // display features bounds + [], // display features types + [], // display features states ); expectEquals(window.viewInsets.bottom, 400.0); @@ -459,7 +468,7 @@ void hooksTests() { test('Window physical touch slop', () { _callHook( '_updateWindowMetrics', - 17, + 20, 0, // window Id 1.0, // devicePixelRatio 800.0, // width @@ -477,6 +486,9 @@ void hooksTests() { 0.0, // systemGestureInsetBottom 0.0, // systemGestureInsetLeft 11.0, // physicalTouchSlop + [], // display features bounds + [], // display features types + [], // display features states ); expectEquals(window.viewConfiguration.gestureSettings, @@ -484,7 +496,7 @@ void hooksTests() { _callHook( '_updateWindowMetrics', - 17, + 20, 0, // window Id 1.0, // devicePixelRatio 800.0, // width @@ -502,6 +514,9 @@ void hooksTests() { 44.0, // systemGestureInsetBottom 0.0, // systemGestureInsetLeft -1.0, // physicalTouchSlop + [], // display features bounds + [], // display features types + [], // display features states ); expectEquals(window.viewConfiguration.gestureSettings, @@ -509,7 +524,7 @@ void hooksTests() { _callHook( '_updateWindowMetrics', - 17, + 20, 0, // window Id 1.0, // devicePixelRatio 800.0, // width @@ -527,6 +542,9 @@ void hooksTests() { 44.0, // systemGestureInsetBottom 0.0, // systemGestureInsetLeft 22.0, // physicalTouchSlop + [], // display features bounds + [], // display features types + [], // display features states ); expectEquals(window.viewConfiguration.gestureSettings, @@ -752,4 +770,7 @@ void _callHook( Object? arg15, Object? arg16, Object? arg17, + Object? arg18, + Object? arg19, + Object? arg20, ]) native 'CallHook'; diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 8ef8b66e662c6..24829cda2744b 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -26,6 +26,9 @@ void _updateWindowMetrics( double systemGestureInsetBottom, double systemGestureInsetLeft, double physicalTouchSlop, + List displayFeaturesBounds, + List displayFeaturesType, + List displayFeaturesState, ) { PlatformDispatcher.instance._updateWindowMetrics( id, @@ -45,6 +48,9 @@ void _updateWindowMetrics( systemGestureInsetBottom, systemGestureInsetLeft, physicalTouchSlop, + displayFeaturesBounds, + displayFeaturesType, + displayFeaturesState, ); } diff --git a/lib/ui/platform_dispatcher.dart b/lib/ui/platform_dispatcher.dart index cc0bdcdfb14b9..257d351b2348a 100644 --- a/lib/ui/platform_dispatcher.dart +++ b/lib/ui/platform_dispatcher.dart @@ -183,6 +183,9 @@ class PlatformDispatcher { double systemGestureInsetBottom, double systemGestureInsetLeft, double physicalTouchSlop, + List displayFeaturesBounds, + List displayFeaturesType, + List displayFeaturesState, ) { final ViewConfiguration previousConfiguration = _viewConfigurations[id] ?? const ViewConfiguration(); @@ -221,10 +224,43 @@ class PlatformDispatcher { gestureSettings: GestureSettings( physicalTouchSlop: physicalTouchSlop == _kUnsetGestureSetting ? null : physicalTouchSlop, ), + displayFeatures: _decodeDisplayFeatures( + bounds: displayFeaturesBounds, + type: displayFeaturesType, + state: displayFeaturesState, + devicePixelRatio: devicePixelRatio, + ), ); _invoke(onMetricsChanged, _onMetricsChangedZone); } + List _decodeDisplayFeatures({ + required List bounds, + required List type, + required List state, + required double devicePixelRatio, + }) { + assert(bounds.length / 4 == type.length, 'Bounds are rectangles, requiring 4 measurements each'); + assert(type.length == state.length); + final List result = []; + for(int i = 0; i < type.length; i++){ + final int rectOffset = i * 4; + result.add(DisplayFeature( + bounds: Rect.fromLTRB( + bounds[rectOffset] / devicePixelRatio, + bounds[rectOffset + 1] / devicePixelRatio, + bounds[rectOffset + 2] / devicePixelRatio, + bounds[rectOffset + 3] / devicePixelRatio, + ), + type: DisplayFeatureType.values[type[i]], + state: state[i] < DisplayFeatureState.values.length + ? DisplayFeatureState.values[state[i]] + : DisplayFeatureState.unknown, + )); + } + return result; + } + /// A callback invoked when any view begins a frame. /// /// A callback that is invoked to notify the application that it is an @@ -1035,6 +1071,7 @@ class ViewConfiguration { this.systemGestureInsets = WindowPadding.zero, this.padding = WindowPadding.zero, this.gestureSettings = const GestureSettings(), + this.displayFeatures = const [], }); /// Copy this configuration with some fields replaced. @@ -1047,7 +1084,8 @@ class ViewConfiguration { WindowPadding? viewPadding, WindowPadding? systemGestureInsets, WindowPadding? padding, - GestureSettings? gestureSettings + GestureSettings? gestureSettings, + List? displayFeatures, }) { return ViewConfiguration( window: window ?? this.window, @@ -1059,6 +1097,7 @@ class ViewConfiguration { systemGestureInsets: systemGestureInsets ?? this.systemGestureInsets, padding: padding ?? this.padding, gestureSettings: gestureSettings ?? this.gestureSettings, + displayFeatures: displayFeatures ?? this.displayFeatures, ); } @@ -1138,6 +1177,26 @@ class ViewConfiguration { /// touch slop constant. final GestureSettings gestureSettings; + /// {@template dart.ui.ViewConfiguration.displayFeatures} + /// Areas of the display that are obstructed by hardware features. + /// + /// This list is populated only on Android. If the device has no display + /// features, this list is empty. + /// + /// The coordinate space in which the [DisplayFeature.bounds] are defined spans + /// across the screens currently in use. This means that the space between the screens + /// is virtually part of the Flutter view space, with the [DisplayFeature.bounds] + /// of the display feature as an obstructed area. The [DisplayFeature.type] can + /// be used to determine if this display feature obstructs the screen or not. + /// For example, [DisplayFeatureType.hinge] and [DisplayFeatureType.cutout] both + /// obstruct the display, while [DisplayFeatureType.fold] is a crease in the display. + /// + /// Folding [DisplayFeature]s like the [DisplayFeatureType.hinge] and + /// [DisplayFeatureType.fold] also have a [DisplayFeature.state] which can be + /// used to determine the posture the device is in. + /// {@endtemplate} + final List displayFeatures; + @override String toString() { return '$runtimeType[window: $window, geometry: $geometry]'; @@ -1443,6 +1502,134 @@ class WindowPadding { } } +/// Area of the display that may be obstructed by a hardware feature. +/// +/// This is populated only on Android. +/// +/// The [bounds] are measured in logical pixels. On devices with two screens the +/// coordinate system starts with [0,0] in the top-left corner of the left or top screen +/// and expands to include both screens and the visual space between them. +/// +/// The [type] describes the behaviour and if [DisplayFeature] obstructs the display. +/// For example, [DisplayFeatureType.hinge] and [DisplayFeatureType.cutout] both obstruct the display, +/// while [DisplayFeatureType.fold] does not. +/// +/// ![Device with a hinge display feature](https://flutter.github.io/assets-for-api-docs/assets/hardware/display_feature_hinge.png) +/// +/// ![Device with a fold display feature](https://flutter.github.io/assets-for-api-docs/assets/hardware/display_feature_fold.png) +/// +/// ![Device with a cutout display feature](https://flutter.github.io/assets-for-api-docs/assets/hardware/display_feature_cutout.png) +/// +/// The [state] contains information about the posture for foldable features +/// ([DisplayFeatureType.hinge] and [DisplayFeatureType.fold]). The posture is +/// the shape of the display, for example [DisplayFeatureState.postureFlat] or +/// [DisplayFeatureState.postureHalfOpened]. For [DisplayFeatureType.cutout], +/// the state is not used and has the [DisplayFeatureState.unknown] value. +class DisplayFeature { + const DisplayFeature({ + required this.bounds, + required this.type, + required this.state, + }) : assert(!identical(type, DisplayFeatureType.cutout) || identical(state, DisplayFeatureState.unknown)); + + /// The area of the flutter view occupied by this display feature, measured in logical pixels. + /// + /// On devices with two screens, the Flutter view spans from the top-left corner + /// of the left or top screen to the bottom-right corner of the right or bottom screen, + /// including the visual area occupied by any display feature. Bounds of display + /// features are reported in this coordinate system. + /// + /// For example, on a dual screen device in portrait mode: + /// + /// * [bounds.left] gives you the size of left screen, in logical pixels. + /// * [bounds.right] gives you the size of the left screen + the hinge width. + final Rect bounds; + + /// Type of display feature, e.g. hinge, fold, cutout. + final DisplayFeatureType type; + + /// Posture of display feature, which is populated only for folds and hinges. + /// + /// For cutouts, this is [DisplayFeatureState.unknown] + final DisplayFeatureState state; + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + return other is DisplayFeature && bounds == other.bounds && + type == other.type && state == other.state; + } + + @override + int get hashCode => hashValues(bounds, type, state); + + @override + String toString() { + return 'DisplayFeature(rect: $bounds, type: $type, state: $state)'; + } +} + +/// Type of [DisplayFeature], describing the [DisplayFeature] behaviour and if +/// it obstructs the display. +/// +/// Some types of [DisplayFeature], like [DisplayFeatureType.fold], can be +/// reported without actually impeding drawing on the screen. They are useful +/// for knowing where the display is bent or has a crease. The +/// [DisplayFeature.bounds] can be 0-width in such cases. +/// +/// The shape formed by the screens for types [DisplayFeatureType.fold] and +/// [DisplayFeatureType.hinge] is called the posture and is exposed in +/// [DisplayFeature.state]. For example, the [DisplayFeatureState.postureFlat] posture +/// means the screens form a flat surface, while [DisplayFeatureState.postureFlipped] +/// posture means the screens are facing opposite directions. +/// +/// ![Device with a hinge display feature](https://flutter.github.io/assets-for-api-docs/assets/hardware/display_feature_hinge.png) +/// +/// ![Device with a fold display feature](https://flutter.github.io/assets-for-api-docs/assets/hardware/display_feature_fold.png) +/// +/// ![Device with a cutout display feature](https://flutter.github.io/assets-for-api-docs/assets/hardware/display_feature_cutout.png) +enum DisplayFeatureType { + /// [DisplayFeature] type is new and not yet known to Flutter. + unknown, + /// A fold in the flexible screen without a physical gap. + /// + /// The bounds for this display feature type indicate where the display makes a crease. + fold, + /// A physical separation with a hinge that allows two display panels to fold. + hinge, + /// A non-displaying area of the screen, usually housing cameras or sensors. + cutout, +} + +/// State of the display feature, which contains information about the posture +/// for foldable features. +/// +/// The posture is the shape made by the parts of the flexible screen or +/// physical screen panels. They are inspired by and similar to +/// [Android Postures](https://developer.android.com/guide/topics/ui/foldables#postures). +/// +/// * For [DisplayFeatureType.fold]s & [DisplayFeatureType.hinge]s, the state is +/// the posture. +/// * For [DisplayFeatureType.cutout]s, the state is not used and has the +/// [DisplayFeatureState.unknown] value. +enum DisplayFeatureState { + /// The display feature is a [DisplayFeatureType.cutout] or this state is new + /// and not yet known to Flutter. + unknown, + /// The foldable device is completely open. + /// + /// The screen space that is presented to the user is flat. + postureFlat, + /// Fold angle is in an intermediate position between opened and closed state. + /// + /// There is a non-flat angle between parts of the flexible screen or between + /// physical screen panels such that the screens start to face each other. + postureHalfOpened, +} + /// An identifier used to select a user's language and formatting preferences. /// /// This represents a [Unicode Language diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 8ddb931d184ea..31e63036081b4 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -225,6 +225,17 @@ abstract class FlutterView { /// applications. WindowPadding get padding => viewConfiguration.padding; + /// {@macro dart.ui.ViewConfiguration.displayFeatures} + /// + /// When this changes, [onMetricsChanged] is called. + /// + /// See also: + /// + /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to + /// observe when this value changes. + /// * [MediaQuery.of], a simpler mechanism to access this data. + List get displayFeatures => viewConfiguration.displayFeatures; + /// Updates the view's rendering on the GPU with the newly provided [Scene]. /// /// This function must be called within the scope of the diff --git a/lib/ui/window/viewport_metrics.cc b/lib/ui/window/viewport_metrics.cc index 961e29dd4cf2e..82867c2e6c917 100644 --- a/lib/ui/window/viewport_metrics.cc +++ b/lib/ui/window/viewport_metrics.cc @@ -19,22 +19,26 @@ ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, physical_height(p_physical_height), physical_touch_slop(p_physical_touch_slop) {} -ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, - double p_physical_width, - double p_physical_height, - double p_physical_padding_top, - double p_physical_padding_right, - double p_physical_padding_bottom, - double p_physical_padding_left, - double p_physical_view_inset_top, - double p_physical_view_inset_right, - double p_physical_view_inset_bottom, - double p_physical_view_inset_left, - double p_physical_system_gesture_inset_top, - double p_physical_system_gesture_inset_right, - double p_physical_system_gesture_inset_bottom, - double p_physical_system_gesture_inset_left, - double p_physical_touch_slop) +ViewportMetrics::ViewportMetrics( + double p_device_pixel_ratio, + double p_physical_width, + double p_physical_height, + double p_physical_padding_top, + double p_physical_padding_right, + double p_physical_padding_bottom, + double p_physical_padding_left, + double p_physical_view_inset_top, + double p_physical_view_inset_right, + double p_physical_view_inset_bottom, + double p_physical_view_inset_left, + double p_physical_system_gesture_inset_top, + double p_physical_system_gesture_inset_right, + double p_physical_system_gesture_inset_bottom, + double p_physical_system_gesture_inset_left, + double p_physical_touch_slop, + const std::vector p_physical_display_features_bounds, + const std::vector p_physical_display_features_type, + const std::vector p_physical_display_features_state) : device_pixel_ratio(p_device_pixel_ratio), physical_width(p_physical_width), physical_height(p_physical_height), @@ -52,7 +56,10 @@ ViewportMetrics::ViewportMetrics(double p_device_pixel_ratio, physical_system_gesture_inset_bottom( p_physical_system_gesture_inset_bottom), physical_system_gesture_inset_left(p_physical_system_gesture_inset_left), - physical_touch_slop(p_physical_touch_slop) {} + physical_touch_slop(p_physical_touch_slop), + physical_display_features_bounds(p_physical_display_features_bounds), + physical_display_features_type(p_physical_display_features_type), + physical_display_features_state(p_physical_display_features_state) {} bool operator==(const ViewportMetrics& a, const ViewportMetrics& b) { return a.device_pixel_ratio == b.device_pixel_ratio && @@ -74,7 +81,11 @@ bool operator==(const ViewportMetrics& a, const ViewportMetrics& b) { b.physical_system_gesture_inset_bottom && a.physical_system_gesture_inset_left == b.physical_system_gesture_inset_left && - a.physical_touch_slop == b.physical_touch_slop; + a.physical_touch_slop == b.physical_touch_slop && + a.physical_display_features_bounds == + b.physical_display_features_bounds && + a.physical_display_features_type == b.physical_display_features_type && + a.physical_display_features_state == b.physical_display_features_state; } std::ostream& operator<<(std::ostream& os, const ViewportMetrics& a) { @@ -89,7 +100,8 @@ std::ostream& operator<<(std::ostream& os, const ViewportMetrics& a) { << "Gesture Insets: [" << a.physical_system_gesture_inset_top << "T " << a.physical_system_gesture_inset_right << "R " << a.physical_system_gesture_inset_bottom << "B " - << a.physical_system_gesture_inset_left << "L]"; + << a.physical_system_gesture_inset_left << "L] " + << "Display Features: " << a.physical_display_features_type.size(); return os; } diff --git a/lib/ui/window/viewport_metrics.h b/lib/ui/window/viewport_metrics.h index 3224b2930609a..1dab8927afb38 100644 --- a/lib/ui/window/viewport_metrics.h +++ b/lib/ui/window/viewport_metrics.h @@ -6,6 +6,7 @@ #define FLUTTER_LIB_UI_WINDOW_VIEWPORT_METRICS_H_ #include +#include namespace flutter { @@ -30,7 +31,10 @@ struct ViewportMetrics { double p_physical_system_gesture_inset_right, double p_physical_system_gesture_inset_bottom, double p_physical_system_gesture_inset_left, - double p_physical_touch_slop); + double p_physical_touch_slop, + const std::vector p_physical_display_features_bounds, + const std::vector p_physical_display_features_type, + const std::vector p_physical_display_features_state); double device_pixel_ratio = 1.0; double physical_width = 0; @@ -48,6 +52,9 @@ struct ViewportMetrics { double physical_system_gesture_inset_bottom = 0; double physical_system_gesture_inset_left = 0; double physical_touch_slop = -1.0; + std::vector physical_display_features_bounds; + std::vector physical_display_features_type; + std::vector physical_display_features_state; }; bool operator==(const ViewportMetrics& a, const ViewportMetrics& b); diff --git a/lib/ui/window/window.cc b/lib/ui/window/window.cc index 66f5e2e7dbef0..23a04398e2600 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -64,22 +64,28 @@ void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { tonic::DartState::Scope scope(dart_state); tonic::LogIfError(tonic::DartInvokeField( library_.value(), "_updateWindowMetrics", - {tonic::ToDart(window_id_), tonic::ToDart(metrics.device_pixel_ratio), - tonic::ToDart(metrics.physical_width), - tonic::ToDart(metrics.physical_height), - tonic::ToDart(metrics.physical_padding_top), - tonic::ToDart(metrics.physical_padding_right), - tonic::ToDart(metrics.physical_padding_bottom), - tonic::ToDart(metrics.physical_padding_left), - tonic::ToDart(metrics.physical_view_inset_top), - tonic::ToDart(metrics.physical_view_inset_right), - tonic::ToDart(metrics.physical_view_inset_bottom), - tonic::ToDart(metrics.physical_view_inset_left), - tonic::ToDart(metrics.physical_system_gesture_inset_top), - tonic::ToDart(metrics.physical_system_gesture_inset_right), - tonic::ToDart(metrics.physical_system_gesture_inset_bottom), - tonic::ToDart(metrics.physical_system_gesture_inset_left), - tonic::ToDart(metrics.physical_touch_slop)})); + { + tonic::ToDart(window_id_), + tonic::ToDart(metrics.device_pixel_ratio), + tonic::ToDart(metrics.physical_width), + tonic::ToDart(metrics.physical_height), + tonic::ToDart(metrics.physical_padding_top), + tonic::ToDart(metrics.physical_padding_right), + tonic::ToDart(metrics.physical_padding_bottom), + tonic::ToDart(metrics.physical_padding_left), + tonic::ToDart(metrics.physical_view_inset_top), + tonic::ToDart(metrics.physical_view_inset_right), + tonic::ToDart(metrics.physical_view_inset_bottom), + tonic::ToDart(metrics.physical_view_inset_left), + tonic::ToDart(metrics.physical_system_gesture_inset_top), + tonic::ToDart(metrics.physical_system_gesture_inset_right), + tonic::ToDart(metrics.physical_system_gesture_inset_bottom), + tonic::ToDart(metrics.physical_system_gesture_inset_left), + tonic::ToDart(metrics.physical_touch_slop), + tonic::ToDart(metrics.physical_display_features_bounds), + tonic::ToDart(metrics.physical_display_features_type), + tonic::ToDart(metrics.physical_display_features_state), + })); } } // namespace flutter diff --git a/lib/web_ui/lib/src/ui/platform_dispatcher.dart b/lib/web_ui/lib/src/ui/platform_dispatcher.dart index 568a727d2f36b..1027aa7f9b12a 100644 --- a/lib/web_ui/lib/src/ui/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/ui/platform_dispatcher.dart @@ -156,6 +156,7 @@ class ViewConfiguration { this.systemGestureInsets = WindowPadding.zero, this.padding = WindowPadding.zero, this.gestureSettings = const GestureSettings(), + this.displayFeatures = const [], }); ViewConfiguration copyWith({ @@ -168,6 +169,7 @@ class ViewConfiguration { WindowPadding? systemGestureInsets, WindowPadding? padding, GestureSettings? gestureSettings, + List? displayFeatures, }) { return ViewConfiguration( window: window ?? this.window, @@ -179,6 +181,7 @@ class ViewConfiguration { systemGestureInsets: systemGestureInsets ?? this.systemGestureInsets, padding: padding ?? this.padding, gestureSettings: gestureSettings ?? this.gestureSettings, + displayFeatures: displayFeatures ?? this.displayFeatures, ); } @@ -191,6 +194,7 @@ class ViewConfiguration { final WindowPadding systemGestureInsets; final WindowPadding padding; final GestureSettings gestureSettings; + final List displayFeatures; @override String toString() { @@ -325,6 +329,50 @@ abstract class WindowPadding { } } +class DisplayFeature { + const DisplayFeature({ + required this.bounds, + required this.type, + required this.state, + }); + + final Rect bounds; + final DisplayFeatureType type; + final DisplayFeatureState state; + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + return other is DisplayFeature && bounds == other.bounds && + type == other.type && state == other.state; + } + + @override + int get hashCode => hashValues(bounds, type, state); + + @override + String toString() { + return 'DisplayFeature(rect: $bounds, type: $type, state: $state)'; + } +} + +enum DisplayFeatureType { + unknown, + fold, + hinge, + cutout, +} + +enum DisplayFeatureState { + unknown, + postureFlat, + postureHalfOpened, + postureFlipped, +} + class Locale { const Locale( this._languageCode, [ diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index b69d30765e4ef..d88e6dc5a56fa 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -14,6 +14,7 @@ abstract class FlutterView { WindowPadding get viewPadding => viewConfiguration.viewPadding; WindowPadding get systemGestureInsets => viewConfiguration.systemGestureInsets; WindowPadding get padding => viewConfiguration.padding; + List get displayFeatures => viewConfiguration.displayFeatures; void render(Scene scene) => platformDispatcher.render(scene, this); } diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc index b986d580c080f..a8a8f35ba8be5 100644 --- a/shell/common/shell_test.cc +++ b/shell/common/shell_test.cc @@ -111,22 +111,25 @@ void ShellTest::VSyncFlush(Shell* shell, bool& will_draw_new_frame) { void ShellTest::SetViewportMetrics(Shell* shell, double width, double height) { flutter::ViewportMetrics viewport_metrics = { - 1, // device pixel ratio - width, // physical width - height, // physical height - 0, // padding top - 0, // padding right - 0, // padding bottom - 0, // padding left - 0, // view inset top - 0, // view inset right - 0, // view inset bottom - 0, // view inset left - 0, // gesture inset top - 0, // gesture inset right - 0, // gesture inset bottom - 0, // gesture inset left - 22 // physical touch slop + 1, // device pixel ratio + width, // physical width + height, // physical height + 0, // padding top + 0, // padding right + 0, // padding bottom + 0, // padding left + 0, // view inset top + 0, // view inset right + 0, // view inset bottom + 0, // view inset left + 0, // gesture inset top + 0, // gesture inset right + 0, // gesture inset bottom + 0, // gesture inset left + 22, // physical touch slop + std::vector(), // display features bounds + std::vector(), // display features type + std::vector() // display features state }; // Set viewport to nonempty, and call Animator::BeginFrame to make the layer // tree pipeline nonempty. Without either of this, the layer tree below diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index cb8e78de53840..0713860f1dc5e 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -179,6 +179,7 @@ android_java_sources = [ "io/flutter/embedding/android/SplashScreen.java", "io/flutter/embedding/android/SplashScreenProvider.java", "io/flutter/embedding/android/TransparencyMode.java", + "io/flutter/embedding/android/WindowInfoRepositoryCallbackAdapterWrapper.java", "io/flutter/embedding/engine/FlutterEngine.java", "io/flutter/embedding/engine/FlutterEngineCache.java", "io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 07d5f500f2531..3bfd377710cd6 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -6,6 +6,7 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.graphics.Insets; @@ -35,9 +36,20 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; +import androidx.core.content.ContextCompat; +import androidx.core.util.Consumer; +import androidx.window.java.layout.WindowInfoRepositoryCallbackAdapter; +import androidx.window.layout.DisplayFeature; +import androidx.window.layout.FoldingFeature; +import androidx.window.layout.FoldingFeature.OcclusionType; +import androidx.window.layout.FoldingFeature.State; +import androidx.window.layout.WindowInfoRepository; +import androidx.window.layout.WindowLayoutInfo; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.renderer.FlutterRenderer.DisplayFeatureState; +import io.flutter.embedding.engine.renderer.FlutterRenderer.DisplayFeatureType; import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.embedding.engine.systemchannels.SettingsChannel; @@ -48,7 +60,9 @@ import io.flutter.view.AccessibilityBridge; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -110,6 +124,8 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC @Nullable private AndroidTouchProcessor androidTouchProcessor; @Nullable private AccessibilityBridge accessibilityBridge; + // Provides access to foldable/hinge information + @Nullable private WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo; // Directly implemented View behavior that communicates with Flutter. private final FlutterRenderer.ViewportMetrics viewportMetrics = new FlutterRenderer.ViewportMetrics(); @@ -144,6 +160,14 @@ public void onFlutterUiNoLongerDisplayed() { } }; + private final Consumer windowInfoListener = + new Consumer() { + @Override + public void accept(WindowLayoutInfo layoutInfo) { + setWindowInfoListenerDisplayFeatures(layoutInfo); + } + }; + /** * Constructs a {@code FlutterView} programmatically, without any XML attributes. * @@ -426,6 +450,114 @@ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) sendViewportMetricsToFlutter(); } + @VisibleForTesting() + protected WindowInfoRepositoryCallbackAdapterWrapper createWindowInfoRepo() { + try { + return new WindowInfoRepositoryCallbackAdapterWrapper( + new WindowInfoRepositoryCallbackAdapter( + WindowInfoRepository.getOrCreate((Activity) getContext()))); + } catch (NoClassDefFoundError noClassDefFoundError) { + // Testing environment uses gn/javac, which does not work with aar files. This is why aar + // are converted to jar files, losing resources and other android-specific files. + // androidx.window does contain resources, which causes it to fail during testing, since the + // class androidx.window.R is not found. + // This method is mocked in the tests involving androidx.window, but this catch block is + // needed for other tests, which would otherwise fail during onAttachedToWindow(). + return null; + } + } + + /** + * Invoked when this is attached to the window. + * + *

We register for {@link androidx.window.layout.WindowInfoRepository} updates. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + this.windowInfoRepo = createWindowInfoRepo(); + if (windowInfoRepo != null) { + windowInfoRepo.addWindowLayoutInfoListener( + ContextCompat.getMainExecutor(getContext()), windowInfoListener); + } + } + + /** + * Invoked when this is detached from the window. + * + *

We unregister from {@link androidx.window.layout.WindowInfoRepository} updates. + */ + @Override + protected void onDetachedFromWindow() { + if (windowInfoRepo != null) { + windowInfoRepo.removeWindowLayoutInfoListener(windowInfoListener); + } + this.windowInfoRepo = null; + super.onDetachedFromWindow(); + } + + /** + * Refresh {@link androidx.window.layout.WindowInfoRepository} and {@link + * android.view.DisplayCutout} display features. Fold, hinge and cutout areas are populated here. + */ + @TargetApi(28) + protected void setWindowInfoListenerDisplayFeatures(WindowLayoutInfo layoutInfo) { + List displayFeatures = layoutInfo.getDisplayFeatures(); + List result = new ArrayList<>(); + + // Data from WindowInfoRepository display features. Fold and hinge areas are + // populated here. + for (DisplayFeature displayFeature : displayFeatures) { + Log.v( + TAG, + "WindowInfoRepository Display Feature reported with bounds = " + + displayFeature.getBounds().toString() + + " and type = " + + displayFeature.getClass().getSimpleName()); + if (displayFeature instanceof FoldingFeature) { + DisplayFeatureType type; + DisplayFeatureState state; + final FoldingFeature feature = (FoldingFeature) displayFeature; + if (feature.getOcclusionType() == OcclusionType.FULL) { + type = DisplayFeatureType.HINGE; + } else { + type = DisplayFeatureType.FOLD; + } + if (feature.getState() == State.FLAT) { + state = DisplayFeatureState.POSTURE_FLAT; + } else if (feature.getState() == State.HALF_OPENED) { + state = DisplayFeatureState.POSTURE_HALF_OPENED; + } else { + state = DisplayFeatureState.UNKNOWN; + } + result.add(new FlutterRenderer.DisplayFeature(displayFeature.getBounds(), type, state)); + } else { + result.add( + new FlutterRenderer.DisplayFeature( + displayFeature.getBounds(), + DisplayFeatureType.UNKNOWN, + DisplayFeatureState.UNKNOWN)); + } + } + + // Data from the DisplayCutout bounds. Cutouts for cameras and other sensors are + // populated here. DisplayCutout was introduced in API 28. + if (Build.VERSION.SDK_INT >= 28) { + WindowInsets insets = getRootWindowInsets(); + if (insets != null) { + DisplayCutout cutout = insets.getDisplayCutout(); + if (cutout != null) { + for (Rect bounds : cutout.getBoundingRects()) { + Log.v(TAG, "DisplayCutout area reported with bounds = " + bounds.toString()); + result.add(new FlutterRenderer.DisplayFeature(bounds, DisplayFeatureType.CUTOUT)); + } + } + } + } + viewportMetrics.displayFeatures = result; + sendViewportMetricsToFlutter(); + } + // TODO(garyq): Add support for notch cutout API: https://github.com/flutter/flutter/issues/56592 // Decide if we want to zero the padding of the sides. When in Landscape orientation, // android may decide to place the software navigation bars on the side. When the nav diff --git a/shell/platform/android/io/flutter/embedding/android/WindowInfoRepositoryCallbackAdapterWrapper.java b/shell/platform/android/io/flutter/embedding/android/WindowInfoRepositoryCallbackAdapterWrapper.java new file mode 100644 index 0000000000000..3d5bc3043da00 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/WindowInfoRepositoryCallbackAdapterWrapper.java @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.android; + +import androidx.core.util.Consumer; +import androidx.window.java.layout.WindowInfoRepositoryCallbackAdapter; +import androidx.window.layout.WindowLayoutInfo; +import java.util.concurrent.Executor; + +/** + * Wraps {@link WindowInfoRepositoryCallbackAdapter} in order to be able to mock it during testing. + */ +public class WindowInfoRepositoryCallbackAdapterWrapper { + + final WindowInfoRepositoryCallbackAdapter adapter; + + public WindowInfoRepositoryCallbackAdapterWrapper(WindowInfoRepositoryCallbackAdapter adapter) { + this.adapter = adapter; + } + + public void addWindowLayoutInfoListener(Executor executor, Consumer consumer) { + adapter.addWindowLayoutInfoListener(executor, consumer); + } + + public void removeWindowLayoutInfoListener(Consumer consumer) { + adapter.removeWindowLayoutInfoListener(consumer); + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index ac484f21f4601..2d49978e52e06 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -580,7 +580,10 @@ public void setViewportMetrics( int systemGestureInsetRight, int systemGestureInsetBottom, int systemGestureInsetLeft, - int physicalTouchSlop) { + int physicalTouchSlop, + int[] displayFeaturesBounds, + int[] displayFeaturesType, + int[] displayFeaturesState) { ensureRunningOnMainThread(); ensureAttachedToNative(); nativeSetViewportMetrics( @@ -600,7 +603,10 @@ public void setViewportMetrics( systemGestureInsetRight, systemGestureInsetBottom, systemGestureInsetLeft, - physicalTouchSlop); + physicalTouchSlop, + displayFeaturesBounds, + displayFeaturesType, + displayFeaturesState); } private native void nativeSetViewportMetrics( @@ -620,7 +626,10 @@ private native void nativeSetViewportMetrics( int systemGestureInsetRight, int systemGestureInsetBottom, int systemGestureInsetLeft, - int physicalTouchSlop); + int physicalTouchSlop, + int[] displayFeaturesBounds, + int[] displayFeaturesType, + int[] displayFeaturesState); // ----- End Render Surface Support ----- // ------ Start Touch Interaction Support --- diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java index f079c3614b272..d68b17bdf7c44 100644 --- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -6,6 +6,7 @@ import android.annotation.TargetApi; import android.graphics.Bitmap; +import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.os.Build; import android.os.Handler; @@ -16,6 +17,8 @@ import io.flutter.embedding.engine.FlutterJNI; import io.flutter.view.TextureRegistry; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; /** @@ -326,7 +329,23 @@ public void setViewportMetrics(@NonNull ViewportMetrics viewportMetrics) { + ", R: " + viewportMetrics.systemGestureInsetRight + ", B: " - + viewportMetrics.viewInsetBottom); + + viewportMetrics.systemGestureInsetRight + + "\n" + + "Display Features: " + + viewportMetrics.displayFeatures.size()); + + int[] displayFeaturesBounds = new int[viewportMetrics.displayFeatures.size() * 4]; + int[] displayFeaturesType = new int[viewportMetrics.displayFeatures.size()]; + int[] displayFeaturesState = new int[viewportMetrics.displayFeatures.size()]; + for (int i = 0; i < viewportMetrics.displayFeatures.size(); i++) { + DisplayFeature displayFeature = viewportMetrics.displayFeatures.get(i); + displayFeaturesBounds[4 * i] = displayFeature.bounds.left; + displayFeaturesBounds[4 * i + 1] = displayFeature.bounds.top; + displayFeaturesBounds[4 * i + 2] = displayFeature.bounds.right; + displayFeaturesBounds[4 * i + 3] = displayFeature.bounds.bottom; + displayFeaturesType[i] = displayFeature.type.encodedValue; + displayFeaturesState[i] = displayFeature.state.encodedValue; + } flutterJNI.setViewportMetrics( viewportMetrics.devicePixelRatio, @@ -344,7 +363,10 @@ public void setViewportMetrics(@NonNull ViewportMetrics viewportMetrics) { viewportMetrics.systemGestureInsetRight, viewportMetrics.systemGestureInsetBottom, viewportMetrics.systemGestureInsetLeft, - viewportMetrics.physicalTouchSlop); + viewportMetrics.physicalTouchSlop, + displayFeaturesBounds, + displayFeaturesType, + displayFeaturesState); } // TODO(mattcarroll): describe the native behavior that this invokes @@ -429,5 +451,103 @@ public static final class ViewportMetrics { boolean validate() { return width > 0 && height > 0 && devicePixelRatio > 0; } + + public List displayFeatures = new ArrayList(); + } + + /** + * Description of a physical feature on the display. + * + *

A display feature is a distinctive physical attribute located within the display panel of + * the device. It can intrude into the application window space and create a visual distortion, + * visual or touch discontinuity, make some area invisible or create a logical divider or + * separation in the screen space. + * + *

Based on {@link androidx.window.layout.DisplayFeature}, with added support for cutouts. + */ + public static final class DisplayFeature { + public final Rect bounds; + public final DisplayFeatureType type; + public final DisplayFeatureState state; + + public DisplayFeature(Rect bounds, DisplayFeatureType type, DisplayFeatureState state) { + this.bounds = bounds; + this.type = type; + this.state = state; + } + + public DisplayFeature(Rect bounds, DisplayFeatureType type) { + this.bounds = bounds; + this.type = type; + this.state = DisplayFeatureState.UNKNOWN; + } + } + + /** + * Types of display features that can appear on the viewport. + * + *

Some, like {@link #FOLD}, can be reported without actually occluding the screen. They are + * useful for knowing where the display is bent or has a crease. The {@link DisplayFeature#bounds} + * can be 0-width in such cases. + */ + public enum DisplayFeatureType { + /** + * Type of display feature not yet known to Flutter. This can happen if WindowManager is updated + * with new types. The {@link DisplayFeature#bounds} is the only known property. + */ + UNKNOWN(0), + + /** + * A fold in the flexible display that does not occlude the screen. Corresponds to {@link + * androidx.window.layout.FoldingFeature.OcclusionType#NONE} + */ + FOLD(1), + + /** + * Splits the display in two separate panels that can fold. Occludes the screen. Corresponds to + * {@link androidx.window.layout.FoldingFeature.OcclusionType#FULL} + */ + HINGE(2), + + /** + * Area of the screen that usually houses cameras or sensors. Occludes the screen. Corresponds + * to {@link android.view.DisplayCutout} + */ + CUTOUT(3); + + public final int encodedValue; + + DisplayFeatureType(int encodedValue) { + this.encodedValue = encodedValue; + } + } + + /** + * State of the display feature. + * + *

For foldables, the state is the posture. For cutouts, this property is {@link #UNKNOWN} + */ + public enum DisplayFeatureState { + /** The display feature is a cutout or this state is new and not yet known to Flutter. */ + UNKNOWN(0), + + /** + * The foldable device is completely open. The screen space that is presented to the user is + * flat. Corresponds to {@link androidx.window.layout.FoldingFeature.State#FLAT} + */ + POSTURE_FLAT(1), + + /** + * The foldable device's hinge is in an intermediate position between opened and closed state. + * There is a non-flat angle between parts of the flexible screen or between physical display + * panels. Corresponds to {@link androidx.window.layout.FoldingFeature.State#HALF_OPENED} + */ + POSTURE_HALF_OPENED(2); + + public final int encodedValue; + + DisplayFeatureState(int encodedValue) { + this.encodedValue = encodedValue; + } } } diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 317ec6562a1ca..d23d6f4158e95 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -745,7 +745,10 @@ private void updateViewportMetrics() { mMetrics.systemGestureInsetRight, mMetrics.systemGestureInsetBottom, mMetrics.systemGestureInsetLeft, - mMetrics.physicalTouchSlop); + mMetrics.physicalTouchSlop, + new int[0], + new int[0], + new int[0]); } // Called by FlutterNativeView to notify first Flutter frame rendered. diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 8b8494b3d3a6d..89d7262dbb1e2 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -281,7 +281,28 @@ static void SetViewportMetrics(JNIEnv* env, jint systemGestureInsetRight, jint systemGestureInsetBottom, jint systemGestureInsetLeft, - jint physicalTouchSlop) { + jint physicalTouchSlop, + jintArray javaDisplayFeaturesBounds, + jintArray javaDisplayFeaturesType, + jintArray javaDisplayFeaturesState) { + // Convert java->c++. javaDisplayFeaturesBounds, javaDisplayFeaturesType and + // javaDisplayFeaturesState cannot be null + jsize rectSize = env->GetArrayLength(javaDisplayFeaturesBounds); + std::vector boundsIntVector(rectSize); + env->GetIntArrayRegion(javaDisplayFeaturesBounds, 0, rectSize, + &boundsIntVector[0]); + std::vector displayFeaturesBounds(boundsIntVector.begin(), + boundsIntVector.end()); + jsize typeSize = env->GetArrayLength(javaDisplayFeaturesType); + std::vector displayFeaturesType(typeSize); + env->GetIntArrayRegion(javaDisplayFeaturesType, 0, typeSize, + &displayFeaturesType[0]); + + jsize stateSize = env->GetArrayLength(javaDisplayFeaturesState); + std::vector displayFeaturesState(stateSize); + env->GetIntArrayRegion(javaDisplayFeaturesState, 0, stateSize, + &displayFeaturesState[0]); + const flutter::ViewportMetrics metrics{ static_cast(devicePixelRatio), static_cast(physicalWidth), @@ -299,6 +320,9 @@ static void SetViewportMetrics(JNIEnv* env, static_cast(systemGestureInsetBottom), static_cast(systemGestureInsetLeft), static_cast(physicalTouchSlop), + displayFeaturesBounds, + displayFeaturesType, + displayFeaturesState, }; ANDROID_SHELL_HOLDER->GetPlatformView()->SetViewportMetrics(metrics); @@ -700,7 +724,7 @@ bool RegisterApi(JNIEnv* env) { }, { .name = "nativeSetViewportMetrics", - .signature = "(JFIIIIIIIIIIIIIII)V", + .signature = "(JFIIIIIIIIIIIIIII[I[I[I)V", .fnPtr = reinterpret_cast(&SetViewportMetrics), }, { diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index 0458ec29d8ca7..87660ded83dbf 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -6,6 +6,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -14,11 +15,13 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Insets; +import android.graphics.Rect; import android.graphics.Region; import android.hardware.HardwareBuffer; import android.media.Image; @@ -32,6 +35,9 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.widget.FrameLayout; +import androidx.core.util.Consumer; +import androidx.window.layout.FoldingFeature; +import androidx.window.layout.WindowLayoutInfo; import io.flutter.TestUtils; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; @@ -40,6 +46,7 @@ import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.plugin.platform.PlatformViewsController; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; @@ -50,6 +57,7 @@ import org.mockito.Spy; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.Shadows; @@ -74,7 +82,7 @@ public void setUp() { @Test public void attachToFlutterEngine_alertsPlatformViews() { - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); FlutterEngine flutterEngine = spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController); @@ -86,7 +94,7 @@ public void attachToFlutterEngine_alertsPlatformViews() { @Test public void detachFromFlutterEngine_alertsPlatformViews() { - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); FlutterEngine flutterEngine = spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController); @@ -99,7 +107,7 @@ public void detachFromFlutterEngine_alertsPlatformViews() { @Test public void detachFromFlutterEngine_turnsOffA11y() { - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); FlutterEngine flutterEngine = spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); @@ -129,7 +137,7 @@ public void detachFromFlutterEngine_revertImageView() { @Test public void onConfigurationChanged_fizzlesWhenNullEngine() { - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); FlutterEngine flutterEngine = spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); @@ -154,7 +162,7 @@ public void itSendsLightPlatformBrightnessToFlutter() { new AtomicReference<>(); // FYI - The default brightness is LIGHT, which is why we don't need to configure it. - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); FlutterEngine flutterEngine = spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); @@ -192,7 +200,7 @@ public void itSendsDarkPlatformBrightnessToFlutter() { AtomicReference reportedBrightness = new AtomicReference<>(); - Context spiedContext = spy(RuntimeEnvironment.application); + Context spiedContext = spy(Robolectric.setupActivity(Activity.class)); Resources spiedResources = spy(spiedContext.getResources()); when(spiedContext.getResources()).thenReturn(spiedResources); @@ -244,7 +252,7 @@ public SettingsChannel.MessageBuilder answer(InvocationOnMock invocation) FlutterViewTest.ShadowFullscreenViewGroup.class }) public void setPaddingTopToZeroForFullscreenMode() { - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); FlutterEngine flutterEngine = spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); @@ -284,7 +292,7 @@ public void setPaddingTopToZeroForFullscreenMode() { FlutterViewTest.ShadowFullscreenViewGroup.class }) public void setPaddingTopToZeroForFullscreenModeLegacy() { - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); FlutterEngine flutterEngine = spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); @@ -316,7 +324,7 @@ public void setPaddingTopToZeroForFullscreenModeLegacy() { @Config(sdk = 30) public void reportSystemInsetWhenNotFullscreen() { // Without custom shadows, the default system ui visibility flags is 0. - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); assertEquals(0, flutterView.getSystemUiVisibility()); FlutterEngine flutterEngine = @@ -355,7 +363,7 @@ public void reportSystemInsetWhenNotFullscreen() { @Config(sdk = 28) public void reportSystemInsetWhenNotFullscreenLegacy() { // Without custom shadows, the default system ui visibility flags is 0. - FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); assertEquals(0, flutterView.getSystemUiVisibility()); FlutterEngine flutterEngine = @@ -625,6 +633,84 @@ public void systemInsetDisplayCutoutSimple() { assertEquals(100, viewportMetricsCaptor.getValue().viewInsetTop); } + @Test + public void itRegistersAndUnregistersToWindowManager() { + Context context = Robolectric.setupActivity(Activity.class); + FlutterView flutterView = spy(new FlutterView(context)); + ShadowDisplay display = + Shadows.shadowOf( + ((WindowManager) + RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay()); + WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = + mock(WindowInfoRepositoryCallbackAdapterWrapper.class); + // For reasoning behing using doReturn instead of when, read "Important gotcha" at + // https://www.javadoc.io/doc/org.mockito/mockito-core/1.10.19/org/mockito/Mockito.html#13 + doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); + + // When a new FlutterView is attached to the window + flutterView.onAttachedToWindow(); + + // Then the WindowManager callback is registered + verify(windowInfoRepo, times(1)).addWindowLayoutInfoListener(any(), any()); + + // When the FlutterView is detached from the window + flutterView.onDetachedFromWindow(); + + // Then the WindowManager callback is unregistered + verify(windowInfoRepo, times(1)).removeWindowLayoutInfoListener(any()); + } + + @Test + public void itSendsHingeDisplayFeatureToFlutter() { + Context context = Robolectric.setupActivity(Activity.class); + FlutterView flutterView = spy(new FlutterView(context)); + ShadowDisplay display = + Shadows.shadowOf( + ((WindowManager) + RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay()); + when(flutterView.getContext()).thenReturn(context); + WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = + mock(WindowInfoRepositoryCallbackAdapterWrapper.class); + doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); + FlutterEngine flutterEngine = + spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni)); + FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); + when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + + FoldingFeature displayFeature = mock(FoldingFeature.class); + when(displayFeature.getBounds()).thenReturn(new Rect(0, 0, 100, 100)); + when(displayFeature.getOcclusionType()).thenReturn(FoldingFeature.OcclusionType.FULL); + when(displayFeature.getState()).thenReturn(FoldingFeature.State.FLAT); + + WindowLayoutInfo testWindowLayout = new WindowLayoutInfo(Arrays.asList(displayFeature)); + + // When FlutterView is attached to the engine and window, and a hinge display feature exists + flutterView.attachToFlutterEngine(flutterEngine); + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(Arrays.asList(), viewportMetricsCaptor.getValue().displayFeatures); + flutterView.onAttachedToWindow(); + ArgumentCaptor> wmConsumerCaptor = + ArgumentCaptor.forClass((Class) Consumer.class); + verify(windowInfoRepo).addWindowLayoutInfoListener(any(), wmConsumerCaptor.capture()); + Consumer wmConsumer = wmConsumerCaptor.getValue(); + wmConsumer.accept(testWindowLayout); + + // Then the Renderer receives the display feature + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals( + FlutterRenderer.DisplayFeatureType.HINGE, + viewportMetricsCaptor.getValue().displayFeatures.get(0).type); + assertEquals( + FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, + viewportMetricsCaptor.getValue().displayFeatures.get(0).state); + assertEquals( + new Rect(0, 0, 100, 100), viewportMetricsCaptor.getValue().displayFeatures.get(0).bounds); + } + @Test public void flutterImageView_acquiresImageAndInvalidates() { final ImageReader mockReader = mock(ImageReader.class); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index f2e22a06e78f8..23b0aaa9a1876 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -1,6 +1,9 @@ package io.flutter.embedding.engine.renderer; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -8,6 +11,7 @@ import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; +import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.os.Looper; import android.view.Surface; @@ -17,6 +21,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -219,4 +224,65 @@ public void run() { // do nothing } } + + @Test + public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { + // Setup the test. + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer.ViewportMetrics metrics = new FlutterRenderer.ViewportMetrics(); + metrics.width = 1000; + metrics.height = 1000; + metrics.devicePixelRatio = 2; + metrics.displayFeatures.add( + new FlutterRenderer.DisplayFeature( + new Rect(10, 20, 30, 40), + FlutterRenderer.DisplayFeatureType.FOLD, + FlutterRenderer.DisplayFeatureState.POSTURE_HALF_OPENED)); + metrics.displayFeatures.add( + new FlutterRenderer.DisplayFeature( + new Rect(50, 60, 70, 80), FlutterRenderer.DisplayFeatureType.CUTOUT)); + + // Execute the behavior under test. + flutterRenderer.setViewportMetrics(metrics); + + // Verify behavior under test. + ArgumentCaptor boundsCaptor = ArgumentCaptor.forClass(int[].class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(int[].class); + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(int[].class); + verify(fakeFlutterJNI) + .setViewportMetrics( + anyFloat(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + boundsCaptor.capture(), + typeCaptor.capture(), + stateCaptor.capture()); + + assertArrayEquals(new int[] {10, 20, 30, 40, 50, 60, 70, 80}, boundsCaptor.getValue()); + assertArrayEquals( + new int[] { + FlutterRenderer.DisplayFeatureType.FOLD.encodedValue, + FlutterRenderer.DisplayFeatureType.CUTOUT.encodedValue + }, + typeCaptor.getValue()); + assertArrayEquals( + new int[] { + FlutterRenderer.DisplayFeatureState.POSTURE_HALF_OPENED.encodedValue, + FlutterRenderer.DisplayFeatureState.UNKNOWN.encodedValue + }, + stateCaptor.getValue()); + } } diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 0c858342a35d7..1f1cfbf47a64a 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -18,6 +18,7 @@ import static org.mockito.Mockito.when; import android.annotation.TargetApi; +import android.app.Activity; import android.content.Context; import android.content.res.AssetManager; import android.graphics.Insets; @@ -67,6 +68,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -1439,7 +1441,7 @@ public void autofill_onProvideVirtualViewStructure() { return; } - FlutterView testView = new FlutterView(RuntimeEnvironment.application); + FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); @@ -1526,7 +1528,7 @@ public void autofill_onProvideVirtualViewStructure_single() { return; } - FlutterView testView = new FlutterView(RuntimeEnvironment.application); + FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); @@ -1578,7 +1580,7 @@ public void autofill_testLifeCycle() { TestAfm testAfm = Shadow.extract(RuntimeEnvironment.application.getSystemService(AutofillManager.class)); - FlutterView testView = new FlutterView(RuntimeEnvironment.application); + FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); @@ -1709,7 +1711,7 @@ public void autofill_testAutofillUpdatesTheFramework() { TestAfm testAfm = Shadow.extract(RuntimeEnvironment.application.getSystemService(AutofillManager.class)); - FlutterView testView = new FlutterView(RuntimeEnvironment.application); + FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); @@ -1803,7 +1805,7 @@ public void autofill_testSetTextIpnutClientUpdatesSideFields() { TestAfm testAfm = Shadow.extract(RuntimeEnvironment.application.getSystemService(AutofillManager.class)); - FlutterView testView = new FlutterView(RuntimeEnvironment.application); + FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); @@ -1967,7 +1969,7 @@ public void sendAppPrivateCommand_hasData() throws JSONException { @TargetApi(30) @Config(sdk = 30) public void ime_windowInsetsSync() { - FlutterView testView = new FlutterView(RuntimeEnvironment.application); + FlutterView testView = new FlutterView(Robolectric.setupActivity(Activity.class)); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); diff --git a/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java b/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java index 60c275bd1ec49..72659ec0193a0 100644 --- a/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.verify; import android.annotation.TargetApi; +import android.app.Activity; import android.view.PointerIcon; import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.engine.dart.DartExecutor; @@ -18,8 +19,8 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @Config( @@ -31,7 +32,7 @@ public class MouseCursorPluginTest { @Test public void mouseCursorPlugin_SetsSystemCursorOnRequest() throws JSONException { // Initialize a general MouseCursorPlugin. - FlutterView testView = spy(new FlutterView(RuntimeEnvironment.application)); + FlutterView testView = spy(new FlutterView(Robolectric.setupActivity(Activity.class))); MouseCursorChannel mouseCursorChannel = new MouseCursorChannel(mock(DartExecutor.class)); MouseCursorPlugin mouseCursorPlugin = new MouseCursorPlugin(testView, mouseCursorChannel); diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 38c919cae33dc..c29506167a03f 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -985,7 +985,10 @@ public void setViewportMetrics( int systemGestureInsetRight, int systemGestureInsetBottom, int systemGestureInsetLeft, - int physicalTouchSlop) {} + int physicalTouchSlop, + int[] displayFeaturesBounds, + int[] displayFeaturesType, + int[] displayFeaturesState) {} @Implementation public void invokePlatformMessageResponseCallback( diff --git a/shell/platform/android/test_runner/build.gradle b/shell/platform/android/test_runner/build.gradle index 688014acbd283..0fe346a39a1a8 100644 --- a/shell/platform/android/test_runner/build.gradle +++ b/shell/platform/android/test_runner/build.gradle @@ -43,6 +43,7 @@ android { testImplementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" testImplementation "androidx.test:core:1.4.0" testImplementation "androidx.tracing:tracing:1.0.0" + testImplementation "androidx.window:window-java:1.0.0-beta03" testImplementation "com.google.android.play:core:1.8.0" testImplementation "com.ibm.icu:icu4j:69.1" testImplementation "org.mockito:mockito-core:3.11.2" diff --git a/shell/platform/fuchsia/flutter/flatland_platform_view.cc b/shell/platform/fuchsia/flutter/flatland_platform_view.cc index 64d8e0dfba7ae..998c3939b0c9e 100644 --- a/shell/platform/fuchsia/flutter/flatland_platform_view.cc +++ b/shell/platform/fuchsia/flutter/flatland_platform_view.cc @@ -91,6 +91,9 @@ void FlatlandPlatformView::OnGetLayout( 0.0f, // p_physical_system_gesture_inset_bottom 0.0f, // p_physical_system_gesture_inset_left, -1.0, // p_physical_touch_slop, + {}, // p_physical_display_features_bounds + {}, // p_physical_display_features_type + {}, // p_physical_display_features_state }); parent_viewport_watcher_->GetLayout( diff --git a/shell/platform/fuchsia/flutter/gfx_platform_view.cc b/shell/platform/fuchsia/flutter/gfx_platform_view.cc index 305e2f6fa523f..ca0828f283c6b 100644 --- a/shell/platform/fuchsia/flutter/gfx_platform_view.cc +++ b/shell/platform/fuchsia/flutter/gfx_platform_view.cc @@ -253,6 +253,9 @@ void GfxPlatformView::OnScenicEvent( 0.0f, // p_physical_system_gesture_inset_bottom 0.0f, // p_physical_system_gesture_inset_left, -1.0, // p_physical_touch_slop, + {}, // p_physical_display_features_bounds + {}, // p_physical_display_features_type + {}, // p_physical_display_features_state }); } } diff --git a/tools/androidx/files.json b/tools/androidx/files.json index f39538de3cc71..07410fb2988cd 100644 --- a/tools/androidx/files.json +++ b/tools/androidx/files.json @@ -65,5 +65,19 @@ "provides": [ "androidx.core.view.WindowInsetsControllerCompat" ] + }, + { + "url": "https://maven.google.com/androidx/window/window-java/1.0.0-beta03/window-java-1.0.0-beta03.aar", + "out_file_name": "androidx_window_java.aar", + "maven_dependency": "androidx.window:window-java:1.0.0-beta03", + "provides": [ + "androidx.window.java.layout.WindowInfoRepositoryCallbackAdapter", + "androidx.window.layout.DisplayFeature", + "androidx.window.layout.FoldingFeature", + "androidx.window.layout.FoldingFeature.OcclusionType", + "androidx.window.layout.FoldingFeature.State", + "androidx.window.layout.WindowLayoutInfo", + "androidx.window.layout.WindowInfoRepository" + ] } ] diff --git a/tools/cipd/android_embedding_bundle/build.gradle b/tools/cipd/android_embedding_bundle/build.gradle index 4b1392cefbc43..73388cc23daa7 100644 --- a/tools/cipd/android_embedding_bundle/build.gradle +++ b/tools/cipd/android_embedding_bundle/build.gradle @@ -44,6 +44,7 @@ android { embedding "androidx.core:core:1.6.0" embedding "androidx.fragment:fragment:1.1.0" embedding "androidx.tracing:tracing:1.0.0" + embedding "androidx.window:window-java:1.0.0-beta03" def lifecycle_version = "2.2.0" embedding "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"