diff --git a/dev/integration_tests/display_cutout_rotation/.gitignore b/dev/integration_tests/display_cutout_rotation/.gitignore
new file mode 100644
index 00000000000..79c113f9b50
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/dev/integration_tests/display_cutout_rotation/.metadata b/dev/integration_tests/display_cutout_rotation/.metadata
new file mode 100644
index 00000000000..ae37682597f
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "0dc4eb31df6fe16c1bac10bef3904eb378056c35"
+ channel: "[user-branch]"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 0dc4eb31df6fe16c1bac10bef3904eb378056c35
+ base_revision: 0dc4eb31df6fe16c1bac10bef3904eb378056c35
+ - platform: android
+ create_revision: 0dc4eb31df6fe16c1bac10bef3904eb378056c35
+ base_revision: 0dc4eb31df6fe16c1bac10bef3904eb378056c35
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/dev/integration_tests/display_cutout_rotation/README.md b/dev/integration_tests/display_cutout_rotation/README.md
new file mode 100644
index 00000000000..7f3c5627b51
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/README.md
@@ -0,0 +1,3 @@
+# display_cutout_rotation
+
+To run test locally use `flutter drive integration_test/display_cutout_test.dart` from this folder.
diff --git a/dev/integration_tests/display_cutout_rotation/analysis_options.yaml b/dev/integration_tests/display_cutout_rotation/analysis_options.yaml
new file mode 100644
index 00000000000..c4a47172d2b
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/analysis_options.yaml
@@ -0,0 +1,5 @@
+include: ../../analysis_options.yaml
+
+analyzer:
+ exclude:
+ - build/**
diff --git a/dev/integration_tests/display_cutout_rotation/android/.gitignore b/dev/integration_tests/display_cutout_rotation/android/.gitignore
new file mode 100644
index 00000000000..55afd919c65
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/.gitignore
@@ -0,0 +1,13 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/build.gradle.kts b/dev/integration_tests/display_cutout_rotation/android/app/build.gradle.kts
new file mode 100644
index 00000000000..f966b60fa9a
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/build.gradle.kts
@@ -0,0 +1,43 @@
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.example.display_cutout_rotation"
+ compileSdk = flutter.compileSdkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.example.display_cutout_rotation"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/debug/AndroidManifest.xml b/dev/integration_tests/display_cutout_rotation/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000000..e00f903eae2
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/AndroidManifest.xml b/dev/integration_tests/display_cutout_rotation/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..1b6f4c2f75c
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/java/com/example/display_cutout_rotation/MainActivity.kt b/dev/integration_tests/display_cutout_rotation/android/app/src/main/java/com/example/display_cutout_rotation/MainActivity.kt
new file mode 100644
index 00000000000..61e46e72d97
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/src/main/java/com/example/display_cutout_rotation/MainActivity.kt
@@ -0,0 +1,42 @@
+// Copyright 2014 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.
+
+@file:Suppress("PackageName")
+
+package com.example.display_cutout_rotation
+
+import android.os.Build
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // https://developer.android.com/training/system-ui
+ // Set app into fullscreen mode without insets from system bars.
+ // Matches api 35 default behavior and is required by test which assumes no other inset
+ // except for a cutout.
+ val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
+ windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
+
+ // The default behavior on SDK level 34 and below is for display cutouts to be consumed
+ // before the insets would reach the engine. In order to receive the display cutouts in the
+ // engine, the test app must request that it be allowed to draw its content behind cutouts.
+ // See
+ // https://developer.android.com/reference/android/view/WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ // LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS was added in api 30 so we need to check api level
+ // before setting the value. Not setting this value will prevent flutter from drawing in
+ // cutout areas which our test is explicitly requires.
+ if (Build.VERSION.SDK_INT >= 30) {
+ window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ }
+ }
+}
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/drawable-v21/launch_background.xml b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 00000000000..9f19e2f9040
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/drawable/launch_background.xml b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 00000000000..3727f9e00a0
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000000..db77bb4b7b0
Binary files /dev/null and b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000000..17987b79bb8
Binary files /dev/null and b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000000..09d4391482b
Binary files /dev/null and b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..d5f1c8d34e7
Binary files /dev/null and b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..4d6372eebdb
Binary files /dev/null and b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/values-night/styles.xml b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000000..f3ab3e83cd3
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/values/styles.xml b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000000..9a0ead3c04f
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev/integration_tests/display_cutout_rotation/android/app/src/profile/AndroidManifest.xml b/dev/integration_tests/display_cutout_rotation/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 00000000000..e00f903eae2
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/dev/integration_tests/display_cutout_rotation/android/build.gradle.kts b/dev/integration_tests/display_cutout_rotation/android/build.gradle.kts
new file mode 100644
index 00000000000..dbee657bb5b
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/dev/integration_tests/display_cutout_rotation/android/gradle.properties b/dev/integration_tests/display_cutout_rotation/android/gradle.properties
new file mode 100644
index 00000000000..f018a61817f
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/dev/integration_tests/display_cutout_rotation/android/gradle/wrapper/gradle-wrapper.properties b/dev/integration_tests/display_cutout_rotation/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000000..afa1e8eb0a8
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
diff --git a/dev/integration_tests/display_cutout_rotation/android/settings.gradle.kts b/dev/integration_tests/display_cutout_rotation/android/settings.gradle.kts
new file mode 100644
index 00000000000..ead4a0bbc60
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.7.0" apply false
+ id("org.jetbrains.kotlin.android") version "1.8.22" apply false
+}
+
+include(":app")
diff --git a/dev/integration_tests/display_cutout_rotation/integration_test/display_cutout_test.dart b/dev/integration_tests/display_cutout_rotation/integration_test/display_cutout_test.dart
new file mode 100644
index 00000000000..cbbc711b6dd
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/integration_test/display_cutout_test.dart
@@ -0,0 +1,131 @@
+// Copyright 2014 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.
+
+import 'dart:ui';
+
+import 'package:display_cutout_rotation/main.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ group('end-to-end test', () {
+ // Test assumes a custom driver that enables
+ // "com.android.internal.display.cutout.emulation.tall".
+ testWidgets('cutout should be on top in portrait mode', (WidgetTester tester) async {
+ // Force rotation
+ await setOrientationAndWaitUntilRotation(tester, DeviceOrientation.portraitUp);
+ // Load app widget.
+ await tester.pumpWidget(const MyApp());
+ final BuildContext context = tester.element(find.byType(Text));
+ final Iterable displayFeatures = getCutouts(tester, context);
+ // Test is expecting one cutout setup in the test harness.
+ expect(displayFeatures.length, 1, reason: 'Single cutout display feature expected');
+ // Verify that app code thinks there is a top cutout.
+ expect(
+ displayFeatures.first.bounds.top,
+ 0,
+ reason:
+ 'cutout should start at the top, does the test device have a '
+ 'camera cutout or window inset?',
+ );
+ });
+
+ testWidgets('cutout should be on left in landscape left', (WidgetTester tester) async {
+ await setOrientationAndWaitUntilRotation(tester, DeviceOrientation.landscapeLeft);
+ // Load app widget.
+ await tester.pumpWidget(const MyApp());
+ final BuildContext context = tester.element(find.byType(Text));
+ // Verify that app code thinks there is a left cutout.
+ final Iterable displayFeatures = getCutouts(tester, context);
+
+ // Test is expecting one cutout setup in the test harness.
+ expect(displayFeatures.length, 1, reason: 'Single cutout display feature expected');
+ expect(
+ displayFeatures.first.bounds.left,
+ 0,
+ reason:
+ 'cutout should start at the left, does the test device have a '
+ 'camera cutout or window inset?',
+ );
+ });
+
+ testWidgets('cutout handles rotation', (WidgetTester tester) async {
+ await setOrientationAndWaitUntilRotation(tester, DeviceOrientation.portraitUp);
+ const MyApp widgetUnderTest = MyApp();
+ // Load app widget.
+ await tester.pumpWidget(widgetUnderTest);
+ BuildContext context = tester.element(find.byType(Text));
+ Iterable displayFeatures = getCutouts(tester, context);
+ // Test is expecting one cutout setup in the test harness.
+ expect(displayFeatures.length, 1, reason: 'Single cutout display feature expected');
+ // Verify that app code thinks there is a top cutout.
+ expect(
+ displayFeatures.first.bounds.top,
+ 0,
+ reason:
+ 'cutout should start at the top, does the test device have a '
+ 'camera cutout or window inset?',
+ );
+ await setOrientationAndWaitUntilRotation(tester, DeviceOrientation.landscapeLeft);
+ await tester.pumpWidget(widgetUnderTest);
+
+ // Requery for display features after rotation.
+ context = tester.element(find.byType(Text));
+ displayFeatures = getCutouts(tester, context);
+ // Test is expecting one cutout setup in the test harness.
+ expect(displayFeatures.length, 1, reason: 'Single cutout display feature expected');
+ expect(
+ displayFeatures.first.bounds.left,
+ 0,
+ reason: 'cutout should start at the left or handle camera',
+ );
+ });
+
+ tearDown(() {
+ // After each test reset to device perfered orientations to avoid
+ // test pollution.
+ SystemChrome.setPreferredOrientations([]);
+ });
+ });
+}
+
+/*
+ * Force rotation then poll to ensure rotation has happened.
+ *
+ * Rotations have an async communication to engine which then has an async
+ * communication to the android operating system.
+ */
+Future setOrientationAndWaitUntilRotation(
+ WidgetTester tester,
+ DeviceOrientation orientation,
+) async {
+ SystemChrome.setPreferredOrientations([orientation]);
+ Orientation expectedOrientation;
+ switch (orientation) {
+ case DeviceOrientation.portraitUp:
+ case DeviceOrientation.portraitDown:
+ expectedOrientation = Orientation.portrait;
+ case DeviceOrientation.landscapeRight:
+ case DeviceOrientation.landscapeLeft:
+ expectedOrientation = Orientation.landscape;
+ }
+ while (true) {
+ final BuildContext context = tester.element(find.byType(Text));
+ if (expectedOrientation == MediaQuery.of(context).orientation) {
+ break;
+ }
+ await tester.pumpAndSettle();
+ }
+}
+
+Iterable getCutouts(WidgetTester tester, BuildContext context) {
+ final List displayFeatures = MediaQuery.of(context).displayFeatures;
+ return displayFeatures.where(
+ (DisplayFeature feature) => feature.type == DisplayFeatureType.cutout,
+ );
+}
diff --git a/dev/integration_tests/display_cutout_rotation/lib/main.dart b/dev/integration_tests/display_cutout_rotation/lib/main.dart
new file mode 100644
index 00000000000..91f3a8ba03c
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/lib/main.dart
@@ -0,0 +1,50 @@
+// Copyright 2014 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.
+
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(const MyApp());
+}
+
+final class MyApp extends StatefulWidget {
+ const MyApp({super.key});
+
+ @override
+ State createState() => _MyAppState();
+}
+
+class _MyAppState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final List displayFeatures = MediaQuery.of(context).displayFeatures;
+ displayFeatures.retainWhere(
+ (DisplayFeature feature) => feature.type == DisplayFeatureType.cutout,
+ );
+ String text;
+ // None of this complexity is required for the test but it helps when
+ // visually debugging or watching a video of a remote device.
+ if (displayFeatures.isEmpty) {
+ text = 'CutoutNone';
+ } else if (displayFeatures.length > 1) {
+ text = 'CutoutMany';
+ } else {
+ final Rect cutout = displayFeatures[0].bounds;
+ if (cutout.top == 0) {
+ text = 'CutoutTop';
+ } else if (cutout.left == 0) {
+ text = 'CutoutLeft';
+ } else {
+ text = 'CutoutNeither';
+ }
+ }
+ // Tests assume there is some text element displayed.
+ return MaterialApp(
+ debugShowCheckedModeBanner: false,
+ home: Text('Cutout status: $text', key: Key(text)),
+ );
+ }
+}
diff --git a/dev/integration_tests/display_cutout_rotation/pubspec.yaml b/dev/integration_tests/display_cutout_rotation/pubspec.yaml
new file mode 100644
index 00000000000..6dce77262dc
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/pubspec.yaml
@@ -0,0 +1,123 @@
+name: display_cutout_rotation
+description: "A new Flutter project."
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+# In Windows, build-name is used as the major, minor, and patch parts
+# of the product and file versions while build-number is used as the build suffix.
+version: 1.0.0+1
+
+environment:
+ sdk: ^3.7.0-0
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+ flutter:
+ sdk: flutter
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: 1.0.8
+
+ characters: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ collection: 1.19.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ material_color_utilities: 0.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ meta: 1.16.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+dev_dependencies:
+ flutter_driver:
+ sdk: flutter
+ flutter_test:
+ sdk: flutter
+
+ # The "flutter_lints" package below contains a set of recommended lints to
+ # encourage good coding practices. The lint set provided by the package is
+ # activated in the `analysis_options.yaml` file located at the root of your
+ # package. See that file for information about deactivating specific lint
+ # rules and activating additional ones.
+ flutter_lints: 5.0.0
+ integration_test:
+ sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+
+ async: 2.12.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ boolean_selector: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ clock: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ fake_async: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ file: 7.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ leak_tracker: 10.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ leak_tracker_flutter_testing: 3.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ leak_tracker_testing: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ lints: 5.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ matcher: 0.12.17 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ path: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ source_span: 1.10.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ stack_trace: 1.12.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ stream_channel: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ string_scanner: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ term_glyph: 1.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ test_api: 0.7.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ vm_service: 14.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ webdriver: 3.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+flutter:
+
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/to/resolution-aware-images
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/to/asset-from-package
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/to/font-from-package
+
+# PUBSPEC CHECKSUM: 7b61
diff --git a/dev/integration_tests/display_cutout_rotation/test_driver/display_cutout_test_test.dart b/dev/integration_tests/display_cutout_rotation/test_driver/display_cutout_test_test.dart
new file mode 100644
index 00000000000..e8ca40aa998
--- /dev/null
+++ b/dev/integration_tests/display_cutout_rotation/test_driver/display_cutout_test_test.dart
@@ -0,0 +1,106 @@
+// Copyright 2014 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.
+
+// ignore_for_file: avoid_print
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter_driver/flutter_driver.dart';
+
+// display_cutout needs a custom driver becuase cutout manipulations needs to be
+// done to a device/emulator in order for the tests to pass.
+Future main() async {
+ if (!(Platform.isLinux || Platform.isMacOS)) {
+ // Not a fundemental limitation, developer shortcut.
+ print('This test must be run on a POSIX host. Skipping...');
+ return;
+ }
+ final bool adbExists = Process.runSync('which', ['adb']).exitCode == 0;
+ if (!adbExists) {
+ print(r'This test needs ADB to exist on the $PATH.');
+ exitCode = 1;
+ return;
+ }
+ // Test requires developer settings added in 28 and behavior added in 30
+ final ProcessResult checkApiLevel = Process.runSync('adb', [
+ 'shell',
+ 'getprop',
+ 'ro.build.version.sdk',
+ ]);
+ final String apiStdout = checkApiLevel.stdout.toString();
+ // Api level 30 or higher.
+ if (apiStdout.startsWith('2') || apiStdout.startsWith('1') || apiStdout.length == 1) {
+ print('This test must be run on api 30 or higher. Skipping...');
+ return;
+ }
+ // Developer settings are required on target device for cutout manipulation.
+ bool shouldResetDevSettings = false;
+ final ProcessResult checkDevSettingsResult = Process.runSync('adb', [
+ 'shell',
+ 'settings',
+ 'get',
+ 'global',
+ 'development_settings_enabled',
+ ]);
+ if (checkDevSettingsResult.stdout.toString().startsWith('0')) {
+ print('Enabling developer settings...');
+ // Developer settings not enabled, enable them and mark that the origional
+ // state should be restored after.
+ shouldResetDevSettings = true;
+ Process.runSync('adb', [
+ 'shell',
+ 'settings',
+ 'put',
+ 'global',
+ 'development_settings_enabled',
+ '1',
+ ]);
+ }
+ // Assumption of diplay_cutout_test.dart is that there is a "tall" notch.
+ print('Adding Synthetic notch...');
+ Process.runSync('adb', [
+ 'shell',
+ 'cmd',
+ 'overlay',
+ 'enable',
+ 'com.android.internal.display.cutout.emulation.tall',
+ ]);
+ print('Starting test.');
+ try {
+ final FlutterDriver driver = await FlutterDriver.connect();
+ final String data = await driver.requestData(null, timeout: const Duration(minutes: 1));
+ await driver.close();
+ final Map result = jsonDecode(data) as Map;
+ print('Test finished!');
+ print(result);
+ exitCode = result['result'] == 'true' ? 0 : 1;
+ } catch (e) {
+ print(e);
+ exitCode = 1;
+ } finally {
+ print('Removing Synthetic notch...');
+ Process.runSync('adb', [
+ 'shell',
+ 'cmd',
+ 'overlay',
+ 'disable',
+ 'com.android.internal.display.cutout.emulation.tall',
+ ]);
+ print('Reverting Adb changes...');
+ if (shouldResetDevSettings) {
+ print('Disabling developer settings...');
+ Process.runSync('adb', [
+ 'shell',
+ 'settings',
+ 'put',
+ 'global',
+ 'development_settings_enabled',
+ '0',
+ ]);
+ }
+ }
+ return;
+}