diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties
index a9abe496a..ca3a3f376 100644
--- a/.github/ci-gradle.properties
+++ b/.github/ci-gradle.properties
@@ -27,3 +27,5 @@ kotlin.incremental=false
# If you want to treat warnings as errors locally, set this property to true
# in your ~/.gradle/gradle.properties file.
warningsAsErrors=false
+
+android.experimental.androidTest.enableEmulatorControl=true
diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml
index a4a49b8ee..7d2e24f22 100644
--- a/.github/workflows/Build.yaml
+++ b/.github/workflows/Build.yaml
@@ -221,6 +221,7 @@ jobs:
disable-animations: true
disk-size: 6000M
heap-size: 600M
+ emulator-options: -show-kernel -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
script: ./gradlew connectedDemoDebugAndroidTest --daemon
- name: Run local tests (including Roborazzi) for the combined coverage report (only API 30)
@@ -261,3 +262,143 @@ jobs:
compression-level: 1
overwrite: false
path: '**/build/reports/jacoco/'
+
+
+ androidTestScreenshots:
+ runs-on: ubuntu-latest
+ timeout-minutes: 55
+ strategy:
+ matrix:
+ include:
+ - api-level: 27
+ profile: pixel_5
+ target: default
+# - api-level: 33
+# profile: pixel_fold
+# target: google_apis
+ - api-level: 35
+ profile: pixel_fold
+ target: google_apis
+
+ steps:
+ - name: Delete unnecessary tools 🔧
+ uses: jlumbroso/free-disk-space@v1.3.1
+ with:
+ android: false # Don't remove Android tools
+ tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY"
+ dotnet: true # rm -rf /usr/share/dotnet
+ haskell: true # rm -rf /opt/ghc...
+ swap-storage: true # rm -f /mnt/swapfile (4GiB)
+ docker-images: false # Takes 16s, enable if needed in the future
+ large-packages: false # includes google-cloud-sdk and it's slow
+
+ - name: Enable KVM group perms
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+ ls /dev/kvm
+
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v3
+
+ # TODO this workflow needs cmdline-tools 13.0 (for pixel_fold). Remove when not necessary.
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+
+ # android-emulator-runner uses /latest
+ - name: Remove old cmdlinetools
+ run: |
+ rm -rf ${{ env.ANDROID_HOME}}/cmdline-tools/latest
+# mv ${{ env.ANDROID_HOME}}/cmdline-tools/11479570 ${{ env.ANDROID_HOME}}/cmdline-tools/latest
+# echo ${{ env.ANDROID_HOME}}/cmdline-tools/latest/bin >> $GITHUB_PATH
+
+ # https://github.com/ReactiveCircus/android-emulator-runner/issues/197
+ - name: Create directory for AVD
+ run: mkdir -p /home/runner/.android/avd
+
+ - name: Collect Workflow Telemetry
+ uses: catchpoint/workflow-telemetry-action@v2
+ with:
+ comment_on_pr: false
+
+ - name: Build projects and run instrumented screenshot tests
+ id: dropshotsverify
+ continue-on-error: true
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: ${{ matrix.api-level }}
+ target: ${{ matrix.target }}
+ arch: x86_64
+ disable-animations: true
+ disk-size: 6000M
+ emulator-build: 12895296 # 35.4.5 - https://developer.android.com/studio/emulator_archive
+ heap-size: 600M
+ profile: ${{ matrix.profile }}
+ emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ script:
+ adb logcat > logcat.log & ./gradlew :app:connectedDemoDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=com.google.samples.apps.nowinandroid.ui.InstrumentedScreenshotTests --daemon
+
+ - name: Upload logcat
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: logcat-${{ matrix.profile }}-${{ matrix.api-level }}
+ path: logcat.log
+
+ - name: Prevent pushing new screenshots if this is a fork
+ id: checkfork_screenshots_instrumented
+ continue-on-error: false
+ if: steps.dropshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
+ run: |
+ echo "::error::Instrumented screenshot tests failed, please create a PR in your fork first." && exit 1
+
+ - name: Record new screenshots if verification failed
+ id: dropshotsrecord
+ continue-on-error: false
+ if: steps.dropshotsverify.outcome == 'failure'
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: ${{ matrix.api-level }}
+ target: ${{ matrix.target }}
+ arch: x86_64
+ disable-animations: true
+ disk-size: 6000M
+ emulator-build: 12895296 # 35.4.5 - https://developer.android.com/studio/emulator_archive
+ heap-size: 600M
+ profile: ${{ matrix.profile }}
+ emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ script:
+ ./gradlew :app:connectedDemoDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=com.google.samples.apps.nowinandroid.ui.InstrumentedScreenshotTests -Pdropshots.record --daemon --stacktrace
+
+
+ - name: Checkout new changes (in case another job already uploaded screenshots)
+ uses: actions/checkout@v4
+# if: steps.dropshotsverify.outputs.newgoldens == 'true'
+ with:
+ clean: false
+
+ - name: Push new device screenshots if available
+ uses: stefanzweifel/git-auto-commit-action@4b8a201e31cadd9829df349894b28c54e6c19fe6
+# if: steps.dropshotsverify.outputs.newgoldens == 'true'
+ with:
+ file_pattern: 'app/src/androidTest/screenshots/*.png'
+ disable_globbing: true
+ commit_message: "🤖 Updates instrumented screenshots. API ${{ matrix.api-level }}"
+
+ - name: Upload test reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-reports-${{ matrix.profile }}-${{ matrix.api-level }}
+ path: '**/build/reports/androidTests'
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 682fbc1b3..3c4817855 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -62,11 +62,6 @@ android {
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
}
}
- testOptions {
- unitTests {
- isIncludeAndroidResources = true
- }
- }
namespace = "com.google.samples.apps.nowinandroid"
}
@@ -128,9 +123,13 @@ dependencies {
androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.datastoreTest)
androidTestImplementation(libs.androidx.test.espresso.core)
+ androidTestImplementation(libs.androidx.test.core)
+ androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.kotlin.test)
+ androidTestImplementation(libs.androidx.test.espresso.device)
+ androidTestImplementation(libs.androidx.test.uiautomator)
baselineProfile(projects.benchmarks)
}
diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt
new file mode 100644
index 000000000..ee21fc322
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.ui
+
+import android.graphics.Bitmap
+import android.util.Log
+import androidx.test.core.app.takeScreenshot
+import androidx.test.espresso.device.DeviceInteraction.Companion.setClosedMode
+import androidx.test.espresso.device.DeviceInteraction.Companion.setFlatMode
+import androidx.test.espresso.device.DeviceInteraction.Companion.setScreenOrientation
+import androidx.test.espresso.device.EspressoDevice.Companion.onDevice
+import androidx.test.espresso.device.action.ScreenOrientation.LANDSCAPE
+import androidx.test.espresso.device.action.ScreenOrientation.PORTRAIT
+import androidx.test.espresso.device.common.executeShellCommand
+import androidx.test.espresso.device.controller.DeviceMode.CLOSED
+import androidx.test.espresso.device.controller.DeviceMode.FLAT
+import androidx.test.espresso.device.filter.RequiresDeviceMode
+import androidx.test.espresso.device.filter.RequiresDisplay
+import androidx.test.espresso.device.sizeclass.HeightSizeClass.Companion.HeightSizeClassEnum
+import androidx.test.espresso.device.sizeclass.WidthSizeClass.Companion.WidthSizeClassEnum
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SdkSuppress
+import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.dropbox.dropshots.Dropshots
+import com.google.samples.apps.nowinandroid.MainActivity
+import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+/**
+ * These tests must be run on the following devices:
+ * - A phone on API 27 (pixel_5)
+ * - A foldable on API 33 (pixel_fold)
+ * - A foldable on API 35 (pixel_fold)
+ */
+@HiltAndroidTest
+@InstrumentedScreenshotTests
+class EdgeToEdgeTest {
+ /**
+ * Manages the components' state and is used to perform injection on your test
+ */
+ @get:Rule(order = 0)
+ val hiltRule = HiltAndroidRule(this)
+
+ /**
+ * Create a temporary folder used to create a Data Store file. This guarantees that
+ * the file is removed in between each test, preventing a crash.
+ */
+ @BindValue
+ @get:Rule(order = 1)
+ val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
+
+ /**
+ * Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission to prevent the
+ * permissions dialog from showing on top.
+ */
+ @get:Rule(order = 2)
+ val postNotificationsPermission = GrantPostNotificationsPermissionRule()
+
+ @get:Rule(order = 3)
+ val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
+
+ @get:Rule(order = 4)
+ val dropshots = Dropshots()
+
+ @Before
+ fun setup() = hiltRule.inject()
+
+ @Before
+ fun enableDemoMode() {
+ executeShellCommand(
+ "settings put global development_settings_enabled 1",
+ )
+ executeShellCommand("settings put global sysui_demo_allowed 1")
+ executeShellCommand(
+ "am broadcast -a com.android.systemui.demo -e command " +
+ "enter",
+ )
+ executeShellCommand(
+ "am broadcast -a com.android.systemui.demo -e command " +
+ "notifications -e visible false",
+ )
+ executeShellCommand(
+ "am broadcast -a com.android.systemui.demo -e command " +
+ "clock -e hhmm 1234",
+ )
+ executeShellCommand(
+ "am broadcast -a com.android.systemui.demo -e command " +
+ "network -e wifi hide",
+ )
+ executeShellCommand(
+ "am broadcast -a com.android.systemui.demo -e command " +
+ "network -e mobile hide",
+ )
+ executeShellCommand(
+ "am broadcast -a com.android.systemui.demo -e command " +
+ "network -e satellite hide",
+ )
+ }
+
+ @After
+ fun disableDemoMode() {
+ exitDemoMode()
+ executeShellCommand(
+ "settings put global sysui_demo_allowed 0",
+ )
+ executeShellCommand(
+ "settings put global development_settings_enabled 0",
+ )
+ }
+
+ private fun exitDemoMode() {
+ executeShellCommand(
+ "am broadcast -a com.android.systemui.demo -e command exit",
+ )
+ }
+
+ companion object {
+ @JvmStatic
+ @AfterClass
+ fun resetDemoMode() {
+ executeShellCommand("am broadcast -a com.android.systemui.demo -e command exit")
+ }
+ }
+
+ @RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM)
+ @SdkSuppress(minSdkVersion = 27, maxSdkVersion = 27)
+ @Test
+ fun edgeToEdge_Phone_Api27() {
+ screenshotSystemBar("edgeToEdge_Phone_systemBar_Api27")
+ screenshotNavigationBar("edgeToEdge_Phone_navBar_Api27")
+ }
+
+ @RequiresDeviceMode(mode = FLAT)
+ @RequiresDeviceMode(mode = CLOSED)
+ @SdkSuppress(minSdkVersion = 33, maxSdkVersion = 33)
+ @Test
+ fun edgeToEdge_Foldable_api33() {
+ runFoldableTests(apiName = "api33")
+ }
+
+ @RequiresDeviceMode(mode = FLAT)
+ @RequiresDeviceMode(mode = CLOSED)
+ @SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+ @Test
+ fun edgeToEdge_Foldable_api35() {
+ runFoldableTests(apiName = "api35")
+ }
+
+ @RequiresDeviceMode(mode = FLAT)
+ @RequiresDeviceMode(mode = CLOSED)
+ @SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+ @Test
+ fun edgeToEdge_Foldable_api35_tallCutout() {
+ onDevice().setClosedMode()
+ forceTallCutout()
+ exitDemoMode()
+ enableDemoMode() // Mode change resets demo mode!
+ screenshotSystemBar("edgeToEdge_Foldable_closed_system_tallCutout_api35")
+
+ onDevice().setFlatMode()
+ exitDemoMode()
+ forceTallCutout()
+ enableDemoMode() // Mode change resets demo mode!
+ screenshotSystemBar("edgeToEdge_Foldable_flat_system_tallCutout_api35")
+
+ onDevice().setClosedMode()
+ resetCutout()
+ }
+
+ // Very flaky:DeviceControllerOperationException: Device could not be set to the
+ // requested screen orientation.
+
+ @RequiresDeviceMode(mode = CLOSED)
+ @SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+ @Test
+ fun edgeToEdge_Foldable_api35_landscape() {
+ onDevice().setClosedMode()
+ onDevice().setScreenOrientation(LANDSCAPE)
+ forceThreeButtonNavigation()
+ exitDemoMode()
+ enableDemoMode() // This fixes the satellite icon showing up uninvited.
+ screenshotSideNavigationBar("edgeToEdge_Foldable_landscape_sideNav3button_35")
+ onDevice().setScreenOrientation(PORTRAIT)
+ }
+
+ private fun runFoldableTests(apiName: String) {
+ resetCutout()
+ onDevice().setClosedMode()
+ screenshotSystemBar("edgeToEdge_Foldable_closed_system_$apiName")
+ forceThreeButtonNavigation()
+ screenshotNavigationBar("edgeToEdge_Foldable_closed_nav3button_$apiName")
+ forceGestureNavigation()
+ screenshotNavigationBar("edgeToEdge_Foldable_closed_navGesture_$apiName")
+
+ onDevice().setFlatMode()
+ exitDemoMode()
+ enableDemoMode() // Flat mode resets demo mode!
+ screenshotSystemBar("edgeToEdge_Foldable_flat_system_$apiName")
+ forceThreeButtonNavigation()
+ screenshotNavigationBar("edgeToEdge_Foldable_flat_nav3button_$apiName")
+ forceGestureNavigation()
+ screenshotNavigationBar("edgeToEdge_Foldable_flat_navGesture_$apiName")
+ onDevice().setClosedMode()
+ }
+
+ private fun screenshotSystemBar(screenshotFileName: String) {
+ waitForWindowUpdate()
+
+ // Crop the top, adding extra pixels to check continuity
+ val bitmap = takeScreenshot().let {
+ val newHeight = 200
+ Bitmap.createBitmap(it, 0, 0, it.width, newHeight)
+ }
+ assertSnapshot(bitmap, screenshotFileName)
+ }
+
+ private fun screenshotNavigationBar(screenshotFileName: String) {
+ waitForWindowUpdate()
+
+ // Crop the bottom, adding extra pixels to check continuity
+ val bitmap = takeScreenshot().let {
+ val newHeight = 200
+ Bitmap.createBitmap(it, 0, it.height - newHeight, it.width, newHeight)
+ }
+ assertSnapshot(bitmap, screenshotFileName)
+ }
+
+ private fun screenshotSideNavigationBar(screenshotFileName: String) {
+ waitForWindowUpdate()
+
+ // Crop the top, adding extra pixels to check continuity
+ val bitmap = takeScreenshot().let {
+ Bitmap.createBitmap(it, it.width - 250, 0, 250, it.height)
+ }
+ assertSnapshot(bitmap, screenshotFileName)
+ }
+
+ private fun forceThreeButtonNavigation() {
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
+ executeShellCommand(
+ "cmd overlay enable-exclusive " +
+ "com.android.internal.systemui.navbar.threebutton",
+ )
+ }
+ waitForWindowUpdate()
+ }
+
+ private fun forceGestureNavigation() {
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
+ executeShellCommand(
+ "cmd overlay enable-exclusive " +
+ "com.android.internal.systemui.navbar.gestural",
+ )
+ }
+ waitForWindowUpdate()
+ }
+
+ private fun forceTallCutout() {
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
+ executeShellCommand(
+ "cmd overlay enable-exclusive " +
+ "--category com.android.internal.display.cutout.emulation.tall ",
+ )
+ }
+ waitForWindowUpdate()
+ }
+ private fun resetCutout() {
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
+ executeShellCommand(
+ "cmd overlay enable-exclusive " +
+ "--category com.android.internal.display.cutout.emulation.noCutout ",
+ )
+ }
+ waitForWindowUpdate()
+ }
+
+ private fun waitForWindowUpdate() {
+ // TODO: This works but it's unclear if it's making it wait too long. Investigate.
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ .waitForWindowUpdate(
+ InstrumentationRegistry.getInstrumentation().targetContext.packageName,
+ 8000,
+ )
+ }
+
+ private fun assertSnapshot(
+ bitmap: Bitmap,
+ name: String,
+ filePath: String? = null,
+ ) {
+ // Try to assert 3 times
+ var count = 2
+ while (true) {
+ try {
+ dropshots.assertSnapshot(bitmap, name, filePath)
+ } catch (e: AssertionError) {
+ if (count == 0) throw e
+ count -= 1
+ Log.i("EdgeToEdgeTest", "Test failed, retrying (count=$count)")
+ waitForWindowUpdate()
+ continue
+ }
+ break
+ }
+ }
+
+ private fun executeShellCommand(command: String) {
+ runOnUiThread {
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).executeShellCommand(command)
+ }
+ // ADB commands are not synchronized. This sleep was found empirically.
+ Thread.sleep(20)
+ }
+}
diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt
new file mode 100644
index 000000000..c279130cb
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.ui
+
+/**
+ * TODO: Move to a test module.
+ */
+annotation class InstrumentedScreenshotTests
diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_nav3button_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_nav3button_api33.png
new file mode 100644
index 000000000..1a61f84f5
Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_nav3button_api33.png differ
diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_navGesture_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_navGesture_api33.png
new file mode 100644
index 000000000..1a61f84f5
Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_navGesture_api33.png differ
diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_system_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_system_api33.png
new file mode 100644
index 000000000..82c3106ac
Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_system_api33.png differ
diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_nav3button_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_nav3button_api33.png
new file mode 100644
index 000000000..603a15716
Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_nav3button_api33.png differ
diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_navGesture_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_navGesture_api33.png
new file mode 100644
index 000000000..720dbc456
Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_navGesture_api33.png differ
diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_system_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_system_api33.png
new file mode 100644
index 000000000..57e80e433
Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_system_api33.png differ
diff --git a/app/src/androidTest/screenshots/edgeToEdge_Phone_navBar_Api27.png b/app/src/androidTest/screenshots/edgeToEdge_Phone_navBar_Api27.png
new file mode 100644
index 000000000..ec52601eb
Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Phone_navBar_Api27.png differ
diff --git a/app/src/androidTest/screenshots/edgeToEdge_Phone_systemBar_Api27.png b/app/src/androidTest/screenshots/edgeToEdge_Phone_systemBar_Api27.png
new file mode 100644
index 000000000..9466cfca1
Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Phone_systemBar_Api27.png differ
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..36313049e
--- /dev/null
+++ b/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
index 1ab3a2ca0..d5de40b34 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
@@ -34,6 +34,7 @@ class AndroidApplicationConventionPlugin : Plugin {
apply(plugin = "org.jetbrains.kotlin.android")
apply(plugin = "nowinandroid.android.lint")
apply(plugin = "com.dropbox.dependency-guard")
+ apply(plugin = "com.dropbox.dropshots")
extensions.configure {
configureKotlinAndroid(this)
@@ -41,6 +42,8 @@ class AndroidApplicationConventionPlugin : Plugin {
@Suppress("UnstableApiUsage")
testOptions.animationsDisabled = true
configureGradleManagedDevices(this)
+ testOptions.emulatorControl.enable = true
+// testOptions.unitTests.isIncludeAndroidResources
}
extensions.configure {
configurePrintApksTask(this)
diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
index 1af5523c5..e04635d02 100644
--- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
@@ -32,6 +32,7 @@ class AndroidFeatureConventionPlugin : Plugin {
extensions.configure {
testOptions.animationsDisabled = true
+ testOptions.emulatorControl.enable = true
configureGradleManagedDevices(this)
}
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
index 3fe727410..6dea352e4 100644
--- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
@@ -40,6 +40,7 @@ class AndroidLibraryConventionPlugin : Plugin {
defaultConfig.targetSdk = 35
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testOptions.animationsDisabled = true
+ testOptions.emulatorControl.enable = true
configureFlavors(this)
configureGradleManagedDevices(this)
// The resource prefix is derived from the module name,
diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt
index f67e9093d..f6a599cd2 100644
--- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt
+++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt
@@ -27,12 +27,12 @@ import org.gradle.kotlin.dsl.invoke
internal fun configureGradleManagedDevices(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
- val pixel4 = DeviceConfig("Pixel 4", 30, "aosp-atd")
+ val pixel4 = DeviceConfig("Pixel 4", 27, "aosp")
val pixel6 = DeviceConfig("Pixel 6", 31, "aosp")
- val pixelC = DeviceConfig("Pixel C", 30, "aosp-atd")
+ val pixelC = DeviceConfig("Pixel C", 30, "aosp")
val allDevices = listOf(pixel4, pixel6, pixelC)
- val ciDevices = listOf(pixel4, pixelC)
+ val ciDevices = listOf(pixel4, pixel6, pixelC)
commonExtension.testOptions {
managedDevices {
diff --git a/build.gradle.kts b/build.gradle.kts
index b7989bab4..da1ea46cd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -52,6 +52,7 @@ plugins {
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.dependencyGuard) apply false
+ alias(libs.plugins.dropshots) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.firebase.perf) apply false
alias(libs.plugins.gms) apply false
diff --git a/gradle.properties b/gradle.properties
index 2e9d9fc30..f0f1407f7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -54,12 +54,14 @@ kotlin.code.style=official
# Disable build features that are enabled by default,
# https://developer.android.com/build/releases/gradle-plugin#default-changes
-android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false
# Run Roborazzi screenshot tests with the local tests
roborazzi.test.verify=true
+# Espresso Device
+android.experimental.androidTest.enableEmulatorControl=true
+
# Prevent uninstall app after instrumented tests
# https://issuetracker.google.com/issues/295039976
android.injected.androidTest.leaveApksInstalledAfterRun=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2b6f96968..801e7da44 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -15,6 +15,7 @@ androidxCore = "1.15.0"
androidxCoreSplashscreen = "1.0.1"
androidxDataStore = "1.1.1"
androidxEspresso = "3.6.1"
+androidxEspressoDevice = "1.0.0-beta01"
androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.8.7"
androidxLintGradle = "1.0.0-alpha03"
@@ -32,6 +33,7 @@ androidxWindowManager = "1.3.0"
androidxWork = "2.10.0"
coil = "2.7.0"
dependencyGuard = "0.5.0"
+dropshots = "0.4.2"
firebaseBom = "33.7.0"
firebaseCrashlyticsPlugin = "3.0.2"
firebasePerfPlugin = "1.4.2"
@@ -102,6 +104,7 @@ androidx-navigation-testing = { group = "androidx.navigation", name = "navigatio
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" }
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" }
androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" }
+androidx-test-espresso-device = { group = "androidx.test.espresso", name = "espresso-device", version.ref = "androidxEspressoDevice" }
androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
@@ -170,6 +173,7 @@ android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidxMacroBenchmark"}
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" }
+dropshots = { id = "com.dropbox.dropshots", version.ref = "dropshots" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" }
gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" }