summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKimberly Crevecoeur <kcrevecoeur@google.com>2024-01-08 13:12:53 -0500
committerGitHub <noreply@github.com>2024-01-08 13:12:53 -0500
commit7109bdea07c6369a2682f0e455af20f7d3436772 (patch)
treed16e1f70b2d9eefa15e5e2df8c8f551ffa8ccd82
parent6aef048c6f6521f806a151f5fed4de8fdb0b37a5 (diff)
downloadjetpack-camera-app-7109bdea07c6369a2682f0e455af20f7d3436772.tar.gz
Image capture benchmark (#85)
Benchmarks to measure the time between an onClick event on the Capture Button and onImageCapture callback being fired. * Front / Rear camera image capture with and without flash. * Macrobenchmark UI automator helper functions to set up camera for testing.
-rw-r--r--benchmark/build.gradle.kts4
-rw-r--r--benchmark/src/main/java/com/google/jetpackcamera/benchmark/ImageCaptureLatencyBenchmark.kt99
-rw-r--r--benchmark/src/main/java/com/google/jetpackcamera/benchmark/Permissions.kt (renamed from benchmark/src/main/java/com/example/benchmark/Permissions.kt)11
-rw-r--r--benchmark/src/main/java/com/google/jetpackcamera/benchmark/StartupBenchmark.kt (renamed from benchmark/src/main/java/com/example/benchmark/StartupBenchmark.kt)14
-rw-r--r--benchmark/src/main/java/com/google/jetpackcamera/benchmark/Utils.kt152
-rw-r--r--domain/camera/build.gradle.kts1
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt2
-rw-r--r--feature/preview/build.gradle.kts1
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt22
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt45
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt35
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt20
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ToastMessage.kt2
-rw-r--r--feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt7
-rw-r--r--feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt21
15 files changed, 375 insertions, 61 deletions
diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts
index bd20c97..416dced 100644
--- a/benchmark/build.gradle.kts
+++ b/benchmark/build.gradle.kts
@@ -20,7 +20,7 @@ plugins {
}
android {
- namespace = "com.example.benchmark"
+ namespace = "com.google.jetpackcamera.benchmark"
compileSdk = 34
compileOptions {
@@ -65,7 +65,7 @@ android {
dependencies {
implementation("androidx.test.ext:junit:1.1.5")
- implementation("androidx.benchmark:benchmark-macro-junit4:1.2.1")
+ implementation("androidx.benchmark:benchmark-macro-junit4:1.2.2")
}
androidComponents {
diff --git a/benchmark/src/main/java/com/google/jetpackcamera/benchmark/ImageCaptureLatencyBenchmark.kt b/benchmark/src/main/java/com/google/jetpackcamera/benchmark/ImageCaptureLatencyBenchmark.kt
new file mode 100644
index 0000000..378bb6e
--- /dev/null
+++ b/benchmark/src/main/java/com/google/jetpackcamera/benchmark/ImageCaptureLatencyBenchmark.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 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.benchmark
+
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.TraceSectionMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ImageCaptureLatencyBenchmark {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun rearCameraNoFlashLatency() {
+ imageCaptureLatency(shouldFaceFront = false, flashMode = FlashMode.OFF)
+ }
+
+ @Test
+ fun frontCameraNoFlashLatency() {
+ imageCaptureLatency(shouldFaceFront = true, flashMode = FlashMode.OFF)
+ }
+
+ @Test
+ fun rearCameraWithFlashLatency() {
+ imageCaptureLatency(shouldFaceFront = false, flashMode = FlashMode.ON)
+ }
+
+ @Test
+ fun frontCameraWithFlashLatency() {
+ imageCaptureLatency(shouldFaceFront = true, flashMode = FlashMode.ON)
+ }
+
+ /**
+ * Measures the time between an onClick event on the Capture Button and onImageCapture
+ * callback being fired from
+ * [takePicture][com.google.jetpackcamera.domain.camera.CameraXCameraUseCase.takePicture].
+ *
+ * @param shouldFaceFront the direction the camera should be facing.
+ * @param flashMode the designated [FlashMode] for the camera.
+ * @param timeout option to change the default timeout length after clicking the Image Capture
+ * button.
+ *
+ */
+ @OptIn(ExperimentalMetricApi::class)
+ private fun imageCaptureLatency(
+ shouldFaceFront: Boolean,
+ flashMode: FlashMode,
+ timeout: Long = 15000
+ ) {
+ benchmarkRule.measureRepeated(
+ packageName = JCA_PACKAGE_NAME,
+ metrics = listOf(
+ TraceSectionMetric(sectionName = IMAGE_CAPTURE_TRACE, targetPackageOnly = false)
+ ),
+ iterations = DEFAULT_TEST_ITERATIONS,
+ setupBlock = {
+ allowCamera()
+ pressHome()
+ startActivityAndWait()
+ toggleQuickSettings(device)
+ setQuickFrontFacingCamera(shouldFaceFront = shouldFaceFront, device = device)
+ setQuickSetFlash(flashMode = flashMode, device = device)
+ toggleQuickSettings(device)
+ device.waitForIdle()
+ }
+
+ ) {
+ device.waitForIdle()
+
+ clickCaptureButton(device)
+
+ // ensure trace is closed
+ findObjectByRes(
+ device = device,
+ testTag = IMAGE_CAPTURE_SUCCESS_TOAST,
+ timeout = timeout,
+ shouldFailIfNotFound = true
+ )
+ }
+ }
+}
diff --git a/benchmark/src/main/java/com/example/benchmark/Permissions.kt b/benchmark/src/main/java/com/google/jetpackcamera/benchmark/Permissions.kt
index 9a6d595..af8d7d5 100644
--- a/benchmark/src/main/java/com/example/benchmark/Permissions.kt
+++ b/benchmark/src/main/java/com/google/jetpackcamera/benchmark/Permissions.kt
@@ -13,17 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.example.benchmark
+package com.google.jetpackcamera.benchmark
import android.Manifest.permission
-import android.os.Build
import androidx.benchmark.macro.MacrobenchmarkScope
import org.junit.Assert
fun MacrobenchmarkScope.allowCamera() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- val command = "pm grant $packageName ${permission.CAMERA}"
- val output = device.executeShellCommand(command)
- Assert.assertEquals("", output)
- }
+ val command = "pm grant $packageName ${permission.CAMERA}"
+ val output = device.executeShellCommand(command)
+ Assert.assertEquals("", output)
}
diff --git a/benchmark/src/main/java/com/example/benchmark/StartupBenchmark.kt b/benchmark/src/main/java/com/google/jetpackcamera/benchmark/StartupBenchmark.kt
index 4b5e4f8..f8c1f71 100644
--- a/benchmark/src/main/java/com/example/benchmark/StartupBenchmark.kt
+++ b/benchmark/src/main/java/com/google/jetpackcamera/benchmark/StartupBenchmark.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.example.benchmark
+package com.google.jetpackcamera.benchmark
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.StartupMode
@@ -25,14 +25,6 @@ import org.junit.Test
import org.junit.runner.RunWith
/**
- * This is an example startup benchmark.
- *
- * It navigates to the device's home screen, and launches the default activity.
- *
- * Before running this benchmark:
- * 1) switch your app's active build variant in the Studio (affects Studio runs only)
- * 2) add `<profileable android:shell="true" />` to your app's manifest, within the `<application>` tag
- *
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance.
*/
@@ -77,9 +69,9 @@ class StartupBenchmark {
startupMode: StartupMode? = StartupMode.COLD
) {
benchmarkRule.measureRepeated(
- packageName = "com.google.jetpackcamera",
+ packageName = JCA_PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()),
- iterations = 5,
+ iterations = DEFAULT_TEST_ITERATIONS,
startupMode = startupMode,
setupBlock = setupBlock
diff --git a/benchmark/src/main/java/com/google/jetpackcamera/benchmark/Utils.kt b/benchmark/src/main/java/com/google/jetpackcamera/benchmark/Utils.kt
new file mode 100644
index 0000000..4d6f8fd
--- /dev/null
+++ b/benchmark/src/main/java/com/google/jetpackcamera/benchmark/Utils.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 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.benchmark
+
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import org.junit.Assert
+
+const val JCA_PACKAGE_NAME = "com.google.jetpackcamera"
+const val DEFAULT_TEST_ITERATIONS = 5
+
+// test tags
+const val CAPTURE_BUTTON = "CaptureButton"
+const val QUICK_SETTINGS_DROP_DOWN_BUTTON = "QuickSettingDropDown"
+const val QUICK_SETTINGS_FLASH_BUTTON = "QuickSetFlash"
+const val QUICK_SETTINGS_FLIP_CAMERA_BUTTON = "QuickSetFlipCamera"
+const val IMAGE_CAPTURE_SUCCESS_TOAST = "ImageCaptureSuccessToast"
+
+// test descriptions
+const val QUICK_SETTINGS_FLASH_OFF = "QUICK SETTINGS FLASH IS OFF"
+const val QUICK_SETTINGS_FLASH_ON = "QUICK SETTINGS FLASH IS ON"
+const val QUICK_SETTINGS_FLASH_AUTO = "QUICK SETTINGS FLASH IS AUTO"
+const val QUICK_SETTINGS_LENS_FRONT = "QUICK SETTINGS LENS FACING FRONT"
+
+// trace tags
+const val IMAGE_CAPTURE_TRACE = "JCA Image Capture"
+
+// enums
+enum class FlashMode {
+ ON,
+ OFF,
+ AUTO
+}
+// todo(kimblebee): designate "default testing settings" to ensure consistency of benchmarks
+
+/**
+ * function to click capture button on device.
+ *
+ * @param duration length of the click.
+ */
+fun clickCaptureButton(device: UiDevice, duration: Long = 0) {
+ findObjectByRes(device, CAPTURE_BUTTON)!!.click(duration)
+}
+
+/**
+ * Toggle open or close quick settings menu on a device.
+ */
+fun toggleQuickSettings(device: UiDevice) {
+ findObjectByRes(
+ device = device,
+ testTag = QUICK_SETTINGS_DROP_DOWN_BUTTON,
+ shouldFailIfNotFound = true
+ )!!.click()
+}
+
+/**
+ * Set device direction using quick settings.
+ *
+ * Quick Settings must first be opened with a call to [toggleQuickSettings]
+ *
+ * @param shouldFaceFront the direction the camera should be facing
+ */
+fun setQuickFrontFacingCamera(shouldFaceFront: Boolean, device: UiDevice) {
+ val isFrontFacing = findObjectByDesc(device, QUICK_SETTINGS_LENS_FRONT) != null
+
+ if (isFrontFacing != shouldFaceFront) {
+ findObjectByRes(
+ device = device,
+ testTag = QUICK_SETTINGS_FLIP_CAMERA_BUTTON,
+ shouldFailIfNotFound = true
+ )!!.click()
+ }
+}
+
+/**
+ * Set device flash mode using quick settings.
+ * @param flashMode the designated [FlashMode] for the camera
+ *
+ */
+
+fun setQuickSetFlash(flashMode: FlashMode, device: UiDevice) {
+ val selector =
+ when (flashMode) {
+ FlashMode.AUTO -> By.desc(QUICK_SETTINGS_FLASH_AUTO)
+ FlashMode.ON -> By.desc(QUICK_SETTINGS_FLASH_ON)
+ FlashMode.OFF -> By.desc(QUICK_SETTINGS_FLASH_OFF)
+ }
+ while (device.findObject(selector) == null) {
+ findObjectByRes(
+ device = device,
+ testTag = QUICK_SETTINGS_FLASH_BUTTON,
+ shouldFailIfNotFound = true
+ )!!.click()
+ }
+}
+
+/**
+ * Find a composable by its test tag.
+ */
+fun findObjectByRes(
+ device: UiDevice,
+ testTag: String,
+ timeout: Long = 2_500,
+ shouldFailIfNotFound: Boolean = false
+): UiObject2? {
+ val selector = By.res(testTag)
+
+ return if (!device.wait(Until.hasObject(selector), timeout)) {
+ if (shouldFailIfNotFound) {
+ Assert.fail("Did not find object with id $testTag")
+ }
+ null
+ } else {
+ device.findObject(selector)
+ }
+}
+
+/**
+ * Find an object by its test description.
+ */
+fun findObjectByDesc(
+ device: UiDevice,
+ testDesc: String,
+ timeout: Long = 2_500,
+ shouldFailIfNotFound: Boolean = false
+): UiObject2? {
+ val selector = By.desc(testDesc)
+
+ return if (!device.wait(Until.hasObject(selector), timeout)) {
+ if (shouldFailIfNotFound) {
+ Assert.fail("Did not find object with id $testDesc in $timeout ms")
+ }
+ null
+ } else {
+ device.findObject(selector)
+ }
+}
diff --git a/domain/camera/build.gradle.kts b/domain/camera/build.gradle.kts
index 4a6bacb..883612e 100644
--- a/domain/camera/build.gradle.kts
+++ b/domain/camera/build.gradle.kts
@@ -52,6 +52,7 @@ android {
}
dependencies {
+ implementation("androidx.tracing:tracing-ktx:1.2.0")
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6")
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt
index 1517d16..1e5d840 100644
--- a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt
@@ -62,6 +62,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
private const val TAG = "CameraXCameraUseCase"
+private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture"
/**
* CameraX based implementation for [CameraUseCase]
@@ -185,7 +186,6 @@ constructor(
)
.setContentValues(contentValues)
.build()
-
recording =
videoCaptureUseCase.output
.prepareRecording(application, mediaStoreOutput)
diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts
index 5cec765..e9a90eb 100644
--- a/feature/preview/build.gradle.kts
+++ b/feature/preview/build.gradle.kts
@@ -64,6 +64,7 @@ android {
}
dependencies {
+ implementation("androidx.tracing:tracing-ktx:1.2.0")
// Compose
val composeBom = platform("androidx.compose:compose-bom:2023.08.00")
implementation(composeBom)
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt
index 70341ea..e0521c1 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt
@@ -55,12 +55,13 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON
import com.google.jetpackcamera.feature.preview.ui.CaptureButton
import com.google.jetpackcamera.feature.preview.ui.FlipCameraButton
import com.google.jetpackcamera.feature.preview.ui.PreviewDisplay
import com.google.jetpackcamera.feature.preview.ui.ScreenFlashScreen
import com.google.jetpackcamera.feature.preview.ui.SettingsNavButton
-import com.google.jetpackcamera.feature.preview.ui.ShowToast
+import com.google.jetpackcamera.feature.preview.ui.ShowTestableToast
import com.google.jetpackcamera.feature.preview.ui.TestingButton
import com.google.jetpackcamera.feature.preview.ui.ZoomScaleText
import com.google.jetpackcamera.feature.quicksettings.QuickSettingsScreen
@@ -234,6 +235,8 @@ fun PreviewScreen(
val multipleEventsCutter = remember { MultipleEventsCutter() }
/*todo: close quick settings on start record/image capture*/
CaptureButton(
+ modifier = Modifier
+ .testTag(CAPTURE_BUTTON),
onClick = {
multipleEventsCutter.processEvent { viewModel.captureImage() }
},
@@ -253,14 +256,15 @@ fun PreviewScreen(
)
}
}
- }
-
- // displays toast when there is a message to show
- if (previewUiState.toastMessageToShow != null) {
- ShowToast(
- toastMessage = previewUiState.toastMessageToShow!!,
- onToastShown = viewModel::onToastShown
- )
+ // displays toast when there is a message to show
+ if (previewUiState.toastMessageToShow != null) {
+ ShowTestableToast(
+ modifier = Modifier
+ .testTag(previewUiState.toastMessageToShow!!.testTag),
+ toastMessage = previewUiState.toastMessageToShow!!,
+ onToastShown = viewModel::onToastShown
+ )
+ }
}
// Screen flash overlay that stays on top of everything but invisible normally. This should
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
index f8de985..e491ab4 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
@@ -21,6 +21,7 @@ import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview.SurfaceProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import androidx.tracing.traceAsync
import com.google.jetpackcamera.domain.camera.CameraUseCase
import com.google.jetpackcamera.feature.preview.ui.ToastMessage
import com.google.jetpackcamera.settings.SettingsRepository
@@ -30,12 +31,15 @@ import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
import com.google.jetpackcamera.settings.model.FlashMode
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
private const val TAG = "PreviewViewModel"
+private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture"
// toast test descriptions
const val IMAGE_CAPTURE_SUCCESS_TOAST_TAG = "ImageCaptureSuccessToast"
@@ -201,27 +205,32 @@ class PreviewViewModel @Inject constructor(
fun captureImage() {
Log.d(TAG, "captureImage")
viewModelScope.launch {
- try {
- cameraUseCase.takePicture()
- // todo: remove toast after postcapture screen implemented
- _previewUiState.emit(
- previewUiState.value.copy(
- toastMessageToShow = ToastMessage(
- stringResource = R.string.toast_image_capture_success,
- testTag = IMAGE_CAPTURE_SUCCESS_TOAST_TAG
+ traceAsync(IMAGE_CAPTURE_TRACE, 0) {
+ try {
+ cameraUseCase.takePicture()
+ // todo: remove toast after postcapture screen implemented
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ toastMessageToShow = ToastMessage(
+ stringResource = R.string.toast_image_capture_success,
+ testTag = IMAGE_CAPTURE_SUCCESS_TOAST_TAG
+ )
)
)
- )
- } catch (exception: ImageCaptureException) {
- // todo: remove toast after postcapture screen implemented
- _previewUiState.emit(
- previewUiState.value.copy(
- toastMessageToShow = ToastMessage(
- stringResource = R.string.toast_capture_failure,
- testTag = IMAGE_CAPTURE_FAIL_TOAST_TAG
+ Log.d(TAG, "cameraUseCase.takePicture success")
+ } catch (exception: ImageCaptureException) {
+ // todo: remove toast after postcapture screen implemented
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ toastMessageToShow = ToastMessage(
+ stringResource = R.string.toast_capture_failure,
+ testTag = IMAGE_CAPTURE_FAIL_TOAST_TAG
+ )
)
)
- )
+ Log.d(TAG, "cameraUseCase.takePicture error")
+ Log.d(TAG, exception.toString())
+ }
}
}
}
@@ -288,6 +297,8 @@ class PreviewViewModel @Inject constructor(
fun onToastShown() {
viewModelScope.launch {
+ // keeps the composable up on screen longer to be detected by UiAutomator
+ delay(2.seconds)
_previewUiState.emit(
previewUiState.value.copy(
toastMessageToShow = null
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt
index 06f3d4c..6d16f1d 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt
@@ -46,6 +46,8 @@ import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -54,7 +56,6 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.jetpackcamera.feature.preview.R
@@ -66,24 +67,37 @@ import kotlinx.coroutines.CompletableDeferred
private const val TAG = "PreviewScreen"
/**
- * Displays a [Toast] with specifications set by a [ToastMessage].
+ * An invisible box that will display a [Toast] with specifications set by a [ToastMessage].
*
* @param toastMessage the specifications for the [Toast].
* @param onToastShown called once the Toast has been displayed.
*/
@Composable
-fun ShowToast(modifier: Modifier = Modifier, toastMessage: ToastMessage, onToastShown: () -> Unit) {
+fun ShowTestableToast(
+ modifier: Modifier = Modifier,
+ toastMessage: ToastMessage,
+ onToastShown: () -> Unit
+) {
+ val toastShownStatus = remember { mutableStateOf(false) }
Box(
- modifier
+ // box seems to need to have some size to be detected by UiAutomator
+ modifier = modifier
+ .size(20.dp)
.testTag(toastMessage.testTag)
) {
- Toast.makeText(
- LocalContext.current,
- stringResource(id = toastMessage.stringResource),
- toastMessage.toastLength
- ).show()
- onToastShown()
+ // prevents toast from being spammed
+ if (!toastShownStatus.value) {
+ Toast.makeText(
+ LocalContext.current,
+ stringResource(id = toastMessage.stringResource),
+ toastMessage.toastLength
+ )
+ .show()
+ toastShownStatus.value = true
+ onToastShown()
+ }
}
+ Log.d(TAG, "Toast Displayed with message: ${stringResource(id = toastMessage.stringResource)}")
}
/**
@@ -246,7 +260,6 @@ fun CaptureButton(
) {
Box(
modifier = modifier
- .testTag("CaptureButton")
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt
new file mode 100644
index 0000000..da4204a
--- /dev/null
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2023 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.feature.preview.ui
+
+const val CAPTURE_BUTTON = "CaptureButton"
+const val SETTINGS_BUTTON = "SettingsButton"
+const val DEFAULT_CAMERA_FACING_SETTING = "SetDefaultCameraFacingSwitch"
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ToastMessage.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ToastMessage.kt
index f86a8dd..b7003da 100644
--- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ToastMessage.kt
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ToastMessage.kt
@@ -22,7 +22,7 @@ import android.widget.Toast
*
* @param stringResource the resource ID of to be displayed.
* @param isLongToast determines if the display time is [Toast.LENGTH_LONG] or [Toast.LENGTH_SHORT].
- * @property testTag the identifiable resource ID of a [ShowToast] on screen.
+ * @property testTag the identifiable resource ID of a [ShowTestableToast] on screen.
*/
class ToastMessage(
val stringResource: Int,
diff --git a/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt
index b2b9f32..6b0ba2f 100644
--- a/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt
+++ b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt
@@ -30,11 +30,14 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
import com.google.jetpackcamera.feature.quicksettings.ui.DropDownIcon
import com.google.jetpackcamera.feature.quicksettings.ui.ExpandedQuickSetRatio
import com.google.jetpackcamera.feature.quicksettings.ui.QuickFlipCamera
@@ -49,6 +52,7 @@ import com.google.jetpackcamera.settings.model.FlashMode
/**
* The UI component for quick settings.
*/
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun QuickSettingsScreen(
modifier: Modifier = Modifier,
@@ -83,6 +87,9 @@ fun QuickSettingsScreen(
.fillMaxSize()
.background(color = backgroundColor.value)
.alpha(alpha = contentAlpha.value)
+ .semantics {
+ testTagsAsResourceId = true
+ }
.clickable {
// if a setting is expanded, click on the background to close it.
// if no other settings are expanded, then close the popup
diff --git a/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt
index 46349d1..d3bb28c 100644
--- a/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt
+++ b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt
@@ -38,6 +38,8 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.google.jetpackcamera.feature.quicksettings.CameraAspectRatio
import com.google.jetpackcamera.feature.quicksettings.CameraFlashMode
@@ -119,7 +121,15 @@ fun QuickSetFlash(
FlashMode.ON -> CameraFlashMode.ON
}
QuickSettingUiItem(
- modifier = modifier,
+ modifier = modifier
+ .semantics {
+ contentDescription =
+ when (enum) {
+ CameraFlashMode.OFF -> "QUICK SETTINGS FLASH IS OFF"
+ CameraFlashMode.AUTO -> "QUICK SETTINGS FLASH IS AUTO"
+ CameraFlashMode.ON -> "QUICK SETTINGS FLASH IS ON"
+ }
+ },
enum = enum,
isHighLighted = currentFlashMode == FlashMode.ON,
onClick =
@@ -141,7 +151,14 @@ fun QuickFlipCamera(
false -> CameraLensFace.BACK
}
QuickSettingUiItem(
- modifier = modifier,
+ modifier = modifier
+ .semantics {
+ contentDescription =
+ when (enum) {
+ CameraLensFace.FRONT -> "QUICK SETTINGS LENS FACING FRONT"
+ CameraLensFace.BACK -> "QUICK SETTINGS LENS FACING BACK"
+ }
+ },
enum = enum,
onClick = { flipCamera(!currentFacingFront) }
)