Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0fb87c2
Extract overlap detection mechanism
temcguir Aug 7, 2026
35a582b
style: apply spotless format
temcguir Aug 7, 2026
92b0639
Address PR feedback: fix scope, missing KDocs, and test assertions
temcguir Aug 7, 2026
3a94742
Fix test environment dependencies and Robolectric window clipping
temcguir Aug 7, 2026
27dfca9
Fix build.gradle.kts: revert screenshot dependencies from later PR
temcguir Aug 7, 2026
66585e4
Fix spotless formatting
temcguir Aug 7, 2026
7855e21
Address PR feedback: internal visibility, KDocs, unused imports
temcguir Aug 7, 2026
0f809ed
Address remaining PR feedback: density-independent assertions and str…
temcguir Aug 7, 2026
38a7d86
Fix UnrememberedMutableState lint error in overlap tests
temcguir Aug 8, 2026
0f57e00
Disable manifest resolution for Robolectric compose testing
temcguir Aug 8, 2026
8c3f5f2
Merge branch 'main' into temcguir/extract_overlap_detection
temcguir Aug 10, 2026
4f9fbc4
Move overlap tests to instrumentation (androidTest) directory to reso…
temcguir Aug 10, 2026
0a4a7f8
Revert moving overlap tests to androidTest; rely on Robolectric and u…
temcguir Aug 10, 2026
2ef1633
Update ComposeTestRule to v2 API
temcguir Aug 10, 2026
c1c3842
Fix kotlinc unresolved reference and clean up intermediate files
temcguir Aug 11, 2026
01e9bb7
update flip camera button
Kimblebee Jul 30, 2026
8adbb22
flip lens only visible if device has more than one lens direction
Kimblebee Aug 3, 2026
0c6e8a0
Apply overlap-aware styling to flip camera button
Kimblebee Aug 12, 2026
a3c1635
Address flip camera button UI improvements and fix test regressions
Kimblebee Aug 12, 2026
d214feb
spotless
Kimblebee Aug 12, 2026
808b723
Merge branch 'main' into kim/reskin/flip_camera_button
Kimblebee Aug 19, 2026
5bb71d0
spotless
Kimblebee Aug 19, 2026
34c810b
Merge branch 'main' into kim/reskin/flip_camera_button
Kimblebee Sep 4, 2026
314f390
Merge branch 'main' into kim/reskin/flip_camera_button
Kimblebee Sep 4, 2026
c997403
Merge remote-tracking branch 'origin/main' into kim/reskin/flip_camer…
Kimblebee Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.jetpackcamera

import android.content.pm.PackageManager
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
Expand Down Expand Up @@ -65,10 +66,18 @@ class DebugOverlayTest {

@Test
fun hideComponentsButton_togglesUiVisibility() {
val pm = InstrumentationRegistry.getInstrumentation().targetContext.packageManager
val hasMultipleCameras = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT) &&
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)

runMainActivityScenarioTest(debugExtra) {
composeTestRule.waitForCaptureButton()
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists()
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertExists()
if (hasMultipleCameras) {
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertExists()
} else {
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertDoesNotExist()
}
composeTestRule.onNodeWithTag(DEBUG_OVERLAY_BUTTON).assertExists()
composeTestRule.onNodeWithTag(LOGICAL_CAMERA_ID_TAG).assertExists()
composeTestRule.onNodeWithTag(PHYSICAL_CAMERA_ID_TAG).assertExists()
Expand All @@ -88,7 +97,11 @@ class DebugOverlayTest {
composeTestRule.onNodeWithTag(BTN_DEBUG_HIDE_COMPONENTS_TAG).performClick()

composeTestRule.waitForNodeWithTag(CAPTURE_BUTTON)
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertExists()
if (hasMultipleCameras) {
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertExists()
} else {
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertDoesNotExist()
}
composeTestRule.onNodeWithTag(DEBUG_OVERLAY_BUTTON).assertExists()
composeTestRule.onNodeWithTag(LOGICAL_CAMERA_ID_TAG).assertExists()
composeTestRule.onNodeWithTag(PHYSICAL_CAMERA_ID_TAG).assertExists()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.jetpackcamera

import android.content.pm.PackageManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
Expand Down Expand Up @@ -72,11 +73,17 @@ class ExternalAutomationCompatibilityTest {
.that(captureButton)
.isNotNull()

// Verify flip camera button is visible via Resource ID
val flipButton = device.findObject(By.res(FLIP_CAMERA_BUTTON))
assertWithMessage("Flip camera button not found by UI Automator via Resource ID")
.that(flipButton)
.isNotNull()
val pm = InstrumentationRegistry.getInstrumentation().targetContext.packageManager
val hasMultipleCameras = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT) &&
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)

if (hasMultipleCameras) {
// Verify flip camera button is visible via Resource ID
val flipButton = device.findObject(By.res(FLIP_CAMERA_BUTTON))
assertWithMessage("Flip camera button not found by UI Automator via Resource ID")
.that(flipButton)
.isNotNull()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package com.google.jetpackcamera

import android.os.Build
import android.provider.MediaStore
import androidx.compose.ui.test.isEnabled
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
Expand All @@ -37,7 +36,7 @@ import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS
import com.google.jetpackcamera.utils.SCREEN_FLASH_OVERLAY_TIMEOUT_MILLIS
import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS
import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS
import com.google.jetpackcamera.utils.assume
import com.google.jetpackcamera.utils.assumeFlipCameraAvailable
import com.google.jetpackcamera.utils.getCurrentLensFacing
import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedTime
import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest
Expand Down Expand Up @@ -120,9 +119,10 @@ internal class FlashDeviceTest {
// Ensure camera has a back camera and flip to it
val lensFacing = composeTestRule.getCurrentLensFacing()
if (lensFacing != LensFacing.BACK) {
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assume(isEnabled()) {
composeTestRule.assumeFlipCameraAvailable(
"Device does not have a back camera to flip to."
}.performClick()
)
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).performClick()
}

composeTestRule.setFlashMode(FlashMode.ON)
Expand All @@ -149,9 +149,10 @@ internal class FlashDeviceTest {
// Ensure camera has a front camera and flip to it
val lensFacing = composeTestRule.getCurrentLensFacing()
if (lensFacing != LensFacing.FRONT) {
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assume(isEnabled()) {
composeTestRule.assumeFlipCameraAvailable(
"Device does not have a front camera to flip to."
}.performClick()
)
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).performClick()
}

composeTestRule.setFlashMode(FlashMode.ON)
Expand Down Expand Up @@ -190,9 +191,10 @@ internal class FlashDeviceTest {
// Ensure camera has the target lens facing camera and flip to it
val lensFacing = composeTestRule.getCurrentLensFacing()
if (lensFacing != targetLensFacing) {
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assume(isEnabled()) {
composeTestRule.assumeFlipCameraAvailable(
"Device does not have a $targetLensFacing camera to flip to."
}.performClick()
)
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).performClick()
}

composeTestRule.setFlashMode(FlashMode.ON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.google.jetpackcamera
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.click
import androidx.compose.ui.test.isEnabled
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
Expand All @@ -38,7 +37,7 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN
import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_SCRIM
import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON
import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS
import com.google.jetpackcamera.utils.assume
import com.google.jetpackcamera.utils.assumeFlipCameraAvailable
import com.google.jetpackcamera.utils.onNodeWithText
import com.google.jetpackcamera.utils.runMainActivityScenarioTest
import com.google.jetpackcamera.utils.searchForQuickSetting
Expand Down Expand Up @@ -102,10 +101,8 @@ class NavigationTest {
composeTestRule.waitForCaptureButton()

// If flipping the camera is available, flip it. Otherwise skip test.
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON)
.assume(isEnabled()) {
"Device does not have multiple cameras to flip between."
}.performClick()
composeTestRule.assumeFlipCameraAvailable()
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).performClick()

// open quick settings
composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN).assertExists().performClick()
Expand Down Expand Up @@ -146,7 +143,7 @@ class NavigationTest {
// Press the device's back button
uiDevice.pressBack()

// Assert we're on PreviewScreen and bottom sheet is closed
// Assert we're on PreviewScreen by verifying bottom sheet is closed and finding the capture button
composeTestRule.waitForNodeWithTagToDisappear(QUICK_SETTINGS_BOTTOM_SHEET)
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertIsDisplayed()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package com.google.jetpackcamera

import android.content.pm.PackageManager
import android.os.Bundle
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.platform.app.InstrumentationRegistry
Expand Down Expand Up @@ -83,8 +82,8 @@ class SingleLensModeTest(private val lensFacing: String) {
// Wait for the capture button to be visible
composeTestRule.waitForCaptureButton()

// Assert that the flip camera button is disabled
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertIsNotEnabled()
// Assert that the flip camera button does not exist
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertDoesNotExist()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package com.google.jetpackcamera

import androidx.compose.ui.test.doubleClick
import androidx.compose.ui.test.isEnabled
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
Expand All @@ -31,7 +30,7 @@ import com.google.jetpackcamera.ui.components.capture.FLIP_CAMERA_BUTTON
import com.google.jetpackcamera.ui.components.capture.PREVIEW_DISPLAY
import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS
import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS
import com.google.jetpackcamera.utils.assume
import com.google.jetpackcamera.utils.assumeFlipCameraAvailable
import com.google.jetpackcamera.utils.getCurrentLensFacing
import com.google.jetpackcamera.utils.runMainActivityScenarioTest
import com.google.jetpackcamera.utils.waitForNodeWithTag
Expand Down Expand Up @@ -111,9 +110,7 @@ inline fun runFlipCameraTest(
composeTestRule.waitForNodeWithTag(PREVIEW_DISPLAY, APP_START_TIMEOUT_MILLIS)

// If flipping the camera is available, flip it. Otherwise skip test.
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assume(isEnabled()) {
"Device does not have multiple cameras to flip between."
}
composeTestRule.assumeFlipCameraAvailable()

block()
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.printToString
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.TruthJUnit.assume
import com.google.errorprone.annotations.CanIgnoreReturnValue
import com.google.jetpackcamera.model.CaptureMode
import com.google.jetpackcamera.model.ConcurrentCameraMode
Expand Down Expand Up @@ -821,3 +822,10 @@ internal fun buildGeneralErrorMessage(

return sb.toString()
}

fun ComposeTestRule.assumeFlipCameraAvailable(
message: String = "Device does not have multiple cameras to flip between."
) {
val isFlipAvailable = onAllNodesWithTag(FLIP_CAMERA_BUTTON).fetchSemanticsNodes().isNotEmpty()
assume().withMessage(message).that(isFlipAvailable).isTrue()
}
Original file line number Diff line number Diff line change
Expand Up @@ -513,10 +513,7 @@ private fun ContentScreen(
modifier = modifier.testTag(FLIP_CAMERA_BUTTON),
onClick = onFlipCamera,
flipLensUiState = flipLensState.value,
enabledCondition = when (val uiState = flipLensState.value) {
is FlipLensUiState.Available -> uiState.availableLensFacings.size > 1
FlipLensUiState.Unavailable -> false
}
enabledCondition = flipLensState.value is FlipLensUiState.Available
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color

/**
* Defines the background style variants for camera controls to maintain visual
Expand All @@ -29,6 +30,17 @@ internal enum class CameraControlBackgroundStyle {
WHITE_20
}

internal val CameraControlBackgroundStyle.containerColor: Color
get() = when (this) {
CameraControlBackgroundStyle.BLACK_60 -> Color.Black.copy(alpha = 0.6f)
CameraControlBackgroundStyle.WHITE_20 -> Color.White.copy(alpha = 0.2f)
}

internal val CameraControlBackgroundStyle.disabledContainerColor: Color
get() = when (this) {
CameraControlBackgroundStyle.BLACK_60 -> Color.Black.copy(alpha = 0.2f)
CameraControlBackgroundStyle.WHITE_20 -> Color.White.copy(alpha = 0.05f)
}
private val DefaultOverlapTargetBounds = mutableStateOf(Rect.Zero)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,11 @@ private fun VerticalMaterialControls(
.height(120.dp),
contentAlignment = Alignment.Center
) {
flipCameraButton(Modifier)
OverlapAwareStyleProvider(
overlapThreshold = 0.5f
) {
flipCameraButton(Modifier)
}
}
}
}
Expand Down
Loading
Loading