diff options
author | Kimberly Crevecoeur <kcrevecoeur@google.com> | 2024-01-08 13:12:53 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-08 13:12:53 -0500 |
commit | 7109bdea07c6369a2682f0e455af20f7d3436772 (patch) | |
tree | d16e1f70b2d9eefa15e5e2df8c8f551ffa8ccd82 | |
parent | 6aef048c6f6521f806a151f5fed4de8fdb0b37a5 (diff) | |
download | jetpack-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.
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) } ) |