Skip to content

Commit

Permalink
Use LensFacing instead of flipCamera (#133)
Browse files Browse the repository at this point in the history
* Add LensFacing to model

* Remove unused CameraUseCase data classes

 Use the classes in the model instead

* Change CameraUseCase.flipCamera() to setLensFacing()

* Use of setLensFacing rather than flipCamera

 Switches all usage to setting LensFacing directly rather
 than relying on booleans.

* Add switch camera tests

 This will switch cameras using three methods:
 1. The flip camera button on the preview screen
 2. Double tapping the screen to flip cameras
 3. Flipping cameras with the quick settings button

* Fix unit test failures

* Remove unused import and unnecessary changes

* Fix BackgroundDeviceTest

 Fixes tests in BackgroundDeviceTest that use the flip camera button

* Fix comments in LocalSettingsRepositoryInstrumentedTest
  • Loading branch information
temcguir authored Mar 6, 2024
1 parent 2b03cc6 commit 3bda628
Show file tree
Hide file tree
Showing 33 changed files with 485 additions and 202 deletions.
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

androidTestImplementation(libs.androidx.rules)
androidTestImplementation(libs.androidx.uiautomator)
androidTestImplementation(libs.truth)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
Expand All @@ -121,6 +121,8 @@ dependencies {

// Camera Preview
implementation(project(":feature:preview"))
// Only needed as androidTestImplementation for now since we only need it for string resources
androidTestImplementation(project(":feature:quicksettings"))

// Settings Screen
implementation(project(":feature:settings"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import com.google.jetpackcamera.feature.preview.ui.QUICK_SETTINGS_BUTTON
import com.google.jetpackcamera.feature.preview.ui.QUICK_SETTINGS_RATIO_1_1_BUTTON
import com.google.jetpackcamera.feature.preview.ui.QUICK_SETTINGS_RATIO_BUTTON
import com.google.jetpackcamera.feature.quicksettings.ui.QUICK_SETTINGS_FLIP_CAMERA_BUTTON
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -57,19 +61,19 @@ class BackgroundDeviceTest {

@Test
fun flipCamera_then_background_foreground() {
uiDevice.findObject(By.res("QuickSettingDropDown")).click()
uiDevice.findObject(By.res("QuickSetFlipCamera")).click()
uiDevice.findObject(By.res("QuickSettingDropDown")).click()
uiDevice.findObject(By.res(QUICK_SETTINGS_BUTTON)).click()
uiDevice.findObject(By.res(QUICK_SETTINGS_FLIP_CAMERA_BUTTON)).click()
uiDevice.findObject(By.res(QUICK_SETTINGS_BUTTON)).click()
uiDevice.waitForIdle(2000)
backgroundThenForegroundApp()
}

@Test
fun setAspectRatio_then_background_foreground() {
uiDevice.findObject(By.res("QuickSettingDropDown")).click()
uiDevice.findObject(By.res("QuickSetAspectRatio")).click()
uiDevice.findObject(By.res("QuickSetAspectRatio1:1")).click()
uiDevice.findObject(By.res("QuickSettingDropDown")).click()
uiDevice.findObject(By.res(QUICK_SETTINGS_BUTTON)).click()
uiDevice.findObject(By.res(QUICK_SETTINGS_RATIO_BUTTON)).click()
uiDevice.findObject(By.res(QUICK_SETTINGS_RATIO_1_1_BUTTON)).click()
uiDevice.findObject(By.res(QUICK_SETTINGS_BUTTON)).click()
uiDevice.waitForIdle(2000)
backgroundThenForegroundApp()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.annotation.StringRes
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.printToString
import androidx.test.core.app.ApplicationProvider
Expand All @@ -32,9 +33,26 @@ import org.junit.AssumptionViolatedException
fun SemanticsNodeInteractionsProvider.onNodeWithText(
@StringRes strRes: Int
): SemanticsNodeInteraction = onNodeWithText(
text = ApplicationProvider.getApplicationContext<Context>().getString(strRes)
text = getResString(strRes)
)

/**
* Allows use of testRule.onNodeWithContentDescription that uses an integer string resource
* rather than a [String] directly.
*/
fun SemanticsNodeInteractionsProvider.onNodeWithContentDescription(
@StringRes strRes: Int
): SemanticsNodeInteraction = onNodeWithContentDescription(
label = getResString(strRes)
)

/**
* Fetch a string resources from a [SemanticsNodeInteractionsProvider] context.
*/
fun SemanticsNodeInteractionsProvider.getResString(@StringRes strRes: Int): String {
return ApplicationProvider.getApplicationContext<Context>().getString(strRes)
}

/**
* Assumes that the provided [matcher] is satisfied for this node.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@
*/
package com.google.jetpackcamera

import android.app.Activity
import androidx.compose.ui.test.isDisplayed
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
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
Expand Down Expand Up @@ -159,12 +157,4 @@ class NavigationTest {
// Assert we're on quick settings by finding the ratio button
composeTestRule.onNodeWithTag(QUICK_SETTINGS_RATIO_BUTTON).assertExists()
}

private inline fun <reified T : Activity> runScenarioTest(
crossinline block: ActivityScenario<T>.() -> Unit
) {
ActivityScenario.launch(T::class.java).use { scenario ->
scenario.apply(block)
}
}
}
193 changes: 193 additions & 0 deletions app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright (C) 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
*
* http://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.jetpackcamera

import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.doubleClick
import androidx.compose.ui.test.isDisplayed
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
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import com.google.common.truth.Truth.assertThat
import com.google.jetpackcamera.feature.preview.ui.FLIP_CAMERA_BUTTON
import com.google.jetpackcamera.feature.preview.ui.PREVIEW_DISPLAY
import com.google.jetpackcamera.feature.preview.ui.QUICK_SETTINGS_BUTTON
import com.google.jetpackcamera.feature.quicksettings.ui.QUICK_SETTINGS_FLIP_CAMERA_BUTTON
import com.google.jetpackcamera.quicksettings.R.string.quick_settings_back_camera_description
import com.google.jetpackcamera.quicksettings.R.string.quick_settings_dropdown_closed_description
import com.google.jetpackcamera.quicksettings.R.string.quick_settings_dropdown_open_description
import com.google.jetpackcamera.quicksettings.R.string.quick_settings_front_camera_description
import com.google.jetpackcamera.settings.model.LensFacing
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class SwitchCameraTest {
@get:Rule
val cameraPermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(android.Manifest.permission.CAMERA)

@get:Rule
val composeTestRule = createEmptyComposeRule()

@Test
fun canFlipCamera_fromPreviewScreenButton() = runFlipCameraTest(composeTestRule) {
val lensFacingStates = mutableListOf<LensFacing>()
// Get initial lens facing
val initialLensFacing = composeTestRule.getCurrentLensFacing()
lensFacingStates.add(initialLensFacing)

// Press the flip camera button
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).performClick()

// Get lens facing after first flip
lensFacingStates.add(composeTestRule.getCurrentLensFacing())

// Press the flip camera button again
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).performClick()

// Get lens facing after second flip
lensFacingStates.add(composeTestRule.getCurrentLensFacing())

assertThat(lensFacingStates).containsExactly(
initialLensFacing,
initialLensFacing.flip(),
initialLensFacing.flip().flip()
).inOrder()
}

@Test
fun canFlipCamera_fromPreviewScreenDoubleTap() = runFlipCameraTest(composeTestRule) {
val lensFacingStates = mutableListOf<LensFacing>()
// Get initial lens facing
val initialLensFacing = composeTestRule.getCurrentLensFacing()
lensFacingStates.add(initialLensFacing)

// Double click display to flip camera
composeTestRule.onNodeWithTag(PREVIEW_DISPLAY)
.performTouchInput { doubleClick() }

// Get lens facing after first flip
lensFacingStates.add(composeTestRule.getCurrentLensFacing())

// Double click display to flip camera again
composeTestRule.onNodeWithTag(PREVIEW_DISPLAY)
.performTouchInput { doubleClick() }

// Get lens facing after second flip
lensFacingStates.add(composeTestRule.getCurrentLensFacing())

assertThat(lensFacingStates).containsExactly(
initialLensFacing,
initialLensFacing.flip(),
initialLensFacing.flip().flip()
).inOrder()
}

@Test
fun canFlipCamera_fromQuickSettings() = runFlipCameraTest(composeTestRule) {
// Navigate to quick settings
composeTestRule.onNodeWithTag(QUICK_SETTINGS_BUTTON)
.assertExists()
.performClick()

val lensFacingStates = mutableListOf<LensFacing>()
// Get initial lens facing
val initialLensFacing = composeTestRule.getCurrentLensFacing()
lensFacingStates.add(initialLensFacing)

// Double click quick settings button to flip camera
composeTestRule.onNodeWithTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON).performClick()

// Get lens facing after first flip
lensFacingStates.add(composeTestRule.getCurrentLensFacing())

// Double click quick settings button to flip camera again
composeTestRule.onNodeWithTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON).performClick()

// Get lens facing after second flip
lensFacingStates.add(composeTestRule.getCurrentLensFacing())

assertThat(lensFacingStates).containsExactly(
initialLensFacing,
initialLensFacing.flip(),
initialLensFacing.flip().flip()
).inOrder()
}
}

inline fun runFlipCameraTest(
composeTestRule: ComposeTestRule,
crossinline block: ActivityScenario<MainActivity>.() -> Unit
) = runScenarioTest {
// Wait for the preview display to be visible
composeTestRule.waitUntil {
composeTestRule.onNodeWithTag(PREVIEW_DISPLAY).isDisplayed()
}

// 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."
}

block()
}

private fun ComposeTestRule.getCurrentLensFacing(): LensFacing {
var needReturnFromQuickSettings = false
onNodeWithContentDescription(quick_settings_dropdown_closed_description).apply {
if (isDisplayed()) {
performClick()
needReturnFromQuickSettings = true
}
}

onNodeWithContentDescription(quick_settings_dropdown_open_description).assertExists(
"LensFacing can only be retrieved from PreviewScreen or QuickSettings screen"
)

try {
return onNodeWithTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON).fetchSemanticsNode(
"Flip camera button is not visible when expected."
).let { node ->
node.config[SemanticsProperties.ContentDescription].any { description ->
when (description) {
getResString(quick_settings_front_camera_description) ->
return@let LensFacing.FRONT

getResString(quick_settings_back_camera_description) ->
return@let LensFacing.BACK

else -> false
}
}
throw AssertionError("Unable to determine lens facing from quick settings")
}
} finally {
if (needReturnFromQuickSettings) {
onNodeWithContentDescription(quick_settings_dropdown_open_description)
.assertExists()
.performClick()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.jetpackcamera

import android.app.Activity
import androidx.test.core.app.ActivityScenario
import com.google.jetpackcamera.settings.model.CameraAppSettings
import java.util.concurrent.atomic.AtomicReference
Expand All @@ -34,3 +35,11 @@ object UiTestUtil {
).previewViewModel!!.previewUiState.value.currentCameraSettings
}
}

inline fun <reified T : Activity> runScenarioTest(
crossinline block: ActivityScenario<T>.() -> Unit
) {
ActivityScenario.launch(T::class.java).use { scenario ->
scenario.apply(block)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
import com.google.jetpackcamera.settings.model.DarkMode
import com.google.jetpackcamera.settings.model.DynamicRange
import com.google.jetpackcamera.settings.model.FlashMode
import com.google.jetpackcamera.settings.model.LensFacing
import java.io.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -108,23 +109,23 @@ class LocalSettingsRepositoryInstrumentedTest {

@Test
fun can_update_default_to_front_camera() = runTest {
// default to front camera starts false
val initialFrontCameraDefault = repository.getCameraAppSettings().isFrontCameraFacing
repository.updateDefaultToFrontCamera()
// default to front camera is now true
val frontCameraDefault = repository.getCameraAppSettings().isFrontCameraFacing
// default lens facing starts as BACK
val initialDefaultLensFacing = repository.getCameraAppSettings().cameraLensFacing
repository.updateDefaultLensFacing(LensFacing.FRONT)
// default lens facing is now FRONT
val newDefaultLensFacing = repository.getCameraAppSettings().cameraLensFacing
advanceUntilIdle()

assertThat(initialFrontCameraDefault).isFalse()
assertThat(frontCameraDefault).isTrue()
assertThat(initialDefaultLensFacing).isEqualTo(LensFacing.BACK)
assertThat(newDefaultLensFacing).isEqualTo(LensFacing.FRONT)
}

@Test
fun can_update_flash_mode() = runTest {
// default to front camera starts false
// default flash mode starts as OFF
val initialFlashModeStatus = repository.getCameraAppSettings().flashMode
repository.updateFlashModeStatus(FlashMode.ON)
// default to front camera is now true
// default flash mode is now ON
val newFlashModeStatus = repository.getCameraAppSettings().flashMode
advanceUntilIdle()

Expand Down
Loading

0 comments on commit 3bda628

Please sign in to comment.