Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
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 @@ -23,8 +23,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import com.google.common.truth.Truth.assertThat
import com.google.jetpackcamera.ui.components.capture.AMPLITUDE_HOT_TAG
import com.google.jetpackcamera.ui.components.capture.AMPLITUDE_NONE_TAG
import com.google.jetpackcamera.ui.components.capture.AUDIO_INPUT_TOGGLE
import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON
import com.google.jetpackcamera.ui.components.capture.FLIP_CAMERA_BUTTON
import com.google.jetpackcamera.ui.components.capture.ZOOM_BUTTON_ROW_TAG
Expand Down Expand Up @@ -76,8 +75,7 @@ class DebugHideComponentsTest {
composeTestRule.waitForNodeWithTagToDisappear(CAPTURE_BUTTON)
composeTestRule.onNodeWithTag(ZOOM_BUTTON_ROW_TAG).assertDoesNotExist()
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertDoesNotExist()
composeTestRule.onNodeWithTag(AMPLITUDE_NONE_TAG).assertDoesNotExist()
composeTestRule.onNodeWithTag(AMPLITUDE_HOT_TAG).assertDoesNotExist()
composeTestRule.onNodeWithTag(AUDIO_INPUT_TOGGLE).assertDoesNotExist()
composeTestRule.onNodeWithTag(DEBUG_OVERLAY_BUTTON).assertDoesNotExist()
composeTestRule.onNodeWithTag(LOGICAL_CAMERA_ID_TAG).assertDoesNotExist()
composeTestRule.onNodeWithTag(PHYSICAL_CAMERA_ID_TAG).assertDoesNotExist()
Expand Down
122 changes: 109 additions & 13 deletions app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,32 @@
package com.google.jetpackcamera

import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.RequiresDevice
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import com.google.common.truth.Truth.assertThat
import com.google.jetpackcamera.ui.components.capture.AMPLITUDE_HOT_TAG
import com.google.jetpackcamera.ui.components.capture.AUDIO_INPUT_TOGGLE
import com.google.jetpackcamera.ui.components.capture.AudioInputState
import com.google.jetpackcamera.ui.components.capture.AudioStateProperty
import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON
import com.google.jetpackcamera.ui.uistateadapter.capture.R
import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS
import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS
import com.google.jetpackcamera.utils.debugExtra
import com.google.jetpackcamera.utils.pressAndDragToLockVideoRecording
import com.google.jetpackcamera.utils.runMainActivityScenarioTest
import com.google.jetpackcamera.utils.waitForCaptureButton
import com.google.jetpackcamera.utils.waitForNodeWithTagAndSemantics
import com.google.jetpackcamera.utils.waitForSnackbarWithText
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@RequiresDevice
class VideoAudioTest {
@get:Rule
val permissionsRule: GrantPermissionRule =
Expand All @@ -62,14 +64,108 @@ class VideoAudioTest {
// icon will only be unmuted if audio is nonzero
composeTestRule.waitForCaptureButton()

// record video
composeTestRule.onNodeWithTag(CAPTURE_BUTTON)
.assertExists().performTouchInput { longClick(durationMillis = 5000) }
// Start video recording and lock it
composeTestRule.pressAndDragToLockVideoRecording()

// assert hot amplitude tag visible
uiDevice.wait(
Until.findObject(By.res(AMPLITUDE_HOT_TAG)),
5000
composeTestRule.waitForNodeWithTagAndSemantics(
tag = AUDIO_INPUT_TOGGLE,
semanticsProperty = AudioStateProperty to AudioInputState.INCOMING
)

// Stop recording
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).performClick()

composeTestRule.waitForSnackbarWithText(
R.string.toast_video_capture_success,
VIDEO_CAPTURE_TIMEOUT_MILLIS
)
}
}

@Test
fun muteAndUnmuteDuringRecording() {
runMainActivityScenarioTest(debugExtra) {
composeTestRule.waitForCaptureButton()

// verify audio button is initially set to enable audio
composeTestRule.waitForNodeWithTagAndSemantics(
tag = AUDIO_INPUT_TOGGLE,
semanticsProperty = AudioStateProperty to AudioInputState.READY
)

// Start video recording and lock it
composeTestRule.pressAndDragToLockVideoRecording()

// Verify amplitude button is hot initially (audio is enabled by default)
composeTestRule.waitForNodeWithTagAndSemantics(
tag = AUDIO_INPUT_TOGGLE,
semanticsProperty = AudioStateProperty to AudioInputState.INCOMING
)

// Tap the amplitude button to mute
composeTestRule.onNodeWithTag(AUDIO_INPUT_TOGGLE).performClick()

// Verify amplitude button is now showing "none/muted" tag
composeTestRule.waitForNodeWithTagAndSemantics(
tag = AUDIO_INPUT_TOGGLE,
semanticsProperty = AudioStateProperty to AudioInputState.OFF
)

// Tap the amplitude button to unmute
composeTestRule.onNodeWithTag(AUDIO_INPUT_TOGGLE).performClick()

// Verify amplitude button is hot again
composeTestRule.waitForNodeWithTagAndSemantics(
tag = AUDIO_INPUT_TOGGLE,
semanticsProperty = AudioStateProperty to AudioInputState.INCOMING
)

// Stop recording
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).performClick()

composeTestRule.waitForSnackbarWithText(
R.string.toast_video_capture_success,
VIDEO_CAPTURE_TIMEOUT_MILLIS
)
}
}

@Test
fun startRecordingMuted() {
runMainActivityScenarioTest(debugExtra) {
composeTestRule.waitForCaptureButton()

// Verify amplitude button is hot initially (audio is enabled by default)
composeTestRule.waitForNodeWithTagAndSemantics(
tag = AUDIO_INPUT_TOGGLE,
semanticsProperty = AudioStateProperty to AudioInputState.READY
)

// Tap the amplitude button to mute
composeTestRule.onNodeWithTag(AUDIO_INPUT_TOGGLE).performClick()

// Verify amplitude button is now showing "none/muted" tag
composeTestRule.waitForNodeWithTagAndSemantics(
tag = AUDIO_INPUT_TOGGLE,
semanticsProperty = AudioStateProperty to AudioInputState.OFF
)

// Start video recording
composeTestRule.pressAndDragToLockVideoRecording()

// Verify it remains muted during recording
// Verify amplitude button is now showing "none/muted" tag
composeTestRule.waitForNodeWithTagAndSemantics(
tag = AUDIO_INPUT_TOGGLE,
semanticsProperty = AudioStateProperty to AudioInputState.OFF
)

// Stop recording
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).performClick()
composeTestRule.waitForSnackbarWithText(
R.string.toast_video_capture_success,
VIDEO_CAPTURE_TIMEOUT_MILLIS
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
* limitations under the License.
*/
package com.google.jetpackcamera.utils

import android.content.Context
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.test.ComposeTimeoutException
Expand Down Expand Up @@ -147,15 +149,40 @@ fun ComposeTestRule.wait(timeoutMillis: Long) {
/* do nothing, we just want to time out*/
}
}

fun ComposeTestRule.waitForCaptureButton(timeoutMillis: Long = APP_START_TIMEOUT_MILLIS) {
// Wait for the capture button to be displayed and enabled
waitUntil(timeoutMillis = timeoutMillis) {
onNode(hasTestTag(CAPTURE_BUTTON) and isEnabled()).isDisplayed()
}
}

fun ComposeTestRule.waitForNodeWithTag(tag: String, timeoutMillis: Long = DEFAULT_TIMEOUT_MILLIS) {
waitUntil(timeoutMillis = timeoutMillis) { onNodeWithTag(tag).isDisplayed() }
fun ComposeTestRule.waitForNodeWithTag(
tag: String,
timeoutMillis: Long = DEFAULT_TIMEOUT_MILLIS,
stateDescription: String? = null
) {
waitUntil(timeoutMillis = timeoutMillis) {
var matcher = hasTestTag(tag)
if (stateDescription != null) {
matcher = matcher and hasStateDescription(stateDescription)
}
onNode(matcher).isDisplayed()
}
}

fun <T> ComposeTestRule.waitForNodeWithTagAndSemantics(
tag: String,
timeoutMillis: Long = DEFAULT_TIMEOUT_MILLIS,
semanticsProperty: Pair<SemanticsPropertyKey<T>, T>
) {
waitUntil(timeoutMillis = timeoutMillis) {
val matcher = hasTestTag(tag) and SemanticsMatcher.expectValue(
semanticsProperty.first,
semanticsProperty.second
)
onNode(matcher).isDisplayed()
}
}

fun ComposeTestRule.waitForNodeWithTagToDisappear(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,6 @@ private fun getPendingRecording(
context(CameraSessionContext)
@OptIn(ExperimentalPersistentRecording::class)
private suspend fun startVideoRecordingInternal(
isInitialAudioEnabled: Boolean,
context: Context,
pendingRecord: PendingRecording,
maxDurationMillis: Long,
Expand All @@ -1027,7 +1026,7 @@ private suspend fun startVideoRecordingInternal(

pendingRecord.apply {
if (isAudioGranted) {
withAudioEnabled(isInitialAudioEnabled)
withAudioEnabled(initialMuted = !initialRecordingSettings.isAudioEnabled)
}
}
.asPersistentRecording()
Expand Down Expand Up @@ -1166,8 +1165,6 @@ private suspend fun startVideoRecordingInternal(
}
}
}
}.apply {
mute(!isInitialAudioEnabled)
}
}

Expand Down Expand Up @@ -1195,7 +1192,6 @@ private suspend fun runVideoRecording(
onVideoRecord
)?.let {
startVideoRecordingInternal(
isInitialAudioEnabled = currentSettings.isAudioEnabled,
context = context,
pendingRecord = it,
maxDurationMillis = maxDurationMillis,
Expand All @@ -1212,13 +1208,10 @@ private suspend fun runVideoRecording(
transientSettings.filterNotNull()
.collectLatest { newTransientSettings ->
if (currentSettings.isAudioEnabled != newTransientSettings.isAudioEnabled) {
recording.mute(newTransientSettings.isAudioEnabled)
}
if (currentSettings.isFlashModeOn() !=
newTransientSettings.isFlashModeOn()
) {
currentSettings = newTransientSettings
// audio mute state will be inverse of if audio is enabled.
recording.mute(!newTransientSettings.isAudioEnabled)
}
currentSettings = newTransientSettings
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -236,11 +237,18 @@ fun AmplitudeToggleButton(
FilledIconToggleButton(
modifier = modifier
.size(buttonSize)
.apply {
if (audioUiState is AudioUiState.Enabled.On) {
testTag(AMPLITUDE_HOT_TAG)
} else {
testTag(AMPLITUDE_NONE_TAG)
.testTag(AUDIO_INPUT_TOGGLE)
.semantics {
Comment thread
Kimblebee marked this conversation as resolved.
audioState = when (audioUiState) {
is AudioUiState.Disabled,
AudioUiState.Enabled.Mute -> AudioInputState.OFF
is AudioUiState.Enabled.On -> {
if (audioUiState.amplitude > 0.0) {
AudioInputState.INCOMING
} else {
AudioInputState.READY
}
}
}
}
.drawBehind {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.google.jetpackcamera.ui.components.capture

import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
const val CAPTURE_BUTTON = "CaptureButton"
const val CAPTURE_MODE_TOGGLE_BUTTON = "CaptureModeToggleButton"
const val FLIP_CAMERA_BUTTON = "FlipCameraButton"
Expand All @@ -24,10 +26,18 @@ const val IMAGE_WELL_TAG = "ImageWellTag"

const val PREVIEW_DISPLAY = "PreviewDisplay"
const val SCREEN_FLASH_OVERLAY = "ScreenFlashOverlay"
const val AMPLITUDE_NONE_TAG = "AmplitudeNoneTag"
const val AMPLITUDE_HOT_TAG = "AmplitudeHotTag"
const val AUDIO_INPUT_TOGGLE = "AudioInputToggle"
Comment thread
Kimblebee marked this conversation as resolved.
const val FOCUS_METERING_INDICATOR_TAG = "FocusMeteringIndicatorTag"

enum class AudioInputState {
OFF,
READY,
INCOMING
}

val AudioStateProperty = SemanticsPropertyKey<AudioInputState>("AudioState")
var SemanticsPropertyReceiver.audioState by AudioStateProperty

const val ZOOM_BUTTON_ROW_TAG = "ZoomButtonRowTag"
const val ZOOM_BUTTON_MIN_TAG = "ZoomButtonMinTag"
const val ZOOM_BUTTON_1_TAG = "ZoomButton1Tag"
Expand Down
Loading