From 802d350c34955a18f68a02aa944359a068343344 Mon Sep 17 00:00:00 2001 From: Jolanda Verhoef Date: Mon, 15 Apr 2024 20:17:57 +0100 Subject: [PATCH 1/2] Update versions and version naming (#156) * Update versions and version naming * Fixes #55. Latest ktlint and spotless do not complain about composable naming. * Get rid of lint warnings * Fix Compose issues (mostly modifier order) * fix spotless --- .idea/kotlinc.xml | 2 +- app/build.gradle.kts | 19 +-- .../jetpackcamera/MainActivityViewModel.kt | 4 +- .../com/google/jetpackcamera/ui/JcaApp.kt | 13 +- .../google/jetpackcamera/ui/PermissionsUi.kt | 24 +-- benchmark/build.gradle.kts | 4 +- build.gradle.kts | 10 ++ core/common/build.gradle.kts | 7 +- data/settings/build.gradle.kts | 8 +- .../settings/model/AspectRatio.kt | 6 +- domain/camera/build.gradle.kts | 13 +- domain/camera/src/main/AndroidManifest.xml | 2 +- .../domain/camera/test/FakeCameraUseCase.kt | 3 - feature/preview/build.gradle.kts | 12 +- .../feature/preview/PreviewScreen.kt | 142 +++++++++--------- .../preview/ui/CameraControlsOverlay.kt | 17 ++- .../feature/preview/ui/CameraXViewfinder.kt | 2 +- .../preview/ui/PreviewScreenComponents.kt | 27 ++-- .../preview/ui/ScreenFlashComponents.kt | 7 +- .../workaround/ComposableCaptureToImage.kt | 55 ++++--- feature/quicksettings/build.gradle.kts | 10 +- .../quicksettings/QuickSettingsScreen.kt | 6 +- .../ui/QuickSettingsComponents.kt | 46 +++--- feature/settings/build.gradle.kts | 11 +- .../jetpackcamera/settings/SettingsScreen.kt | 2 +- .../settings/ui/SettingsComponents.kt | 61 +++++--- gradle.properties | 8 +- gradle/init.gradle.kts | 4 +- gradle/libs.versions.toml | 104 +++++++------ settings.gradle.kts | 1 + 30 files changed, 356 insertions(+), 274 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 2b8a50fc..8d81632f 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dea2a004..6f86f9b8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,23 +22,22 @@ plugins { } android { + compileSdk = libs.versions.compileSdk.get().toInt() namespace = "com.google.jetpackcamera" - compileSdk = 34 defaultConfig { applicationId = "com.google.jetpackcamera" - minSdk = 21 - targetSdk = 34 + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() versionCode = 1 versionName = "0.1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary = true - } } buildTypes { + getByName("debug") { + signingConfig = signingConfigs.getByName("debug") + } getByName("release") { isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) @@ -57,16 +56,18 @@ android { jvmToolchain(17) } buildFeatures { + buildConfig = true compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.0" + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() } - packagingOptions { + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } + @Suppress("UnstableApiUsage") testOptions { managedDevices { localDevices { diff --git a/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt b/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt index d18dbec1..5d89e1ad 100644 --- a/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt +++ b/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.stateIn @HiltViewModel class MainActivityViewModel @Inject constructor( - val settingsRepository: SettingsRepository + settingsRepository: SettingsRepository ) : ViewModel() { val uiState: StateFlow = settingsRepository.cameraAppSettings.map { Success(it) @@ -42,6 +42,6 @@ class MainActivityViewModel @Inject constructor( } sealed interface MainActivityUiState { - object Loading : MainActivityUiState + data object Loading : MainActivityUiState data class Success(val cameraAppSettings: CameraAppSettings) : MainActivityUiState } diff --git a/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt b/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt index 538577c0..bdef4e38 100644 --- a/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt +++ b/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt @@ -38,10 +38,11 @@ import com.google.jetpackcamera.ui.Routes.SETTINGS_ROUTE @OptIn(ExperimentalPermissionsApi::class) @Composable fun JcaApp( + /*TODO(b/306236646): remove after still capture*/ previewMode: PreviewMode, onPreviewViewModel: (PreviewViewModel) -> Unit, - onRequestWindowColorMode: (Int) -> Unit - /*TODO(b/306236646): remove after still capture*/ + onRequestWindowColorMode: (Int) -> Unit, + modifier: Modifier = Modifier ) { val permissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) @@ -50,11 +51,12 @@ fun JcaApp( JetpackCameraNavHost( onPreviewViewModel = onPreviewViewModel, previewMode = previewMode, - onRequestWindowColorMode = onRequestWindowColorMode + onRequestWindowColorMode = onRequestWindowColorMode, + modifier = modifier ) } else { CameraPermission( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), cameraPermissionState = permissionState ) } @@ -65,9 +67,10 @@ private fun JetpackCameraNavHost( previewMode: PreviewMode, onPreviewViewModel: (PreviewViewModel) -> Unit, onRequestWindowColorMode: (Int) -> Unit, + modifier: Modifier = Modifier, navController: NavHostController = rememberNavController() ) { - NavHost(navController = navController, startDestination = PREVIEW_ROUTE) { + NavHost(navController = navController, startDestination = PREVIEW_ROUTE, modifier = modifier) { composable(PREVIEW_ROUTE) { PreviewScreen( onPreviewViewModel = onPreviewViewModel, diff --git a/app/src/main/java/com/google/jetpackcamera/ui/PermissionsUi.kt b/app/src/main/java/com/google/jetpackcamera/ui/PermissionsUi.kt index 5100dff9..6877ed0d 100644 --- a/app/src/main/java/com/google/jetpackcamera/ui/PermissionsUi.kt +++ b/app/src/main/java/com/google/jetpackcamera/ui/PermissionsUi.kt @@ -49,7 +49,7 @@ import com.google.jetpackcamera.R @OptIn(ExperimentalPermissionsApi::class) @Composable -fun CameraPermission(modifier: Modifier = Modifier, cameraPermissionState: PermissionState) { +fun CameraPermission(cameraPermissionState: PermissionState, modifier: Modifier = Modifier) { PermissionTemplate( modifier = modifier, permissionState = cameraPermissionState, @@ -64,14 +64,14 @@ fun CameraPermission(modifier: Modifier = Modifier, cameraPermissionState: Permi @OptIn(ExperimentalPermissionsApi::class) @Composable fun PermissionTemplate( - modifier: Modifier = Modifier, permissionState: PermissionState, - onSkipPermission: (() -> Unit)? = null, painter: Painter, iconAccessibilityText: String, title: String, bodyText: String, - requestButtonText: String + requestButtonText: String, + modifier: Modifier = Modifier, + onSkipPermission: (() -> Unit)? = null ) { Column( modifier = modifier.background(MaterialTheme.colorScheme.primary), @@ -110,7 +110,7 @@ fun PermissionTemplate( } @Composable -fun PermissionImage(modifier: Modifier = Modifier, painter: Painter, accessibilityText: String) { +fun PermissionImage(painter: Painter, accessibilityText: String, modifier: Modifier = Modifier) { Box(modifier = modifier) { Icon( modifier = Modifier @@ -126,9 +126,9 @@ fun PermissionImage(modifier: Modifier = Modifier, painter: Painter, accessibili @OptIn(ExperimentalPermissionsApi::class) @Composable fun PermissionButtonSection( - modifier: Modifier = Modifier, permissionState: PermissionState, requestButtonText: String, + modifier: Modifier = Modifier, onSkipPermission: (() -> Unit)? ) { Box(modifier = modifier) { @@ -159,9 +159,9 @@ fun PermissionButtonSection( @OptIn(ExperimentalPermissionsApi::class) @Composable fun PermissionButton( - modifier: Modifier = Modifier, permissionState: PermissionState, - requestButtonText: String + requestButtonText: String, + modifier: Modifier = Modifier ) { Button( modifier = modifier, @@ -181,13 +181,13 @@ fun PermissionButton( } @Composable -fun PermissionText(modifier: Modifier = Modifier, title: String, bodyText: String) { +fun PermissionText(title: String, bodyText: String, modifier: Modifier = Modifier) { Box( modifier = modifier .height(IntrinsicSize.Min) ) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() .align(Alignment.Center) ) { @@ -213,7 +213,7 @@ fun PermissionText(modifier: Modifier = Modifier, title: String, bodyText: Strin } @Composable -fun PermissionTitleText(modifier: Modifier = Modifier, text: String, color: Color) { +fun PermissionTitleText(text: String, color: Color, modifier: Modifier = Modifier) { Text( modifier = modifier, color = color, @@ -224,7 +224,7 @@ fun PermissionTitleText(modifier: Modifier = Modifier, text: String, color: Colo } @Composable -fun PermissionBodyText(modifier: Modifier = Modifier, text: String, color: Color) { +fun PermissionBodyText(text: String, color: Color, modifier: Modifier = Modifier) { Text( modifier = modifier, color = color, diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index a94a31a3..72fb9ec8 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -21,7 +21,7 @@ plugins { android { namespace = "com.google.jetpackcamera.benchmark" - compileSdk = 34 + compileSdk = libs.versions.compileSdk.get().toInt() compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -35,7 +35,7 @@ android { defaultConfig { //Our app has a minSDK of 21, but in order for the benchmark tool to function, it must be 23 minSdk = 23 - targetSdk = 34 + targetSdk = libs.versions.targetSdk.get().toInt() // allows the benchmark to be run on an emulator testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = diff --git a/build.gradle.kts b/build.gradle.kts index 676fb11c..982992fd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,4 +38,14 @@ gradle.taskGraph.whenReady { task.dependsOn(tasks["installGitHooks"]) } } +} + +// Task to print all the module paths in the project e.g. :core:data +// Used by module graph generator script +tasks.register("printModulePaths") { + subprojects { + if (subprojects.size == 0) { + println(this.path) + } + } } \ No newline at end of file diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 81365c7b..1eba0736 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -23,11 +23,12 @@ plugins { android { namespace = "com.google.jetpackcamera.core.common" - compileSdk = 34 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 21 - targetSdk = 34 + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 2c56c183..60c9aa0e 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -24,11 +24,12 @@ plugins { android { namespace = "com.google.jetpackcamera.data.settings" - compileSdk = 34 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 21 - targetSdk = 34 + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -42,6 +43,7 @@ android { jvmToolchain(17) } + @Suppress("UnstableApiUsage") testOptions { managedDevices { localDevices { diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/AspectRatio.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/AspectRatio.kt index 67381540..8de6ea3c 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/AspectRatio.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/AspectRatio.kt @@ -28,14 +28,14 @@ enum class AspectRatio(val ratio: Rational) { /** returns the AspectRatio enum equivalent of a provided AspectRatioProto */ fun fromProto(aspectRatioProto: AspectRatioProto): AspectRatio { return when (aspectRatioProto) { - AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN -> AspectRatio.NINE_SIXTEEN - AspectRatioProto.ASPECT_RATIO_ONE_ONE -> AspectRatio.ONE_ONE + AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN -> NINE_SIXTEEN + AspectRatioProto.ASPECT_RATIO_ONE_ONE -> ONE_ONE // defaults to 3:4 aspect ratio AspectRatioProto.ASPECT_RATIO_THREE_FOUR, AspectRatioProto.ASPECT_RATIO_UNDEFINED, AspectRatioProto.UNRECOGNIZED - -> AspectRatio.THREE_FOUR + -> THREE_FOUR } } } diff --git a/domain/camera/build.gradle.kts b/domain/camera/build.gradle.kts index 4eccca33..c79cf4bc 100644 --- a/domain/camera/build.gradle.kts +++ b/domain/camera/build.gradle.kts @@ -23,11 +23,12 @@ plugins { android { namespace = "com.google.jetpackcamera.data.camera" - compileSdk = 34 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 21 - targetSdk = 34 + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -45,8 +46,8 @@ dependencies { // Testing testImplementation(libs.junit) testImplementation(libs.truth) - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") - testImplementation("org.mockito:mockito-core:5.2.0") + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockito.core) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -64,7 +65,7 @@ dependencies { kapt(libs.dagger.hilt.compiler) // Tracing - implementation("androidx.tracing:tracing-ktx:1.2.0") + implementation(libs.androidx.tracing) // Graphics libraries implementation(libs.androidx.graphics.core) diff --git a/domain/camera/src/main/AndroidManifest.xml b/domain/camera/src/main/AndroidManifest.xml index 1e7c244b..5c675bbe 100644 --- a/domain/camera/src/main/AndroidManifest.xml +++ b/domain/camera/src/main/AndroidManifest.xml @@ -14,6 +14,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + \ No newline at end of file diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/test/FakeCameraUseCase.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/test/FakeCameraUseCase.kt index 3d5c7c9e..c32a93dc 100644 --- a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/test/FakeCameraUseCase.kt +++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/test/FakeCameraUseCase.kt @@ -148,9 +148,6 @@ class FakeCameraUseCase( override fun getZoomScale(): StateFlow = _zoomScale.asStateFlow() private val _surfaceRequest = MutableStateFlow(null) - fun setSurfaceRequest(surfaceRequest: SurfaceRequest) { - _surfaceRequest.value = surfaceRequest - } override fun getSurfaceRequest(): StateFlow = _surfaceRequest.asStateFlow() override fun getScreenFlashEvents() = screenFlashEvents diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts index 9a62287a..bbce15e8 100644 --- a/feature/preview/build.gradle.kts +++ b/feature/preview/build.gradle.kts @@ -23,11 +23,12 @@ plugins { android { namespace = "com.google.jetpackcamera.feature.preview" - compileSdk = 34 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 21 - targetSdk = 34 + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -40,11 +41,14 @@ android { jvmToolchain(17) } buildFeatures { + buildConfig = true compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.0" + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() } + + @Suppress("UnstableApiUsage") testOptions { unitTests { isReturnDefaultValues = true 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 d0b6864d..ab28a6fd 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 @@ -23,6 +23,7 @@ import android.view.Display import androidx.camera.core.SurfaceRequest import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size @@ -70,6 +71,7 @@ fun PreviewScreen( onPreviewViewModel: (PreviewViewModel) -> Unit, onNavigateToSettings: () -> Unit, previewMode: PreviewMode, + modifier: Modifier = Modifier, onRequestWindowColorMode: (Int) -> Unit = {}, viewModel: PreviewViewModel = hiltViewModel() ) { @@ -92,8 +94,9 @@ fun PreviewScreen( } when (previewUiState.cameraState) { - CameraState.NOT_READY -> LoadingScreen() + CameraState.NOT_READY -> LoadingScreen(modifier) CameraState.READY -> ContentScreen( + modifier = modifier, previewUiState = previewUiState, previewMode = previewMode, screenFlashUiState = screenFlashUiState, @@ -126,6 +129,7 @@ private fun ContentScreen( previewMode: PreviewMode, screenFlashUiState: ScreenFlash.ScreenFlashUiState, surfaceRequest: SurfaceRequest?, + modifier: Modifier = Modifier, onNavigateToSettings: () -> Unit = {}, onClearUiScreenBrightness: (Float) -> Unit = {}, onSetLensFacing: (newLensFacing: LensFacing) -> Unit = {}, @@ -149,12 +153,10 @@ private fun ContentScreen( onRequestWindowColorMode: (Int) -> Unit = {}, onSnackBarResult: () -> Unit = {} ) { - val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - val blinkState = remember { BlinkState(coroutineScope = scope) } - Scaffold(snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - }) { + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } + ) { val lensFacing = remember(previewUiState) { previewUiState.currentCameraSettings.cameraLensFacing } @@ -164,78 +166,82 @@ private fun ContentScreen( onSetLensFacing(lensFacing.flip()) } } - // display camera feed. this stays behind everything else - PreviewDisplay( - onFlipCamera = onFlipCamera, - onTapToFocus = onTapToFocus, - onZoomChange = onChangeZoomScale, - onRequestWindowColorMode = onRequestWindowColorMode, - aspectRatio = previewUiState.currentCameraSettings.aspectRatio, - surfaceRequest = surfaceRequest, - blinkState = blinkState - ) - QuickSettingsScreenOverlay( - modifier = Modifier, - isOpen = previewUiState.quickSettingsIsOpen, - toggleIsOpen = onToggleQuickSettings, - currentCameraSettings = previewUiState.currentCameraSettings, - onLensFaceClick = onSetLensFacing, - onFlashModeClick = onChangeFlash, - onAspectRatioClick = onChangeAspectRatio, - onCaptureModeClick = onChangeCaptureMode, - onDynamicRangeClick = onChangeDynamicRange - // onTimerClick = {}/*TODO*/ - ) - // relative-grid style overlay on top of preview display - CameraControlsOverlay( - previewUiState = previewUiState, - onNavigateToSettings = onNavigateToSettings, - previewMode = previewMode, - onFlipCamera = onFlipCamera, - onChangeFlash = onChangeFlash, - onToggleQuickSettings = onToggleQuickSettings, - onCaptureImage = onCaptureImage, - onCaptureImageWithUri = onCaptureImageWithUri, - onStartVideoRecording = onStartVideoRecording, - onStopVideoRecording = onStopVideoRecording, - blinkState = blinkState - ) - // displays toast when there is a message to show - if (previewUiState.toastMessageToShow != null) { - TestableToast( - modifier = Modifier.testTag(previewUiState.toastMessageToShow.testTag), - toastMessage = previewUiState.toastMessageToShow, - onToastShown = onToastShown + val scope = rememberCoroutineScope() + val blinkState = remember { BlinkState(coroutineScope = scope) } + Box(modifier.fillMaxSize()) { + // display camera feed. this stays behind everything else + PreviewDisplay( + onFlipCamera = onFlipCamera, + onTapToFocus = onTapToFocus, + onZoomChange = onChangeZoomScale, + aspectRatio = previewUiState.currentCameraSettings.aspectRatio, + surfaceRequest = surfaceRequest, + onRequestWindowColorMode = onRequestWindowColorMode, + blinkState = blinkState ) - } - if (previewUiState.snackBarToShow != null) { - TestableSnackBar( - modifier = Modifier.testTag(previewUiState.snackBarToShow.testTag), - snackBarToShow = previewUiState.snackBarToShow, - scope = scope, - snackbarHostState = snackbarHostState, - onSnackBarResult = onSnackBarResult + QuickSettingsScreenOverlay( + modifier = Modifier, + isOpen = previewUiState.quickSettingsIsOpen, + toggleIsOpen = onToggleQuickSettings, + currentCameraSettings = previewUiState.currentCameraSettings, + onLensFaceClick = onSetLensFacing, + onFlashModeClick = onChangeFlash, + onAspectRatioClick = onChangeAspectRatio, + onCaptureModeClick = onChangeCaptureMode, + onDynamicRangeClick = onChangeDynamicRange // onTimerClick = {}/*TODO*/ ) - } + // relative-grid style overlay on top of preview display + CameraControlsOverlay( + previewUiState = previewUiState, + onNavigateToSettings = onNavigateToSettings, + previewMode = previewMode, + onFlipCamera = onFlipCamera, + onChangeFlash = onChangeFlash, + onToggleQuickSettings = onToggleQuickSettings, + onCaptureImage = onCaptureImage, + onCaptureImageWithUri = onCaptureImageWithUri, + onStartVideoRecording = onStartVideoRecording, + onStopVideoRecording = onStopVideoRecording, + blinkState = blinkState - // Screen flash overlay that stays on top of everything but invisible normally. This should - // not be enabled based on whether screen flash is enabled because a previous image capture - // may still be running after flash mode change and clear actions (e.g. brightness restore) - // may need to be handled later. Compose smart recomposition should be able to optimize this - // if the relevant states are no longer changing. - ScreenFlashScreen( - screenFlashUiState = screenFlashUiState, - onInitialBrightnessCalculated = onClearUiScreenBrightness - ) + ) + // displays toast when there is a message to show + if (previewUiState.toastMessageToShow != null) { + TestableToast( + modifier = Modifier.testTag(previewUiState.toastMessageToShow.testTag), + toastMessage = previewUiState.toastMessageToShow, + onToastShown = onToastShown + ) + } + + if (previewUiState.snackBarToShow != null) { + TestableSnackBar( + modifier = Modifier.testTag(previewUiState.snackBarToShow.testTag), + snackBarToShow = previewUiState.snackBarToShow, + scope = scope, + snackbarHostState = snackbarHostState, + onSnackBarResult = onSnackBarResult + ) + } + // Screen flash overlay that stays on top of everything but invisible normally. This should + // not be enabled based on whether screen flash is enabled because a previous image capture + // may still be running after flash mode change and clear actions (e.g. brightness restore) + // may need to be handled later. Compose smart recomposition should be able to optimize this + // if the relevant states are no longer changing. + ScreenFlashScreen( + screenFlashUiState = screenFlashUiState, + onInitialBrightnessCalculated = onClearUiScreenBrightness + ) + } } } @Composable -private fun LoadingScreen() { +private fun LoadingScreen(modifier: Modifier = Modifier) { Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(Color.Black), verticalArrangement = Arrangement.Center, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt index 442c350b..7f218ded 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt @@ -67,9 +67,11 @@ class ZoomLevelDisplayState(showInitially: Boolean = false) { @Composable fun CameraControlsOverlay( previewUiState: PreviewUiState, - zoomLevelDisplayState: ZoomLevelDisplayState = remember { ZoomLevelDisplayState() }, - onNavigateToSettings: () -> Unit, previewMode: PreviewMode, + blinkState: BlinkState, + modifier: Modifier = Modifier, + zoomLevelDisplayState: ZoomLevelDisplayState = remember { ZoomLevelDisplayState() }, + onNavigateToSettings: () -> Unit = {}, onFlipCamera: () -> Unit = {}, onChangeFlash: (FlashMode) -> Unit = {}, onToggleQuickSettings: () -> Unit = {}, @@ -81,8 +83,7 @@ fun CameraControlsOverlay( (PreviewViewModel.ImageCaptureEvent) -> Unit ) -> Unit = { _, _, _, _ -> }, onStartVideoRecording: () -> Unit = {}, - onStopVideoRecording: () -> Unit = {}, - blinkState: BlinkState + onStopVideoRecording: () -> Unit = {} ) { // Show the current zoom level for a short period of time, only when the level changes. var firstRun by remember { mutableStateOf(true) } @@ -95,7 +96,7 @@ fun CameraControlsOverlay( } CompositionLocalProvider(LocalContentColor provides Color.White) { - Box(Modifier.fillMaxSize()) { + Box(modifier.fillMaxSize()) { if (previewUiState.videoRecordingState == VideoRecordingState.INACTIVE) { ControlsTop( modifier = Modifier @@ -144,10 +145,10 @@ private fun ControlsTop( Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { // button to open default settings page SettingsNavButton( - Modifier + modifier = Modifier .padding(12.dp) .testTag(SETTINGS_BUTTON), - onNavigateToSettings + onNavigateToSettings = onNavigateToSettings ) if (!isQuickSettingsOpen) { QuickSettingsIndicators( @@ -208,7 +209,7 @@ private fun ControlsBottom( Row(Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceEvenly) { if (!isQuickSettingsOpen && videoRecordingState == VideoRecordingState.INACTIVE) { FlipCameraButton( - modifier = modifier.testTag(FLIP_CAMERA_BUTTON), + modifier = Modifier.testTag(FLIP_CAMERA_BUTTON), onClick = onFlipCamera, // enable only when phone has front and rear camera enabledCondition = currentCameraSettings.isBackCameraAvailable && diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraXViewfinder.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraXViewfinder.kt index 9488ab68..862f9af0 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraXViewfinder.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraXViewfinder.kt @@ -61,8 +61,8 @@ import kotlinx.coroutines.launch */ @Composable fun CameraXViewfinder( - modifier: Modifier = Modifier, surfaceRequest: SurfaceRequest, + modifier: Modifier = Modifier, implementationMode: ImplementationMode = ImplementationMode.PERFORMANCE, onRequestWindowColorMode: (Int) -> Unit = {} ) { 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 46b78f71..28c13b91 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 @@ -83,9 +83,9 @@ private const val TAG = "PreviewScreen" */ @Composable fun TestableToast( - modifier: Modifier = Modifier, toastMessage: ToastMessage, - onToastShown: () -> Unit + onToastShown: () -> Unit, + modifier: Modifier = Modifier ) { Box( // box seems to need to have some size to be detected by UiAutomator @@ -167,7 +167,8 @@ fun PreviewDisplay( onRequestWindowColorMode: (Int) -> Unit, aspectRatio: AspectRatio, surfaceRequest: SurfaceRequest?, - blinkState: BlinkState + blinkState: BlinkState, + modifier: Modifier = Modifier ) { val transformableState = rememberTransformableState( onTransformation = { zoomChange, _, _ -> @@ -236,7 +237,8 @@ class BlinkState( fun StabilizationIcon( supportedStabilizationMode: List, videoStabilization: Stabilization, - previewStabilization: Stabilization + previewStabilization: Stabilization, + modifier: Modifier = Modifier ) { if (supportedStabilizationMode.isNotEmpty() && (videoStabilization == Stabilization.ON || previewStabilization == Stabilization.ON) @@ -249,7 +251,8 @@ fun StabilizationIcon( } Icon( imageVector = Icons.Filled.VideoStable, - contentDescription = descriptionText + contentDescription = descriptionText, + modifier = modifier ) } } @@ -258,7 +261,7 @@ fun StabilizationIcon( * A temporary button that can be added to preview for quick testing purposes */ @Composable -fun TestingButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) { +fun TestingButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) { SuggestionChip( onClick = { onClick() }, modifier = modifier, @@ -270,9 +273,9 @@ fun TestingButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: Stri @Composable fun FlipCameraButton( - modifier: Modifier = Modifier, enabledCondition: Boolean, - onClick: () -> Unit + onClick: () -> Unit, + modifier: Modifier = Modifier ) { IconButton( modifier = modifier.size(40.dp), @@ -288,7 +291,7 @@ fun FlipCameraButton( } @Composable -fun SettingsNavButton(modifier: Modifier, onNavigateToSettings: () -> Unit) { +fun SettingsNavButton(onNavigateToSettings: () -> Unit, modifier: Modifier = Modifier) { IconButton( modifier = modifier, onClick = onNavigateToSettings @@ -302,7 +305,7 @@ fun SettingsNavButton(modifier: Modifier, onNavigateToSettings: () -> Unit) { } @Composable -fun ZoomScaleText(zoomScale: Float) { +fun ZoomScaleText(zoomScale: Float, modifier: Modifier = Modifier) { val contentAlpha = animateFloatAsState( targetValue = 10f, label = "zoomScaleAlphaAnimation", @@ -317,11 +320,11 @@ fun ZoomScaleText(zoomScale: Float) { @Composable fun CaptureButton( - modifier: Modifier = Modifier, onClick: () -> Unit, onLongPress: () -> Unit, onRelease: () -> Unit, - videoRecordingState: VideoRecordingState + videoRecordingState: VideoRecordingState, + modifier: Modifier = Modifier ) { var isPressedDown by remember { mutableStateOf(false) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ScreenFlashComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ScreenFlashComponents.kt index b8017af4..53b14c50 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ScreenFlashComponents.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/ScreenFlashComponents.kt @@ -56,7 +56,10 @@ fun ScreenFlashScreen( } @Composable -fun ScreenFlashOverlay(screenFlashUiState: ScreenFlash.ScreenFlashUiState) { +fun ScreenFlashOverlay( + screenFlashUiState: ScreenFlash.ScreenFlashUiState, + modifier: Modifier = Modifier +) { // Update overlay transparency gradually val alpha by animateFloatAsState( targetValue = if (screenFlashUiState.enabled) 1f else 0f, @@ -65,7 +68,7 @@ fun ScreenFlashOverlay(screenFlashUiState: ScreenFlash.ScreenFlashUiState) { finishedListener = { screenFlashUiState.onChangeComplete() } ) Box( - modifier = Modifier + modifier = modifier .run { if (screenFlashUiState.enabled) { this.testTag(SCREEN_FLASH_OVERLAY) diff --git a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/workaround/ComposableCaptureToImage.kt b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/workaround/ComposableCaptureToImage.kt index bad8ec8a..d89ec028 100644 --- a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/workaround/ComposableCaptureToImage.kt +++ b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/workaround/ComposableCaptureToImage.kt @@ -13,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:SuppressLint("UseSdkSuppress") + package com.google.jetpackcamera.feature.preview.workaround +import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.ContextWrapper @@ -52,14 +55,16 @@ import kotlin.math.roundToInt * * See [robolectric issue 8071](https://github.com/robolectric/robolectric/issues/8071) for details. */ + @OptIn(ExperimentalTestApi::class) @RequiresApi(Build.VERSION_CODES.O) fun SemanticsNodeInteraction.captureToImage(): ImageBitmap { val node = fetchSemanticsNode("Failed to capture a node to bitmap.") // Validate we are in popup - val popupParentMaybe = node.findClosestParentNode(includeSelf = true) { - it.config.contains(SemanticsProperties.IsPopup) - } + val popupParentMaybe = + node.findClosestParentNode(includeSelf = true) { + it.config.contains(SemanticsProperties.IsPopup) + } if (popupParentMaybe != null) { return processMultiWindowScreenshot(node) } @@ -67,9 +72,10 @@ fun SemanticsNodeInteraction.captureToImage(): ImageBitmap { val view = (node.root as ViewRootForTest).view // If we are in dialog use its window to capture the bitmap - val dialogParentNodeMaybe = node.findClosestParentNode(includeSelf = true) { - it.config.contains(SemanticsProperties.IsDialog) - } + val dialogParentNodeMaybe = + node.findClosestParentNode(includeSelf = true) { + it.config.contains(SemanticsProperties.IsDialog) + } var dialogWindow: Window? = null if (dialogParentNodeMaybe != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { @@ -86,12 +92,13 @@ fun SemanticsNodeInteraction.captureToImage(): ImageBitmap { val windowToUse = dialogWindow ?: view.context.getActivityWindow() val nodeBounds = node.boundsInRoot - val nodeBoundsRect = Rect( - nodeBounds.left.roundToInt(), - nodeBounds.top.roundToInt(), - nodeBounds.right.roundToInt(), - nodeBounds.bottom.roundToInt() - ) + val nodeBoundsRect = + Rect( + nodeBounds.left.roundToInt(), + nodeBounds.top.roundToInt(), + nodeBounds.right.roundToInt(), + nodeBounds.bottom.roundToInt() + ) val locationInWindow = intArrayOf(0, 0) view.getLocationInWindow(locationInWindow) @@ -129,13 +136,14 @@ private fun processMultiWindowScreenshot(node: SemanticsNode): ImageBitmap { val combinedBitmap = InstrumentationRegistry.getInstrumentation().uiAutomation.takeScreenshot() - val finalBitmap = Bitmap.createBitmap( - combinedBitmap, - (nodePositionInScreen.x + nodeBoundsInRoot.left).roundToInt(), - (nodePositionInScreen.y + nodeBoundsInRoot.top).roundToInt(), - nodeBoundsInRoot.width.roundToInt(), - nodeBoundsInRoot.height.roundToInt() - ) + val finalBitmap = + Bitmap.createBitmap( + combinedBitmap, + (nodePositionInScreen.x + nodeBoundsInRoot.left).roundToInt(), + (nodePositionInScreen.y + nodeBoundsInRoot.top).roundToInt(), + nodeBoundsInRoot.width.roundToInt(), + nodeBoundsInRoot.height.roundToInt() + ) return finalBitmap.asImageBitmap() } @@ -227,10 +235,11 @@ private object PixelCopyHelper { private fun Window.generateBitmapFromPixelCopy(boundsInWindow: Rect, destBitmap: Bitmap) { val latch = CountDownLatch(1) var copyResult = 0 - val onCopyFinished = PixelCopy.OnPixelCopyFinishedListener { result -> - copyResult = result - latch.countDown() - } + val onCopyFinished = + PixelCopy.OnPixelCopyFinishedListener { result -> + copyResult = result + latch.countDown() + } PixelCopyHelper.request( this, boundsInWindow, diff --git a/feature/quicksettings/build.gradle.kts b/feature/quicksettings/build.gradle.kts index ef6aa90c..b1f93967 100644 --- a/feature/quicksettings/build.gradle.kts +++ b/feature/quicksettings/build.gradle.kts @@ -22,11 +22,12 @@ plugins { android { namespace = "com.google.jetpackcamera.quicksettings" - compileSdk = 34 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 21 - targetSdk = 34 + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -39,10 +40,11 @@ android { jvmToolchain(17) } buildFeatures { + buildConfig = true compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.0" + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() } } 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 a167e5e9..a6176bd6 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 @@ -63,15 +63,15 @@ import com.google.jetpackcamera.settings.model.LensFacing */ @Composable fun QuickSettingsScreenOverlay( - modifier: Modifier = Modifier, currentCameraSettings: CameraAppSettings, - isOpen: Boolean = false, toggleIsOpen: () -> Unit, onLensFaceClick: (lensFace: LensFacing) -> Unit, onFlashModeClick: (flashMode: FlashMode) -> Unit, onAspectRatioClick: (aspectRation: AspectRatio) -> Unit, onCaptureModeClick: (captureMode: CaptureMode) -> Unit, - onDynamicRangeClick: (dynamicRange: DynamicRange) -> Unit + onDynamicRangeClick: (dynamicRange: DynamicRange) -> Unit, + modifier: Modifier = Modifier, + isOpen: Boolean = false ) { var shouldShowQuickSetting by remember { mutableStateOf(IsExpandedQuickSetting.NONE) 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 7ef35328..06e60ed2 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 @@ -62,7 +62,11 @@ import kotlin.math.min // completed components ready to go into preview screen @Composable -fun ExpandedQuickSetRatio(setRatio: (aspectRatio: AspectRatio) -> Unit, currentRatio: AspectRatio) { +fun ExpandedQuickSetRatio( + setRatio: (aspectRatio: AspectRatio) -> Unit, + currentRatio: AspectRatio, + modifier: Modifier = Modifier +) { val buttons: Array<@Composable () -> Unit> = arrayOf( { @@ -91,7 +95,7 @@ fun ExpandedQuickSetRatio(setRatio: (aspectRatio: AspectRatio) -> Unit, currentR ) } ) - ExpandedQuickSetting(quickSettingButtons = buttons) + ExpandedQuickSetting(modifier = modifier, quickSettingButtons = buttons) } @Composable @@ -125,10 +129,10 @@ fun QuickSetHdr( @Composable fun QuickSetRatio( - modifier: Modifier = Modifier, onClick: () -> Unit, ratio: AspectRatio, currentRatio: AspectRatio, + modifier: Modifier = Modifier, isHighlightEnabled: Boolean = false ) { val enum = @@ -148,9 +152,9 @@ fun QuickSetRatio( @Composable fun QuickSetFlash( - modifier: Modifier = Modifier, onClick: (FlashMode) -> Unit, - currentFlashMode: FlashMode + currentFlashMode: FlashMode, + modifier: Modifier = Modifier ) { val enum = when (currentFlashMode) { FlashMode.OFF -> CameraFlashMode.OFF @@ -178,9 +182,9 @@ fun QuickSetFlash( @Composable fun QuickFlipCamera( - modifier: Modifier = Modifier, setLensFacing: (LensFacing) -> Unit, - currentLensFacing: LensFacing + currentLensFacing: LensFacing, + modifier: Modifier = Modifier ) { val enum = when (currentLensFacing) { @@ -196,9 +200,9 @@ fun QuickFlipCamera( @Composable fun QuickSetCaptureMode( - modifier: Modifier = Modifier, setCaptureMode: (CaptureMode) -> Unit, - currentCaptureMode: CaptureMode + currentCaptureMode: CaptureMode, + modifier: Modifier = Modifier ) { val enum: CameraCaptureMode = when (currentCaptureMode) { @@ -221,10 +225,15 @@ fun QuickSetCaptureMode( * Button to toggle quick settings */ @Composable -fun ToggleQuickSettingsButton(toggleDropDown: () -> Unit, isOpen: Boolean) { +fun ToggleQuickSettingsButton( + toggleDropDown: () -> Unit, + isOpen: Boolean, + modifier: Modifier = Modifier +) { Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + modifier = modifier ) { // dropdown icon Icon( @@ -249,9 +258,9 @@ fun ToggleQuickSettingsButton(toggleDropDown: () -> Unit, isOpen: Boolean) { @Composable fun QuickSettingUiItem( - modifier: Modifier = Modifier, enum: QuickSettingsEnum, onClick: () -> Unit, + modifier: Modifier = Modifier, isHighLighted: Boolean = false, enabled: Boolean = true ) { @@ -271,11 +280,11 @@ fun QuickSettingUiItem( */ @Composable fun QuickSettingUiItem( - modifier: Modifier = Modifier, - painter: Painter, text: String, + painter: Painter, accessibilityText: String, onClick: () -> Unit, + modifier: Modifier = Modifier, isHighLighted: Boolean = false, enabled: Boolean = true ) { @@ -383,11 +392,11 @@ fun QuickSettingsGrid( * The top bar indicators for quick settings items. */ @Composable -fun Indicator(enum: QuickSettingsEnum, onClick: () -> Unit) { +fun Indicator(enum: QuickSettingsEnum, onClick: () -> Unit, modifier: Modifier = Modifier) { Icon( painter = enum.getPainter(), contentDescription = stringResource(id = enum.getDescriptionResId()), - modifier = Modifier + modifier = modifier .size(dimensionResource(id = R.dimen.quick_settings_indicator_size)) .clickable { onClick() } ) @@ -411,9 +420,10 @@ fun FlashModeIndicator(currentFlashMode: FlashMode, onClick: (flashMode: FlashMo @Composable fun QuickSettingsIndicators( currentFlashMode: FlashMode, - onFlashModeClick: (flashMode: FlashMode) -> Unit + onFlashModeClick: (flashMode: FlashMode) -> Unit, + modifier: Modifier = Modifier ) { - Row { + Row(modifier) { FlashModeIndicator(currentFlashMode, onFlashModeClick) } } diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index e0e722e7..2bb18420 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -23,11 +23,12 @@ plugins { android { namespace = "com.google.jetpackcamera.settings" - compileSdk = 34 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 21 - targetSdk = 34 + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -40,12 +41,14 @@ android { jvmToolchain(17) } buildFeatures { + buildConfig = true compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.0" + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() } + @Suppress("UnstableApiUsage") testOptions { managedDevices { localDevices { diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt index a26c32b1..63b39a90 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt @@ -184,7 +184,7 @@ data class VersionInfoHolder( @Preview(name = "Light Mode") @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun Preview_SettingsScreen() { +private fun Preview_SettingsScreen() { SettingsPreviewTheme { SettingsScreen( uiState = SettingsUiState(DEFAULT_CAMERA_APP_SETTINGS), diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt index 1ae0ce20..2db7571b 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt @@ -75,7 +75,7 @@ const val FPS_60 = 60 @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SettingsPageHeader(modifier: Modifier = Modifier, title: String, navBack: () -> Unit) { +fun SettingsPageHeader(title: String, navBack: () -> Unit, modifier: Modifier = Modifier) { TopAppBar( modifier = modifier, title = { @@ -96,7 +96,7 @@ fun SettingsPageHeader(modifier: Modifier = Modifier, title: String, navBack: () } @Composable -fun SectionHeader(modifier: Modifier = Modifier, title: String) { +fun SectionHeader(title: String, modifier: Modifier = Modifier) { Text( modifier = modifier .padding(start = 20.dp, top = 10.dp), @@ -108,9 +108,9 @@ fun SectionHeader(modifier: Modifier = Modifier, title: String) { @Composable fun DefaultCameraFacing( - modifier: Modifier = Modifier, cameraAppSettings: CameraAppSettings, - setDefaultLensFacing: (LensFacing) -> Unit + setDefaultLensFacing: (LensFacing) -> Unit, + modifier: Modifier = Modifier ) { SwitchSettingUI( modifier = modifier, @@ -128,9 +128,9 @@ fun DefaultCameraFacing( @Composable fun DarkModeSetting( - modifier: Modifier = Modifier, currentDarkMode: DarkMode, - setDarkMode: (DarkMode) -> Unit + setDarkMode: (DarkMode) -> Unit, + modifier: Modifier = Modifier ) { BasicPopupSetting( modifier = modifier, @@ -165,9 +165,9 @@ fun DarkModeSetting( @Composable fun FlashModeSetting( - modifier: Modifier = Modifier, currentFlashMode: FlashMode, - setFlashMode: (FlashMode) -> Unit + setFlashMode: (FlashMode) -> Unit, + modifier: Modifier = Modifier ) { BasicPopupSetting( modifier = modifier, @@ -201,8 +201,13 @@ fun FlashModeSetting( } @Composable -fun AspectRatioSetting(currentAspectRatio: AspectRatio, setAspectRatio: (AspectRatio) -> Unit) { +fun AspectRatioSetting( + currentAspectRatio: AspectRatio, + setAspectRatio: (AspectRatio) -> Unit, + modifier: Modifier = Modifier +) { BasicPopupSetting( + modifier = modifier, title = stringResource(id = R.string.aspect_ratio_title), leadingIcon = null, description = when (currentAspectRatio) { @@ -233,8 +238,13 @@ fun AspectRatioSetting(currentAspectRatio: AspectRatio, setAspectRatio: (AspectR } @Composable -fun CaptureModeSetting(currentCaptureMode: CaptureMode, setCaptureMode: (CaptureMode) -> Unit) { +fun CaptureModeSetting( + currentCaptureMode: CaptureMode, + setCaptureMode: (CaptureMode) -> Unit, + modifier: Modifier = Modifier +) { BasicPopupSetting( + modifier = modifier, title = stringResource(R.string.capture_mode_title), leadingIcon = null, description = when (currentCaptureMode) { @@ -265,10 +275,10 @@ fun CaptureModeSetting(currentCaptureMode: CaptureMode, setCaptureMode: (Capture @Composable fun TargetFpsSetting( - modifier: Modifier = Modifier, currentTargetFps: Int, supportedFps: List, - setTargetFps: (Int) -> Unit + setTargetFps: (Int) -> Unit, + modifier: Modifier = Modifier ) { BasicPopupSetting( modifier = modifier, @@ -352,7 +362,8 @@ fun StabilizationSetting( currentTargetFps: Int, supportedStabilizationMode: List, setVideoStabilization: (Stabilization) -> Unit, - setPreviewStabilization: (Stabilization) -> Unit + setPreviewStabilization: (Stabilization) -> Unit, + modifier: Modifier = Modifier ) { // if the preview stabilization was left ON and the target frame rate was set to 15, // this setting needs to be reset to OFF @@ -366,6 +377,7 @@ fun StabilizationSetting( // entire setting disabled when no available fps or target fps = 60 // stabilization is unsupported >30 fps BasicPopupSetting( + modifier = modifier, title = stringResource(R.string.video_stabilization_title), leadingIcon = null, enabled = ( @@ -450,8 +462,9 @@ fun StabilizationSetting( } @Composable -fun VersionInfo(versionName: String, buildType: String = "") { +fun VersionInfo(versionName: String, modifier: Modifier = Modifier, buildType: String = "") { SettingUI( + modifier = modifier, title = stringResource(id = R.string.version_info_title), leadingIcon = null ) { @@ -475,12 +488,12 @@ fun VersionInfo(versionName: String, buildType: String = "") { @Composable fun BasicPopupSetting( - modifier: Modifier = Modifier, title: String, description: String?, - enabled: Boolean = true, leadingIcon: @Composable (() -> Unit)?, - popupContents: @Composable () -> Unit + popupContents: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true ) { val popupStatus = remember { mutableStateOf(false) } SettingUI( @@ -514,13 +527,13 @@ fun BasicPopupSetting( */ @Composable fun SwitchSettingUI( - modifier: Modifier = Modifier, title: String, description: String?, leadingIcon: @Composable (() -> Unit)?, onSwitchChanged: (Boolean) -> Unit, settingValue: Boolean, - enabled: Boolean + enabled: Boolean, + modifier: Modifier = Modifier ) { SettingUI( modifier = modifier.toggleable( @@ -550,11 +563,11 @@ fun SwitchSettingUI( */ @Composable fun SettingUI( - modifier: Modifier = Modifier, title: String, + leadingIcon: @Composable (() -> Unit)?, + modifier: Modifier = Modifier, enabled: Boolean = true, description: String? = null, - leadingIcon: @Composable (() -> Unit)?, trailingContent: @Composable (() -> Unit)? ) { ListItem( @@ -588,11 +601,11 @@ fun SettingUI( */ @Composable fun SingleChoiceSelector( - modifier: Modifier = Modifier, text: String, - secondaryText: String? = null, selected: Boolean, onClick: () -> Unit, + modifier: Modifier = Modifier, + secondaryText: String? = null, enabled: Boolean = true ) { Row( @@ -625,7 +638,7 @@ fun SingleChoiceSelector( @Preview(name = "Light Mode") @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun Preview_VersionInfo() { +private fun Preview_VersionInfo() { SettingsPreviewTheme { VersionInfo(versionName = "0.1.0", buildType = "debug") } diff --git a/gradle.properties b/gradle.properties index 5c20ac75..9e52ff1d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,15 +15,19 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true +# Suppress warnings for experimental AGP properties +android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,\ + android.experimental.testOptions.managedDevices.maxConcurrentDevices,\ + android.experimental.testOptions.managedDevices.setupTimeoutMinutes,\ + android.testoptions.manageddevices.emulator.gpu # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false -# Properites to make gradle managed devices testing more stable (see https://issuetracker.google.com/287312019#comment41) +# Properties to make gradle managed devices testing more stable (see https://issuetracker.google.com/287312019#comment41) android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 android.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 # Ensure we can run managed devices on servers that don't support hardware rendering diff --git a/gradle/init.gradle.kts b/gradle/init.gradle.kts index 35c0da59..8b5cab4e 100644 --- a/gradle/init.gradle.kts +++ b/gradle/init.gradle.kts @@ -14,10 +14,10 @@ * limitations under the License. */ -val ktlintVersion = "1.1.1" +val ktlintVersion = "1.2.1" initscript { - val spotlessVersion = "6.22.0" + val spotlessVersion = "6.25.0" repositories { mavenCentral() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4577d25b..a650cf07 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,66 +1,74 @@ [versions] -accompanistPermissions = "0.26.5-rc" -androidJunit = "1.1.5" +# Used directly in build.gradle.kts files +compileSdk = "34" +minSdk = "21" +targetSdk = "34" +composeCompiler = "1.5.10" + +# Used below in dependency definitions +# Compose and Accompanist versions are linked +# See https://github.com/google/accompanist?tab=readme-ov-file#compose-versions +composeBom = "2024.04.00" +accompanist = "0.34.0" +# kotlinPlugin and composeCompiler are linked +# See https://developer.android.com/jetpack/androidx/releases/compose-kotlin +kotlinPlugin = "1.9.22" androidGradlePlugin = "8.4.0-rc01" +protobufPlugin = "0.9.4" + androidxActivityCompose = "1.8.2" androidxAppCompat = "1.6.1" -androidxCore = "1.12.0" +androidxBenchmark = "1.2.3" +androidxCamera = "1.4.0-SNAPSHOT" +androidxCameraViewfinder = "1.0.0-SNAPSHOT" +androidxConcurrentFutures = "1.1.0" androidxCoreKtx = "1.12.0" +androidxDatastore = "1.0.0" androidxGraphicsCore = "1.0.0-beta01" +androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.7.0" +androidxNavigationCompose = "2.7.7" +androidxProfileinstaller = "1.3.1" +androidxTestEspresso = "3.5.1" +androidxTestJunit = "1.1.5" +androidxTestMonitor = "1.6.1" +androidxTestRules = "1.5.0" +androidxTestUiautomator = "2.3.0" androidxTracing = "1.2.0" -atomicfu = "0.23.2" -benchmarkMacroJunit4 = "1.2.3" -camerax = "1.4.0-SNAPSHOT" -camerax-viewfinder-compose = "1.0.0-SNAPSHOT" -composeBom = "2024.02.00" -coreKtx = "1.5.0" -coroutinesCore = "1.7.3" -coroutinesTest = "1.7.3" -datastore = "1.0.0" -espressoCore = "3.5.1" -futures = "1.1.0" -hilt = "2.48" -hiltNavigationCompose = "1.1.0" +kotlinxAtomicfu = "0.23.2" +kotlinxCoroutines = "1.8.0" +hilt = "2.51" junit = "4.13.2" -kotlinPlugin = "1.8.0" material = "1.11.0" -mockitoCore = "5.2.0" -navigationCompose = "2.7.7" -profileinstaller = "1.3.1" -protobuf = "3.21.12" -protobuf-plugin = "0.9.1" +mockitoCore = "5.6.0" +protobuf = "3.25.2" robolectric = "4.11.1" -testMonitor = "1.6.1" -truth = "1.0.1" -uiautomator = "2.2.0" +truth = "1.4.2" [libraries] -accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } android-material = { module = "com.google.android.material:material", version.ref = "material" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityCompose" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppCompat" } -androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } -androidx-core = { module = "androidx.core:core", version.ref = "androidxCore" } +androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidxBenchmark" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCoreKtx" } -androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } -androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } +androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidxDatastore" } +androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" } androidx-graphics-core = { module = "androidx.graphics:graphics-core", version.ref = "androidxGraphicsCore" } -androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidJunit" } +androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestJunit" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } -androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" } -androidx-rules = { module = "androidx.test:rules", version.ref = "coreKtx" } -androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "testMonitor" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigationCompose" } +androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "androidxProfileinstaller" } +androidx-rules = { module = "androidx.test:rules", version.ref = "androidxTestRules" } +androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidxTestMonitor" } androidx-tracing = { module = "androidx.tracing:tracing-ktx", version.ref = "androidxTracing" } -androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" } -camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" } -camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" } -camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax" } -camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } -camera-video = { module = "androidx.camera:camera-video", version.ref = "camerax" } -camera-viewfinder-compose = { module = "androidx.camera:camera-viewfinder-compose", version.ref = "camerax-viewfinder-compose" } +androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidxTestUiautomator" } +camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidxCamera" } +camera-core = { module = "androidx.camera:camera-core", version.ref = "androidxCamera" } +camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidxCamera" } +camera-video = { module = "androidx.camera:camera-video", version.ref = "androidxCamera" } +camera-viewfinder-compose = { module = "androidx.camera:camera-viewfinder-compose", version.ref = "androidxCameraViewfinder" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-junit = { module = "androidx.compose.ui:ui-test-junit4" } compose-material3 = { module = "androidx.compose.material3:material3" } @@ -70,12 +78,12 @@ compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } -futures-ktx = { module = "androidx.concurrent:concurrent-futures-ktx", version.ref = "futures" } -hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +futures-ktx = { module = "androidx.concurrent:concurrent-futures-ktx", version.ref = "androidxConcurrentFutures" } +hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } junit = { module = "junit:junit", version.ref = "junit" } -kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesCore" } -kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesTest" } +kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinxAtomicfu" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" } protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } @@ -87,6 +95,6 @@ android-application = { id = "com.android.application", version.ref = "androidGr android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -google-protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" } +google-protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinPlugin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlinPlugin" } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 40685426..1fe59d05 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,7 @@ pluginManagement { gradlePluginPortal() } } +@Suppress("UnstableApiUsage") dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { From 17c50b55f1dcd19bc2752a7d765a815895cc2558 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 17 Apr 2024 22:18:27 +0000 Subject: [PATCH 2/2] Add per-lens and system constraints for HDR video (#168) * Remove constraints from CameraAppSettings These don't need to be persisted, and only need to be updated once on initialization. * Add constraints classes * Add constraints for dynamic range Whenever the camera is switched, the constraints of supported dynamic range need to be updated. * Add ConstraintsRepository to data layer Make ConstraintsRepository an injectible singleton to ensure we're using the same instance across the camera use case and the viewmodels * Add constraints to SettingsScreen * Add constraints to preview screen / quick settings * Add constraints to all tests and use new UiState classes Fix UiTestUtil dependency on new PreviewUiState sealed class Fix up FakeSettingsRepository to handle constraints Adapt PreviewViewModelTest to fit with constraint changes Delete tests that are no longer valid since Settings no longer contain constraints Fixup LocalSettingsRepositoryInstrumentedTest to use new naming for default settings Adapt CameraAppSettingsViewModelTest to use new SystemConstraints and SettingsViewModel changes * Apply spotless * Reorder and delete old proto fields for app settings We should only do this in pre-release versions. After a release, the field numbers need to be locked in a deprecated or added to a list of `reserved`. --- .../com/google/jetpackcamera/UiTestUtil.kt | 12 +- .../jetpackcamera/MainActivityViewModel.kt | 2 +- ...LocalSettingsRepositoryInstrumentedTest.kt | 51 +--- .../settings/ConstraintsRepository.kt | 41 +++ .../settings/JcaSettingsSerializer.kt | 6 - .../settings/LocalSettingsRepository.kt | 107 +------- .../jetpackcamera/settings/SettingsModule.kt | 18 ++ .../settings/SettingsRepository.kt | 19 +- .../settings/model/CameraAppSettings.kt | 11 +- .../settings/model/Constraints.kt | 47 ++++ .../test/FakeJcaSettingsSerializer.kt | 6 +- .../settings/test/FakeSettingsRepository.kt | 61 +---- .../jetpackcamera/settings/jca_settings.proto | 28 +- .../domain/camera/CameraXCameraUseCase.kt | 172 +++++++----- .../feature/preview/PreviewScreen.kt | 23 +- .../feature/preview/PreviewUiState.kt | 46 ++-- .../feature/preview/PreviewViewModel.kt | 252 ++++++++---------- .../preview/ui/CameraControlsOverlay.kt | 31 ++- .../preview/ui/PreviewScreenComponents.kt | 6 +- .../feature/preview/PreviewViewModelTest.kt | 34 ++- .../quicksettings/QuickSettingsScreen.kt | 26 +- .../CameraAppSettingsViewModelTest.kt | 62 +++-- .../jetpackcamera/settings/SettingsScreen.kt | 51 ++-- .../jetpackcamera/settings/SettingsUiState.kt | 12 +- .../settings/SettingsViewModel.kt | 93 ++----- .../settings/ui/SettingsComponents.kt | 13 +- 26 files changed, 596 insertions(+), 634 deletions(-) create mode 100644 data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsRepository.kt create mode 100644 data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt diff --git a/app/src/androidTest/java/com/google/jetpackcamera/UiTestUtil.kt b/app/src/androidTest/java/com/google/jetpackcamera/UiTestUtil.kt index 543ab492..cae42410 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/UiTestUtil.kt @@ -22,10 +22,12 @@ import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.test.core.app.ActivityScenario +import com.google.jetpackcamera.feature.preview.PreviewUiState import com.google.jetpackcamera.feature.quicksettings.ui.QUICK_SETTINGS_FLIP_CAMERA_BUTTON import com.google.jetpackcamera.quicksettings.R import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.LensFacing +import java.lang.IllegalStateException import java.util.concurrent.atomic.AtomicReference const val APP_START_TIMEOUT_MILLIS = 10_000L @@ -42,7 +44,15 @@ object UiTestUtil { ): CameraAppSettings { return getActivity( activityScenario - ).previewViewModel!!.previewUiState.value.currentCameraSettings + ).previewViewModel!!.previewUiState.value.let { + when (it) { + is PreviewUiState.Ready -> it.currentCameraSettings + else -> throw IllegalStateException( + "Can only retrieve camera app settings from PreviewUiState.Ready," + + " but state was ${it::class}" + ) + } + } } } diff --git a/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt b/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt index 5d89e1ad..1b6cca28 100644 --- a/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt +++ b/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.stateIn class MainActivityViewModel @Inject constructor( settingsRepository: SettingsRepository ) : ViewModel() { - val uiState: StateFlow = settingsRepository.cameraAppSettings.map { + val uiState: StateFlow = settingsRepository.defaultCameraAppSettings.map { Success(it) }.stateIn( scope = viewModelScope, diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt index c02e3632..44846acb 100644 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt +++ b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt @@ -89,7 +89,7 @@ class LocalSettingsRepositoryInstrumentedTest { // if you've created a new setting value and this test is failing, be sure to check that // JcaSettingsSerializer.kt defaultValue has been properly modified :) - val cameraAppSettings: CameraAppSettings = repository.getCameraAppSettings() + val cameraAppSettings: CameraAppSettings = repository.getCurrentDefaultCameraAppSettings() advanceUntilIdle() assertThat(cameraAppSettings).isEqualTo(DEFAULT_CAMERA_APP_SETTINGS) @@ -97,9 +97,9 @@ class LocalSettingsRepositoryInstrumentedTest { @Test fun can_update_dark_mode() = runTest { - val initialDarkModeStatus = repository.getCameraAppSettings().darkMode + val initialDarkModeStatus = repository.getCurrentDefaultCameraAppSettings().darkMode repository.updateDarkModeStatus(DarkMode.LIGHT) - val newDarkModeStatus = repository.getCameraAppSettings().darkMode + val newDarkModeStatus = repository.getCurrentDefaultCameraAppSettings().darkMode advanceUntilIdle() assertThat(initialDarkModeStatus).isNotEqualTo(newDarkModeStatus) @@ -110,10 +110,11 @@ class LocalSettingsRepositoryInstrumentedTest { @Test fun can_update_default_to_front_camera() = runTest { // default lens facing starts as BACK - val initialDefaultLensFacing = repository.getCameraAppSettings().cameraLensFacing + val initialDefaultLensFacing = + repository.getCurrentDefaultCameraAppSettings().cameraLensFacing repository.updateDefaultLensFacing(LensFacing.FRONT) // default lens facing is now FRONT - val newDefaultLensFacing = repository.getCameraAppSettings().cameraLensFacing + val newDefaultLensFacing = repository.getCurrentDefaultCameraAppSettings().cameraLensFacing advanceUntilIdle() assertThat(initialDefaultLensFacing).isEqualTo(LensFacing.BACK) @@ -123,59 +124,27 @@ class LocalSettingsRepositoryInstrumentedTest { @Test fun can_update_flash_mode() = runTest { // default flash mode starts as OFF - val initialFlashModeStatus = repository.getCameraAppSettings().flashMode + val initialFlashModeStatus = repository.getCurrentDefaultCameraAppSettings().flashMode repository.updateFlashModeStatus(FlashMode.ON) // default flash mode is now ON - val newFlashModeStatus = repository.getCameraAppSettings().flashMode + val newFlashModeStatus = repository.getCurrentDefaultCameraAppSettings().flashMode advanceUntilIdle() assertThat(initialFlashModeStatus).isEqualTo(FlashMode.OFF) assertThat(newFlashModeStatus).isEqualTo(FlashMode.ON) } - @Test - fun can_update_available_camera_lens() = runTest { - // available cameras start true - val initialFrontCamera = repository.getCameraAppSettings().isFrontCameraAvailable - val initialBackCamera = repository.getCameraAppSettings().isBackCameraAvailable - - repository.updateAvailableCameraLens(frontLensAvailable = false, backLensAvailable = false) - // available cameras now false - advanceUntilIdle() - val newFrontCamera = repository.getCameraAppSettings().isFrontCameraAvailable - val newBackCamera = repository.getCameraAppSettings().isBackCameraAvailable - - assertThat(initialFrontCamera && initialBackCamera).isTrue() - assertThat(newFrontCamera || newBackCamera).isFalse() - } - @Test fun can_update_dynamic_range() = runTest { - val initialDynamicRange = repository.getCameraAppSettings().dynamicRange + val initialDynamicRange = repository.getCurrentDefaultCameraAppSettings().dynamicRange repository.updateDynamicRange(dynamicRange = DynamicRange.HLG10) advanceUntilIdle() - val newDynamicRange = repository.getCameraAppSettings().dynamicRange + val newDynamicRange = repository.getCurrentDefaultCameraAppSettings().dynamicRange assertThat(initialDynamicRange).isEqualTo(DynamicRange.SDR) assertThat(newDynamicRange).isEqualTo(DynamicRange.HLG10) } - - @Test - fun can_update_supported_dynamic_ranges() = runTest { - val initialSupportedDynamicRanges = repository.getCameraAppSettings().supportedDynamicRanges - - repository.updateSupportedDynamicRanges( - supportedDynamicRanges = listOf(DynamicRange.SDR, DynamicRange.HLG10) - ) - - advanceUntilIdle() - - val newSupportedDynamicRanges = repository.getCameraAppSettings().supportedDynamicRanges - - assertThat(initialSupportedDynamicRanges).containsExactly(DynamicRange.SDR) - assertThat(newSupportedDynamicRanges).containsExactly(DynamicRange.SDR, DynamicRange.HLG10) - } } diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsRepository.kt new file mode 100644 index 00000000..0d150c60 --- /dev/null +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.settings + +import com.google.jetpackcamera.settings.model.SystemConstraints +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface ConstraintsRepository { + val systemConstraints: StateFlow +} + +interface SettableConstraintsRepository : ConstraintsRepository { + fun updateSystemConstraints(systemConstraints: SystemConstraints) +} + +class SettableConstraintsRepositoryImpl @Inject constructor() : SettableConstraintsRepository { + + private val _systemConstraints = MutableStateFlow(null) + override val systemConstraints: StateFlow + get() = _systemConstraints.asStateFlow() + + override fun updateSystemConstraints(systemConstraints: SystemConstraints) { + _systemConstraints.value = systemConstraints + } +} diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt index b48aaa69..e0196eba 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt @@ -26,18 +26,12 @@ object JcaSettingsSerializer : Serializer { override val defaultValue: JcaSettings = JcaSettings.newBuilder() .setDarkModeStatus(DarkMode.DARK_MODE_SYSTEM) .setDefaultLensFacing(LensFacing.LENS_FACING_BACK) - .setBackCameraAvailable(true) - .setFrontCameraAvailable(true) .setFlashModeStatus(FlashMode.FLASH_MODE_OFF) .setAspectRatioStatus(AspectRatio.ASPECT_RATIO_NINE_SIXTEEN) .setCaptureModeStatus(CaptureMode.CAPTURE_MODE_MULTI_STREAM) .setStabilizePreview(PreviewStabilization.PREVIEW_STABILIZATION_UNDEFINED) .setStabilizeVideo(VideoStabilization.VIDEO_STABILIZATION_UNDEFINED) - .setStabilizePreviewSupported(false) - .setStabilizeVideoSupported(false) .setDynamicRangeStatus(DynamicRange.DYNAMIC_RANGE_UNSPECIFIED) - .addSupportedDynamicRanges(DynamicRange.DYNAMIC_RANGE_SDR) - .addAllSupportedFrameRates(emptySet()) .build() override suspend fun readFrom(input: InputStream): JcaSettings { diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt index 9a1de698..80d6e008 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt @@ -32,7 +32,6 @@ import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.LensFacing.Companion.toProto import com.google.jetpackcamera.settings.model.Stabilization -import com.google.jetpackcamera.settings.model.SupportedStabilizationMode import javax.inject.Inject import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -49,7 +48,7 @@ class LocalSettingsRepository @Inject constructor( private val jcaSettings: DataStore ) : SettingsRepository { - override val cameraAppSettings = jcaSettings.data + override val defaultCameraAppSettings = jcaSettings.data .map { CameraAppSettings( cameraLensFacing = LensFacing.fromProto(it.defaultLensFacing), @@ -65,30 +64,21 @@ class LocalSettingsRepository @Inject constructor( FlashModeProto.FLASH_MODE_OFF -> FlashMode.OFF else -> FlashMode.OFF }, - isFrontCameraAvailable = it.frontCameraAvailable, - isBackCameraAvailable = it.backCameraAvailable, aspectRatio = AspectRatio.fromProto(it.aspectRatioStatus), previewStabilization = Stabilization.fromProto(it.stabilizePreview), videoCaptureStabilization = Stabilization.fromProto(it.stabilizeVideo), - supportedStabilizationModes = getSupportedStabilization( - previewSupport = it.stabilizePreviewSupported, - videoSupport = it.stabilizeVideoSupported - ), targetFrameRate = it.targetFrameRate, captureMode = when (it.captureModeStatus) { CaptureModeProto.CAPTURE_MODE_SINGLE_STREAM -> CaptureMode.SINGLE_STREAM CaptureModeProto.CAPTURE_MODE_MULTI_STREAM -> CaptureMode.MULTI_STREAM else -> CaptureMode.MULTI_STREAM }, - dynamicRange = DynamicRange.fromProto(it.dynamicRangeStatus), - supportedDynamicRanges = it.supportedDynamicRangesList.map { dynRngProto -> - DynamicRange.fromProto(dynRngProto) - }, - supportedFixedFrameRates = it.supportedFrameRatesList + dynamicRange = DynamicRange.fromProto(it.dynamicRangeStatus) ) } - override suspend fun getCameraAppSettings(): CameraAppSettings = cameraAppSettings.first() + override suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings = + defaultCameraAppSettings.first() override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { jcaSettings.updateData { currentSettings -> @@ -124,20 +114,6 @@ class LocalSettingsRepository @Inject constructor( } } - override suspend fun updateAvailableCameraLens( - frontLensAvailable: Boolean, - backLensAvailable: Boolean - ) { - // if a front or back lens is not present, the option to change - // the direction of the camera should be disabled - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setFrontCameraAvailable(frontLensAvailable) - .setBackCameraAvailable(backLensAvailable) - .build() - } - } - override suspend fun updateTargetFrameRate(targetFrameRate: Int) { jcaSettings.updateData { currentSettings -> currentSettings.toBuilder() @@ -146,38 +122,6 @@ class LocalSettingsRepository @Inject constructor( } } - override suspend fun updateSupportedFixedFrameRate( - supportedFrameRates: Set, - currentTargetFrameRate: Int - ) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .clearSupportedFrameRates() - .addAllSupportedFrameRates(supportedFrameRates) - .build() - } - when (currentTargetFrameRate) { - TARGET_FPS_NONE -> {} - TARGET_FPS_15 -> { - if (!supportedFrameRates.contains(TARGET_FPS_15)) { - updateTargetFrameRate(TARGET_FPS_NONE) - } - } - - TARGET_FPS_30 -> { - if (!supportedFrameRates.contains(30)) { - updateTargetFrameRate(TARGET_FPS_NONE) - } - } - - TARGET_FPS_60 -> { - if (!supportedFrameRates.contains(60)) { - updateTargetFrameRate(TARGET_FPS_NONE) - } - } - } - } - override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { val newStatus = when (aspectRatio) { AspectRatio.NINE_SIXTEEN -> AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN @@ -229,36 +173,6 @@ class LocalSettingsRepository @Inject constructor( } } - override suspend fun updateVideoStabilizationSupported(isSupported: Boolean) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setStabilizeVideoSupported(isSupported) - .build() - } - } - - override suspend fun updatePreviewStabilizationSupported(isSupported: Boolean) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setStabilizePreviewSupported(isSupported) - .build() - } - } - - private fun getSupportedStabilization( - previewSupport: Boolean, - videoSupport: Boolean - ): List { - return buildList { - if (previewSupport) { - add(SupportedStabilizationMode.ON) - } - if (videoSupport) { - add(SupportedStabilizationMode.HIGH_QUALITY) - } - } - } - override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { jcaSettings.updateData { currentSettings -> currentSettings.toBuilder() @@ -266,17 +180,4 @@ class LocalSettingsRepository @Inject constructor( .build() } } - - override suspend fun updateSupportedDynamicRanges(supportedDynamicRanges: List) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .clearSupportedDynamicRanges() - .addAllSupportedDynamicRanges( - supportedDynamicRanges.map { - it.toProto() - } - ) - .build() - } - } } diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt index 3c9fb71e..f523b558 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt @@ -19,6 +19,7 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton /** * Dagger [Module] for settings data layer. @@ -31,4 +32,21 @@ interface SettingsModule { fun bindsSettingsRepository( localSettingsRepository: LocalSettingsRepository ): SettingsRepository + + @Binds + @Singleton + fun bindsSettableConstraintsRepository( + settableConstraintsRepository: SettableConstraintsRepositoryImpl + ): SettableConstraintsRepository + + /** + * ConstraintsRepository without setter. + * + * This is the same instance as the singleton SettableConstraintsRepository, but does not + * have the ability to update the constraints. + */ + @Binds + fun bindsConstraintsRepository( + constraintsRepository: SettableConstraintsRepository + ): ConstraintsRepository } diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepository.kt index 8f336086..6e0fb3dc 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepository.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepository.kt @@ -30,7 +30,9 @@ import kotlinx.coroutines.flow.Flow */ interface SettingsRepository { - val cameraAppSettings: Flow + val defaultCameraAppSettings: Flow + + suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings suspend fun updateDefaultLensFacing(lensFacing: LensFacing) @@ -38,9 +40,6 @@ interface SettingsRepository { suspend fun updateFlashModeStatus(flashMode: FlashMode) - // set device values from cameraUseCase - suspend fun updateAvailableCameraLens(frontLensAvailable: Boolean, backLensAvailable: Boolean) - suspend fun updateAspectRatio(aspectRatio: AspectRatio) suspend fun updateCaptureMode(captureMode: CaptureMode) @@ -49,19 +48,7 @@ interface SettingsRepository { suspend fun updateVideoStabilization(stabilization: Stabilization) - suspend fun updateVideoStabilizationSupported(isSupported: Boolean) - - suspend fun updatePreviewStabilizationSupported(isSupported: Boolean) suspend fun updateDynamicRange(dynamicRange: DynamicRange) - suspend fun updateSupportedDynamicRanges(supportedDynamicRanges: List) - suspend fun updateTargetFrameRate(targetFrameRate: Int) - - suspend fun updateSupportedFixedFrameRate( - supportedFrameRates: Set, - currentTargetFrameRate: Int - ) - - suspend fun getCameraAppSettings(): CameraAppSettings } diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt index 7cfdfec8..0bb37c2e 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt @@ -21,21 +21,20 @@ const val TARGET_FPS_AUTO = 0 */ data class CameraAppSettings( val cameraLensFacing: LensFacing = LensFacing.BACK, - val isFrontCameraAvailable: Boolean = true, - val isBackCameraAvailable: Boolean = true, val darkMode: DarkMode = DarkMode.SYSTEM, val flashMode: FlashMode = FlashMode.OFF, val captureMode: CaptureMode = CaptureMode.MULTI_STREAM, val aspectRatio: AspectRatio = AspectRatio.NINE_SIXTEEN, val previewStabilization: Stabilization = Stabilization.UNDEFINED, val videoCaptureStabilization: Stabilization = Stabilization.UNDEFINED, - val supportedStabilizationModes: List = emptyList(), val dynamicRange: DynamicRange = DynamicRange.SDR, - val supportedDynamicRanges: List = listOf(DynamicRange.SDR), val defaultHdrDynamicRange: DynamicRange = DynamicRange.HLG10, val zoomScale: Float = 1f, - val targetFrameRate: Int = TARGET_FPS_AUTO, - val supportedFixedFrameRates: List = emptyList() + val targetFrameRate: Int = TARGET_FPS_AUTO ) +fun SystemConstraints.forCurrentLens(cameraAppSettings: CameraAppSettings): CameraConstraints? { + return perLensConstraints[cameraAppSettings.cameraLensFacing] +} + val DEFAULT_CAMERA_APP_SETTINGS = CameraAppSettings() diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt new file mode 100644 index 00000000..d4f73640 --- /dev/null +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt @@ -0,0 +1,47 @@ +/* + * 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.settings.model + +data class SystemConstraints( + val availableLenses: List, + val perLensConstraints: Map +) + +data class CameraConstraints( + val supportedStabilizationModes: Set, + val supportedFixedFrameRates: Set, + val supportedDynamicRanges: Set +) + +/** + * Useful set of constraints for testing + */ +val TYPICAL_SYSTEM_CONSTRAINTS = + SystemConstraints( + availableLenses = listOf(LensFacing.FRONT, LensFacing.BACK), + perLensConstraints = buildMap { + for (lensFacing in listOf(LensFacing.FRONT, LensFacing.BACK)) { + put( + lensFacing, + CameraConstraints( + supportedFixedFrameRates = setOf(15, 30), + supportedStabilizationModes = emptySet(), + supportedDynamicRanges = setOf(DynamicRange.SDR) + ) + ) + } + } + ) diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt index 368812cf..d207a999 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt @@ -20,6 +20,7 @@ import androidx.datastore.core.Serializer import com.google.jetpackcamera.settings.AspectRatio import com.google.jetpackcamera.settings.CaptureMode import com.google.jetpackcamera.settings.DarkMode +import com.google.jetpackcamera.settings.DynamicRange import com.google.jetpackcamera.settings.FlashMode import com.google.jetpackcamera.settings.JcaSettings import com.google.jetpackcamera.settings.LensFacing @@ -37,15 +38,12 @@ class FakeJcaSettingsSerializer( override val defaultValue: JcaSettings = JcaSettings.newBuilder() .setDarkModeStatus(DarkMode.DARK_MODE_SYSTEM) .setDefaultLensFacing(LensFacing.LENS_FACING_BACK) - .setBackCameraAvailable(true) - .setFrontCameraAvailable(true) .setFlashModeStatus(FlashMode.FLASH_MODE_OFF) .setAspectRatioStatus(AspectRatio.ASPECT_RATIO_NINE_SIXTEEN) .setCaptureModeStatus(CaptureMode.CAPTURE_MODE_MULTI_STREAM) .setStabilizePreview(PreviewStabilization.PREVIEW_STABILIZATION_UNDEFINED) .setStabilizeVideo(VideoStabilization.VIDEO_STABILIZATION_UNDEFINED) - .setStabilizeVideoSupported(false) - .setStabilizePreviewSupported(false) + .setDynamicRangeStatus(DynamicRange.DYNAMIC_RANGE_SDR) .build() override suspend fun readFrom(input: InputStream): JcaSettings { diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt index 7bd51ed6..c7ed7b77 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt @@ -25,16 +25,17 @@ import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.Stabilization -import com.google.jetpackcamera.settings.model.SupportedStabilizationMode import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow object FakeSettingsRepository : SettingsRepository { private var currentCameraSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS - private var isPreviewStabilizationSupported: Boolean = false - private var isVideoStabilizationSupported: Boolean = false - override val cameraAppSettings: Flow = flow { emit(currentCameraSettings) } + override val defaultCameraAppSettings: Flow = + flow { emit(currentCameraSettings) } + + override suspend fun getCurrentDefaultCameraAppSettings() = defaultCameraAppSettings.first() override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { currentCameraSettings = currentCameraSettings.copy(cameraLensFacing = lensFacing) @@ -48,20 +49,6 @@ object FakeSettingsRepository : SettingsRepository { currentCameraSettings = currentCameraSettings.copy(flashMode = flashMode) } - override suspend fun getCameraAppSettings(): CameraAppSettings { - return currentCameraSettings - } - - override suspend fun updateAvailableCameraLens( - frontLensAvailable: Boolean, - backLensAvailable: Boolean - ) { - currentCameraSettings = currentCameraSettings.copy( - isFrontCameraAvailable = frontLensAvailable, - isBackCameraAvailable = backLensAvailable - ) - } - override suspend fun updateCaptureMode(captureMode: CaptureMode) { currentCameraSettings = currentCameraSettings.copy(captureMode = captureMode) @@ -77,41 +64,11 @@ object FakeSettingsRepository : SettingsRepository { currentCameraSettings.copy(videoCaptureStabilization = stabilization) } - override suspend fun updateVideoStabilizationSupported(isSupported: Boolean) { - isVideoStabilizationSupported = isSupported - setSupportedStabilizationMode() - } - - override suspend fun updatePreviewStabilizationSupported(isSupported: Boolean) { - isPreviewStabilizationSupported = isSupported - setSupportedStabilizationMode() - } - - private fun setSupportedStabilizationMode() { - val stabilizationModes = - buildList { - if (isPreviewStabilizationSupported) { - add(SupportedStabilizationMode.ON) - } - if (isVideoStabilizationSupported) { - add(SupportedStabilizationMode.HIGH_QUALITY) - } - } - - currentCameraSettings = - currentCameraSettings.copy(supportedStabilizationModes = stabilizationModes) - } - override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { currentCameraSettings = currentCameraSettings.copy(dynamicRange = dynamicRange) } - override suspend fun updateSupportedDynamicRanges(supportedDynamicRanges: List) { - currentCameraSettings = - currentCameraSettings.copy(supportedDynamicRanges = supportedDynamicRanges) - } - override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { currentCameraSettings = currentCameraSettings.copy(aspectRatio = aspectRatio) @@ -121,12 +78,4 @@ object FakeSettingsRepository : SettingsRepository { currentCameraSettings = currentCameraSettings.copy(targetFrameRate = targetFrameRate) } - - override suspend fun updateSupportedFixedFrameRate( - supportedFrameRates: Set, - currentTargetFrameRate: Int - ) { - currentCameraSettings = - currentCameraSettings.copy(supportedFixedFrameRates = supportedFrameRates.toList()) - } } diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto index 0dddee96..cc87e43c 100644 --- a/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto +++ b/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto @@ -30,20 +30,16 @@ option java_package = "com.google.jetpackcamera.settings"; option java_multiple_files = true; message JcaSettings { - bool default_front_camera = 2 [deprecated = true]; - bool front_camera_available = 3; - bool back_camera_available = 4; - DarkMode dark_mode_status = 5; - FlashMode flash_mode_status = 6; - AspectRatio aspect_ratio_status = 7; - CaptureMode capture_mode_status = 8; - PreviewStabilization stabilize_preview = 9; - VideoStabilization stabilize_video = 10; - bool stabilize_video_supported = 11; - bool stabilize_preview_supported = 12; - DynamicRange dynamic_range_status = 13; - repeated DynamicRange supported_dynamic_ranges = 14; - LensFacing default_lens_facing = 15; - int32 target_frame_rate = 16; - repeated int32 supported_frame_rates = 17; + // Camera settings + LensFacing default_lens_facing = 1; + FlashMode flash_mode_status = 2; + int32 target_frame_rate = 3; + AspectRatio aspect_ratio_status = 4; + CaptureMode capture_mode_status = 5; + PreviewStabilization stabilize_preview = 6; + VideoStabilization stabilize_video = 7; + DynamicRange dynamic_range_status = 8; + + // Non-camera app settings + DarkMode dark_mode_status = 9; } \ No newline at end of file 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 779422b4..f06b700a 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 @@ -48,17 +48,21 @@ import androidx.concurrent.futures.await import androidx.core.content.ContextCompat import com.google.jetpackcamera.domain.camera.CameraUseCase.ScreenFlashEvent.Type import com.google.jetpackcamera.domain.camera.effects.SingleSurfaceForcingEffect +import com.google.jetpackcamera.settings.SettableConstraintsRepository import com.google.jetpackcamera.settings.SettingsRepository import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.CameraConstraints import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.Stabilization import com.google.jetpackcamera.settings.model.SupportedStabilizationMode +import com.google.jetpackcamera.settings.model.SystemConstraints import dagger.hilt.android.scopes.ViewModelScoped import java.io.FileNotFoundException +import java.lang.IllegalArgumentException import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -101,9 +105,9 @@ constructor( private val application: Application, private val coroutineScope: CoroutineScope, private val defaultDispatcher: CoroutineDispatcher, - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, + private val constraintsRepository: SettableConstraintsRepository ) : CameraUseCase { - private val fixedFrameRates = setOf(15, 30, 60) private lateinit var cameraProvider: ProcessCameraProvider private val imageCaptureUseCase = ImageCapture.Builder().build() @@ -112,6 +116,7 @@ constructor( private lateinit var videoCaptureUseCase: VideoCapture private var recording: Recording? = null private lateinit var captureMode: CaptureMode + private lateinit var systemConstraints: SystemConstraints private val screenFlashEvents: MutableSharedFlow = MutableSharedFlow() @@ -121,37 +126,57 @@ constructor( override suspend fun initialize() { cameraProvider = ProcessCameraProvider.getInstance(application).await() - // updates values for available camera lens - val availableCameraLens = + // updates values for available cameras + val availableCameraLenses = listOf( LensFacing.FRONT, LensFacing.BACK - ).filter { lensFacing -> - cameraProvider.hasCamera(lensFacing.toCameraSelector()) + ).filter { + cameraProvider.hasCamera(it.toCameraSelector()) } - // updates values for available camera lens if necessary - settingsRepository.updateAvailableCameraLens( - availableCameraLens.contains(LensFacing.FRONT), - availableCameraLens.contains(LensFacing.BACK) - ) + // Build and update the system constraints + systemConstraints = SystemConstraints( + availableLenses = availableCameraLenses, + perLensConstraints = buildMap { + val availableCameraInfos = cameraProvider.availableCameraInfos + for (lensFacing in availableCameraLenses) { + val selector = lensFacing.toCameraSelector() + selector.filter(availableCameraInfos).firstOrNull()?.let { camInfo -> + val supportedDynamicRanges = + Recorder.getVideoCapabilities(camInfo).supportedDynamicRanges + .mapNotNull(CXDynamicRange::toSupportedAppDynamicRange) + .toSet() + + val supportedStabilizationModes = buildSet { + if (isPreviewStabilizationSupported(camInfo)) { + add(SupportedStabilizationMode.ON) + } - currentSettings.value = settingsRepository.cameraAppSettings.first() - } + if (isVideoStabilizationSupported(camInfo)) { + add(SupportedStabilizationMode.HIGH_QUALITY) + } + } - /** - * Returns the union of supported fixed frame rates fom a device's cameras - */ - private fun getDeviceSupportedFrameRates(): Set { - val supportedFixedFrameRates = mutableSetOf() - cameraProvider.availableCameraInfos.forEach { cameraInfo -> - cameraInfo.supportedFrameRateRanges.forEach { e -> - if (e.upper == e.lower && fixedFrameRates.contains(e.upper)) { - supportedFixedFrameRates.add(e.upper) + val supportedFixedFrameRates = getSupportedFrameRates(camInfo) + + put( + lensFacing, + CameraConstraints( + supportedStabilizationModes = supportedStabilizationModes, + supportedFixedFrameRates = supportedFixedFrameRates, + supportedDynamicRanges = supportedDynamicRanges + ) + ) + } } } - } - return supportedFixedFrameRates + ) + + constraintsRepository.updateSystemConstraints(systemConstraints) + + currentSettings.value = + settingsRepository.defaultCameraAppSettings.first().tryApplyDynamicRangeConstraints() } /** @@ -232,25 +257,13 @@ constructor( cameraProvider.availableCameraInfos ).first() - // get device-supported fixed frame rates - settingsRepository.updateSupportedFixedFrameRate( - getDeviceSupportedFrameRates(), - sessionSettings.targetFrameRate - ) - - // get device-supported stabilization modes - val supportedStabilizationModes = getDeviceSupportedStabilizations() - - settingsRepository.updatePreviewStabilizationSupported( - supportedStabilizationModes.contains(SupportedStabilizationMode.ON) - ) - settingsRepository.updateVideoStabilizationSupported( - supportedStabilizationModes.contains(SupportedStabilizationMode.HIGH_QUALITY) - ) - - settingsRepository.updateSupportedDynamicRanges( - getSupportedDynamicRanges(cameraInfo) - ) + val lensFacing = sessionSettings.cameraSelector.toAppLensFacing() + val cameraConstraints = checkNotNull( + systemConstraints.perLensConstraints[lensFacing] + ) { + "Unable to retrieve CameraConstraints for $lensFacing. " + + "Was the use case initialized?" + } val initialTransientSettings = transientSettings .filterNotNull() @@ -259,7 +272,7 @@ constructor( val useCaseGroup = createUseCaseGroup( sessionSettings, initialTransientSettings, - supportedStabilizationModes.toList(), + cameraConstraints.supportedStabilizationModes, effect = when (sessionSettings.captureMode) { CaptureMode.SINGLE_STREAM -> SingleSurfaceForcingEffect(coroutineScope) CaptureMode.MULTI_STREAM -> null @@ -474,10 +487,31 @@ constructor( // Sets the camera to the designated lensFacing direction override suspend fun setLensFacing(lensFacing: LensFacing) { currentSettings.update { old -> - old?.copy(cameraLensFacing = lensFacing) + if (systemConstraints.availableLenses.contains(lensFacing)) { + old?.copy(cameraLensFacing = lensFacing) + ?.tryApplyDynamicRangeConstraints() + } else { + old + } } } + private fun CameraAppSettings.tryApplyDynamicRangeConstraints(): CameraAppSettings { + return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + with(constraints.supportedDynamicRanges) { + val newDynamicRange = if (contains(dynamicRange)) { + dynamicRange + } else { + DynamicRange.SDR + } + + this@tryApplyDynamicRangeConstraints.copy( + dynamicRange = newDynamicRange + ) + } + } ?: this + } + override fun tapToFocus( display: Display, surfaceWidth: Int, @@ -566,7 +600,7 @@ constructor( private fun createUseCaseGroup( sessionSettings: PerpetualSessionSettings, initialTransientSettings: TransientSessionSettings, - supportedStabilizationModes: List, + supportedStabilizationModes: Set, effect: CameraEffect? = null ): UseCaseGroup { val previewUseCase = createPreviewUseCase(sessionSettings, supportedStabilizationModes) @@ -601,14 +635,9 @@ constructor( } } - private fun getSupportedDynamicRanges(cameraInfo: CameraInfo): List { - return Recorder - .getVideoCapabilities(cameraInfo).supportedDynamicRanges.toSupportedAppDynamicRanges() - } - private fun createVideoUseCase( sessionSettings: PerpetualSessionSettings, - supportedStabilizationMode: List + supportedStabilizationMode: Set ): VideoCapture { return VideoCapture.Builder(recorder).apply { // set video stabilization @@ -629,7 +658,7 @@ constructor( private fun shouldVideoBeStabilized( sessionSettings: PerpetualSessionSettings, - supportedStabilizationModes: List + supportedStabilizationModes: Set ): Boolean { // video is on and target fps is not 60 return (sessionSettings.targetFrameRate != TARGET_FPS_60) && @@ -643,7 +672,7 @@ constructor( private fun createPreviewUseCase( sessionSettings: PerpetualSessionSettings, - supportedStabilizationModes: List + supportedStabilizationModes: Set ): Preview { val previewUseCaseBuilder = Preview.Builder() // set preview stabilization @@ -660,7 +689,7 @@ constructor( private fun shouldPreviewBeStabilized( sessionSettings: PerpetualSessionSettings, - supportedStabilizationModes: List + supportedStabilizationModes: Set ): Boolean { // only supported if target fps is 30 or none return ( @@ -675,12 +704,9 @@ constructor( ) } - private fun LensFacing.toCameraSelector(): CameraSelector = when (this) { - LensFacing.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA - LensFacing.BACK -> CameraSelector.DEFAULT_BACK_CAMERA - } - companion object { + private val FIXED_FRAME_RATES = setOf(TARGET_FPS_15, TARGET_FPS_30, TARGET_FPS_60) + /** * Checks if preview stabilization is supported by the device. * @@ -696,6 +722,16 @@ constructor( private fun isVideoStabilizationSupported(cameraInfo: CameraInfo): Boolean { return Recorder.getVideoCapabilities(cameraInfo).isStabilizationSupported } + + private fun getSupportedFrameRates(camInfo: CameraInfo): Set { + return buildSet { + camInfo.supportedFrameRateRanges.forEach { e -> + if (e.upper == e.lower && FIXED_FRAME_RATES.contains(e.upper)) { + add(e.upper) + } + } + } + } } } @@ -714,8 +750,16 @@ private fun DynamicRange.toCXDynamicRange(): CXDynamicRange { DynamicRange.HLG10 -> CXDynamicRange.HLG_10_BIT } } -private fun Set.toSupportedAppDynamicRanges(): List { - return this.mapNotNull { - it.toSupportedAppDynamicRange() - } + +private fun LensFacing.toCameraSelector(): CameraSelector = when (this) { + LensFacing.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA + LensFacing.BACK -> CameraSelector.DEFAULT_BACK_CAMERA +} + +private fun CameraSelector.toAppLensFacing(): LensFacing = when (this) { + CameraSelector.DEFAULT_FRONT_CAMERA -> LensFacing.FRONT + CameraSelector.DEFAULT_BACK_CAMERA -> LensFacing.BACK + else -> throw IllegalArgumentException( + "Unknown CameraSelector -> LensFacing mapping. [CameraSelector: $this]" + ) } 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 ab28a6fd..fa17b8e0 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 @@ -57,9 +57,11 @@ import com.google.jetpackcamera.feature.preview.ui.TestableToast import com.google.jetpackcamera.feature.quicksettings.QuickSettingsScreenOverlay import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CaptureMode +import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing +import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS private const val TAG = "PreviewScreen" @@ -93,11 +95,11 @@ fun PreviewScreen( } } - when (previewUiState.cameraState) { - CameraState.NOT_READY -> LoadingScreen(modifier) - CameraState.READY -> ContentScreen( + when (val currentUiState = previewUiState) { + is PreviewUiState.NotReady -> LoadingScreen() + is PreviewUiState.Ready -> ContentScreen( modifier = modifier, - previewUiState = previewUiState, + previewUiState = currentUiState, previewMode = previewMode, screenFlashUiState = screenFlashUiState, surfaceRequest = surfaceRequest, @@ -125,7 +127,7 @@ fun PreviewScreen( @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable private fun ContentScreen( - previewUiState: PreviewUiState, + previewUiState: PreviewUiState.Ready, previewMode: PreviewMode, screenFlashUiState: ScreenFlash.ScreenFlashUiState, surfaceRequest: SurfaceRequest?, @@ -186,6 +188,7 @@ private fun ContentScreen( isOpen = previewUiState.quickSettingsIsOpen, toggleIsOpen = onToggleQuickSettings, currentCameraSettings = previewUiState.currentCameraSettings, + systemConstraints = previewUiState.systemConstraints, onLensFaceClick = onSetLensFacing, onFlashModeClick = onChangeFlash, onAspectRatioClick = onChangeAspectRatio, @@ -205,7 +208,6 @@ private fun ContentScreen( onStartVideoRecording = onStartVideoRecording, onStopVideoRecording = onStopVideoRecording, blinkState = blinkState - ) // displays toast when there is a message to show if (previewUiState.toastMessageToShow != null) { @@ -257,7 +259,7 @@ private fun LoadingScreen(modifier: Modifier = Modifier) { private fun ContentScreenPreview() { MaterialTheme { ContentScreen( - previewUiState = PreviewUiState(), + previewUiState = FAKE_PREVIEW_UI_STATE_READY, previewMode = PreviewMode.StandardMode {}, screenFlashUiState = ScreenFlash.ScreenFlashUiState(), surfaceRequest = null @@ -270,7 +272,7 @@ private fun ContentScreenPreview() { private fun ContentScreen_WhileRecording() { MaterialTheme(colorScheme = darkColorScheme()) { ContentScreen( - previewUiState = PreviewUiState( + previewUiState = FAKE_PREVIEW_UI_STATE_READY.copy( videoRecordingState = VideoRecordingState.ACTIVE ), previewMode = PreviewMode.StandardMode {}, @@ -279,3 +281,8 @@ private fun ContentScreen_WhileRecording() { ) } } + +private val FAKE_PREVIEW_UI_STATE_READY = PreviewUiState.Ready( + currentCameraSettings = DEFAULT_CAMERA_APP_SETTINGS, + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS +) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt index 745fc073..c2336e45 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt @@ -15,27 +15,30 @@ */ package com.google.jetpackcamera.feature.preview -import androidx.camera.core.CameraSelector import com.google.jetpackcamera.feature.preview.ui.SnackBarData import com.google.jetpackcamera.feature.preview.ui.ToastMessage import com.google.jetpackcamera.settings.model.CameraAppSettings -import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS +import com.google.jetpackcamera.settings.model.SystemConstraints /** * Defines the current state of the [PreviewScreen]. */ -data class PreviewUiState( - val cameraState: CameraState = CameraState.NOT_READY, - // "quick" settings - val currentCameraSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS, - val lensFacing: Int = CameraSelector.LENS_FACING_BACK, - val zoomScale: Float = 1f, - val videoRecordingState: VideoRecordingState = VideoRecordingState.INACTIVE, - val quickSettingsIsOpen: Boolean = false, - // todo: remove after implementing post capture screen - val toastMessageToShow: ToastMessage? = null, - val snackBarToShow: SnackBarData? = null -) +sealed interface PreviewUiState { + object NotReady : PreviewUiState + + data class Ready( + // "quick" settings + val currentCameraSettings: CameraAppSettings, + val systemConstraints: SystemConstraints, + val zoomScale: Float = 1f, + val videoRecordingState: VideoRecordingState = VideoRecordingState.INACTIVE, + val quickSettingsIsOpen: Boolean = false, + + // todo: remove after implementing post capture screen + val toastMessageToShow: ToastMessage? = null, + val snackBarToShow: SnackBarData? = null + ) : PreviewUiState +} /** * Defines the current state of Video Recording @@ -51,18 +54,3 @@ enum class VideoRecordingState { */ ACTIVE } - -/** - * Defines the current state of the camera. - */ -enum class CameraState { - /** - * Camera hasn't been initialized. - */ - NOT_READY, - - /** - * Camera is open and presenting a preview stream. - */ - READY -} 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 d16f9bda..85a2bb12 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 @@ -27,9 +27,9 @@ import com.google.jetpackcamera.domain.camera.CameraUseCase import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.feature.preview.ui.SnackBarData +import com.google.jetpackcamera.settings.ConstraintsRepository import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CaptureMode -import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing @@ -42,8 +42,10 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch private const val TAG = "PreviewViewModel" @@ -54,12 +56,15 @@ private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture" */ @HiltViewModel class PreviewViewModel @Inject constructor( - private val cameraUseCase: CameraUseCase + private val cameraUseCase: CameraUseCase, + private val constraintsRepository: ConstraintsRepository + ) : ViewModel() { private val _previewUiState: MutableStateFlow = - MutableStateFlow(PreviewUiState(currentCameraSettings = DEFAULT_CAMERA_APP_SETTINGS)) + MutableStateFlow(PreviewUiState.NotReady) - val previewUiState: StateFlow = _previewUiState + val previewUiState: StateFlow = + _previewUiState.asStateFlow() val surfaceRequest: StateFlow = cameraUseCase.getSurfaceRequest() @@ -73,29 +78,33 @@ class PreviewViewModel @Inject constructor( // used to ensure we don't start the camera before initialization is complete. private var initializationDeferred: Deferred = viewModelScope.async { cameraUseCase.initialize() - _previewUiState.emit( - previewUiState.value.copy( - cameraState = CameraState.READY - ) - ) } init { viewModelScope.launch { combine( cameraUseCase.getCurrentSettings().filterNotNull(), + constraintsRepository.systemConstraints.filterNotNull(), cameraUseCase.getZoomScale() - ) { cameraAppSettings, zoomScale -> - previewUiState.value.copy( - currentCameraSettings = cameraAppSettings, - zoomScale = zoomScale - ) - }.collect { - // TODO: only update settings that were actually changed - // currently resets all "quick" settings to stored settings - Log.d(TAG, "UPDATE UI STATE: ${it.zoomScale}") - _previewUiState.emit(it) - } + ) { cameraAppSettings, systemConstraints, zoomScale -> + _previewUiState.update { old -> + when (old) { + is PreviewUiState.Ready -> + old.copy( + currentCameraSettings = cameraAppSettings, + systemConstraints = systemConstraints, + zoomScale = zoomScale + ) + + is PreviewUiState.NotReady -> + PreviewUiState.Ready( + currentCameraSettings = cameraAppSettings, + systemConstraints = systemConstraints, + zoomScale = zoomScale + ) + } + } + }.collect {} } } @@ -142,54 +151,15 @@ class PreviewViewModel @Inject constructor( /** Sets the camera to a designated lens facing */ fun setLensFacing(newLensFacing: LensFacing) { viewModelScope.launch { - // TODO(tm): Move constraint checks into CameraUseCase - if (( - newLensFacing == LensFacing.BACK && - previewUiState.value.currentCameraSettings.isBackCameraAvailable - ) || - ( - newLensFacing == LensFacing.FRONT && - previewUiState.value.currentCameraSettings.isFrontCameraAvailable - ) - ) { - // apply to cameraUseCase - cameraUseCase.setLensFacing(newLensFacing) - } + // apply to cameraUseCase + cameraUseCase.setLensFacing(newLensFacing) } } fun captureImage() { Log.d(TAG, "captureImage") viewModelScope.launch { - traceAsync(IMAGE_CAPTURE_TRACE, 0) { - try { - cameraUseCase.takePicture() - // todo: remove toast after postcapture screen implemented - _previewUiState.emit( - previewUiState.value.copy( - snackBarToShow = SnackBarData( - stringResource = R.string.toast_image_capture_success, - withDismissAction = true, - testTag = IMAGE_CAPTURE_SUCCESS_TAG - ) - ) - ) - Log.d(TAG, "cameraUseCase.takePicture success") - } catch (exception: Exception) { - // todo: remove toast after postcapture screen implemented - _previewUiState.emit( - previewUiState.value.copy( - snackBarToShow = SnackBarData( - stringResource = R.string.toast_capture_failure, - withDismissAction = true, - testTag = IMAGE_CAPTURE_FAILURE_TAG - ) - ) - ) - Log.d(TAG, "cameraUseCase.takePicture error") - Log.d(TAG, exception.toString()) - } - } + captureImageInternal(cameraUseCase::takePicture) } } @@ -201,38 +171,49 @@ class PreviewViewModel @Inject constructor( ) { Log.d(TAG, "captureImageWithUri") viewModelScope.launch { - traceAsync(IMAGE_CAPTURE_TRACE, 0) { - try { - val savedUri = - cameraUseCase.takePicture(contentResolver, imageCaptureUri, ignoreUri) - .savedUri - // todo: remove toast after postcapture screen implemented - _previewUiState.emit( - previewUiState.value.copy( - snackBarToShow = SnackBarData( - stringResource = R.string.toast_image_capture_success, - withDismissAction = true, - testTag = IMAGE_CAPTURE_SUCCESS_TAG - ) - ) - ) - onImageCapture(ImageCaptureEvent.ImageSaved(savedUri)) - Log.d(TAG, "cameraUseCase.takePicture success") - } catch (exception: Exception) { - // todo: remove toast after postcapture screen implemented - _previewUiState.emit( - previewUiState.value.copy( - snackBarToShow = SnackBarData( - stringResource = R.string.toast_capture_failure, - withDismissAction = true, - testTag = IMAGE_CAPTURE_FAILURE_TAG - ) - ) - ) - Log.d(TAG, "cameraUseCase.takePicture error") - Log.d(TAG, exception.toString()) + captureImageInternal( + doTakePicture = { + cameraUseCase.takePicture(contentResolver, imageCaptureUri, ignoreUri).savedUri + }, + onSuccess = { savedUri -> onImageCapture(ImageCaptureEvent.ImageSaved(savedUri)) }, + onFailure = { exception -> onImageCapture(ImageCaptureEvent.ImageCaptureError(exception)) } + ) + } + } + + private suspend fun captureImageInternal( + doTakePicture: suspend () -> T, + onSuccess: (T) -> Unit = {}, + onFailure: (exception: Exception) -> Unit = {} + ) { + try { + traceAsync(IMAGE_CAPTURE_TRACE, 0) { + doTakePicture() + }.also { result -> + onSuccess(result) + } + Log.d(TAG, "cameraUseCase.takePicture success") + SnackBarData( + stringResource = R.string.toast_image_capture_success, + withDismissAction = true, + testTag = IMAGE_CAPTURE_SUCCESS_TAG + ) + } catch (exception: Exception) { + onFailure(exception) + Log.d(TAG, "cameraUseCase.takePicture error", exception) + SnackBarData( + stringResource = R.string.toast_capture_failure, + withDismissAction = true, + testTag = IMAGE_CAPTURE_FAILURE_TAG + ) + }.also { snackBarData -> + _previewUiState.update { old -> + (old as? PreviewUiState.Ready)?.copy( + // todo: remove toast after postcapture screen implemented + snackBarToShow = snackBarData + ) ?: old } } } @@ -242,40 +223,35 @@ class PreviewViewModel @Inject constructor( recordingJob = viewModelScope.launch { try { cameraUseCase.startVideoRecording { - when (it) { + val snackBarData = when (it) { CameraUseCase.OnVideoRecordEvent.OnVideoRecorded -> - viewModelScope.launch { - _previewUiState.emit( - previewUiState.value.copy( - snackBarToShow = SnackBarData( - stringResource = R.string.toast_video_capture_success, - withDismissAction = true - ) - ) - ) - } - - else -> viewModelScope.launch { - _previewUiState.emit( - previewUiState.value.copy( - snackBarToShow = SnackBarData( - stringResource = R.string.toast_video_capture_failure, - withDismissAction = true - ) - ) + SnackBarData( + stringResource = R.string.toast_video_capture_success, + withDismissAction = true ) + else -> + SnackBarData( + stringResource = R.string.toast_video_capture_failure, + withDismissAction = true + ) + } + + viewModelScope.launch { + _previewUiState.update { old -> + (old as? PreviewUiState.Ready)?.copy( + snackBarToShow = snackBarData + ) ?: old } } } - _previewUiState.emit( - previewUiState.value.copy( + _previewUiState.update { old -> + (old as? PreviewUiState.Ready)?.copy( videoRecordingState = VideoRecordingState.ACTIVE - ) - ) + ) ?: old + } Log.d(TAG, "cameraUseCase.startRecording success") } catch (exception: IllegalStateException) { - Log.d(TAG, "cameraUseCase.startVideoRecording error") - Log.d(TAG, exception.toString()) + Log.d(TAG, "cameraUseCase.startVideoRecording error", exception) } } } @@ -283,11 +259,11 @@ class PreviewViewModel @Inject constructor( fun stopVideoRecording() { Log.d(TAG, "stopVideoRecording") viewModelScope.launch { - _previewUiState.emit( - previewUiState.value.copy( + _previewUiState.update { old -> + (old as? PreviewUiState.Ready)?.copy( videoRecordingState = VideoRecordingState.INACTIVE - ) - ) + ) ?: old + } } cameraUseCase.stopVideoRecording() recordingJob?.cancel() @@ -305,16 +281,12 @@ class PreviewViewModel @Inject constructor( // modify ui values fun toggleQuickSettings() { - toggleQuickSettings(!previewUiState.value.quickSettingsIsOpen) - } - - private fun toggleQuickSettings(isOpen: Boolean) { viewModelScope.launch { - _previewUiState.emit( - previewUiState.value.copy( - quickSettingsIsOpen = isOpen - ) - ) + _previewUiState.update { old -> + (old as? PreviewUiState.Ready)?.copy( + quickSettingsIsOpen = !old.quickSettingsIsOpen + ) ?: old + } } } @@ -329,27 +301,27 @@ class PreviewViewModel @Inject constructor( } /** - * Sets current value of [PreviewUiState.toastMessageToShow] to null. + * Sets current value of [PreviewUiState.Ready.toastMessageToShow] to null. */ fun onToastShown() { viewModelScope.launch { // keeps the composable up on screen longer to be detected by UiAutomator delay(2.seconds) - _previewUiState.emit( - previewUiState.value.copy( + _previewUiState.update { old -> + (old as? PreviewUiState.Ready)?.copy( toastMessageToShow = null - ) - ) + ) ?: old + } } } fun onSnackBarResult() { viewModelScope.launch { - _previewUiState.emit( - previewUiState.value.copy( + _previewUiState.update { old -> + (old as? PreviewUiState.Ready)?.copy( snackBarToShow = null - ) - ) + ) ?: old + } } } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt index 7f218ded..2d1eefe2 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt @@ -48,8 +48,10 @@ import com.google.jetpackcamera.feature.quicksettings.ui.QuickSettingsIndicators import com.google.jetpackcamera.feature.quicksettings.ui.ToggleQuickSettingsButton import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.FlashMode +import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.Stabilization -import com.google.jetpackcamera.settings.model.SupportedStabilizationMode +import com.google.jetpackcamera.settings.model.SystemConstraints +import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -66,7 +68,7 @@ class ZoomLevelDisplayState(showInitially: Boolean = false) { @Composable fun CameraControlsOverlay( - previewUiState: PreviewUiState, + previewUiState: PreviewUiState.Ready, previewMode: PreviewMode, blinkState: BlinkState, modifier: Modifier = Modifier, @@ -117,7 +119,7 @@ fun CameraControlsOverlay( zoomLevel = previewUiState.zoomScale, showZoomLevel = zoomLevelDisplayState.showZoomLevel, isQuickSettingsOpen = previewUiState.quickSettingsIsOpen, - currentCameraSettings = previewUiState.currentCameraSettings, + systemConstraints = previewUiState.systemConstraints, videoRecordingState = previewUiState.videoRecordingState, previewMode = previewMode, onFlipCamera = onFlipCamera, @@ -167,7 +169,6 @@ private fun ControlsTop( horizontalArrangement = Arrangement.SpaceEvenly ) { StabilizationIcon( - supportedStabilizationMode = currentCameraSettings.supportedStabilizationModes, videoStabilization = currentCameraSettings.videoCaptureStabilization, previewStabilization = currentCameraSettings.previewStabilization ) @@ -180,7 +181,7 @@ private fun ControlsBottom( zoomLevel: Float, showZoomLevel: Boolean, isQuickSettingsOpen: Boolean, - currentCameraSettings: CameraAppSettings, + systemConstraints: SystemConstraints, videoRecordingState: VideoRecordingState, previewMode: PreviewMode, modifier: Modifier = Modifier, @@ -212,8 +213,7 @@ private fun ControlsBottom( modifier = Modifier.testTag(FLIP_CAMERA_BUTTON), onClick = onFlipCamera, // enable only when phone has front and rear camera - enabledCondition = currentCameraSettings.isBackCameraAvailable && - currentCameraSettings.isFrontCameraAvailable + enabledCondition = systemConstraints.availableLenses.size > 1 ) } } @@ -346,7 +346,6 @@ private fun Preview_ControlsTop_WithStabilization() { ControlsTop( isQuickSettingsOpen = false, currentCameraSettings = CameraAppSettings( - supportedStabilizationModes = listOf(SupportedStabilizationMode.HIGH_QUALITY), videoCaptureStabilization = Stabilization.ON, previewStabilization = Stabilization.ON ) @@ -362,7 +361,7 @@ private fun Preview_ControlsBottom() { zoomLevel = 1.3f, showZoomLevel = true, isQuickSettingsOpen = false, - currentCameraSettings = CameraAppSettings(), + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, videoRecordingState = VideoRecordingState.INACTIVE, previewMode = PreviewMode.StandardMode {} ) @@ -377,7 +376,7 @@ private fun Preview_ControlsBottom_NoZoomLevel() { zoomLevel = 1.3f, showZoomLevel = false, isQuickSettingsOpen = false, - currentCameraSettings = CameraAppSettings(), + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, videoRecordingState = VideoRecordingState.INACTIVE, previewMode = PreviewMode.StandardMode {} ) @@ -392,7 +391,7 @@ private fun Preview_ControlsBottom_QuickSettingsOpen() { zoomLevel = 1.3f, showZoomLevel = true, isQuickSettingsOpen = true, - currentCameraSettings = CameraAppSettings(), + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, videoRecordingState = VideoRecordingState.INACTIVE, previewMode = PreviewMode.StandardMode {} ) @@ -407,7 +406,13 @@ private fun Preview_ControlsBottom_NoFlippableCamera() { zoomLevel = 1.3f, showZoomLevel = true, isQuickSettingsOpen = false, - currentCameraSettings = CameraAppSettings(isBackCameraAvailable = false), + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS.copy( + availableLenses = listOf(LensFacing.FRONT), + perLensConstraints = mapOf( + LensFacing.FRONT to + TYPICAL_SYSTEM_CONSTRAINTS.perLensConstraints[LensFacing.FRONT]!! + ) + ), videoRecordingState = VideoRecordingState.INACTIVE, previewMode = PreviewMode.StandardMode {} ) @@ -422,7 +427,7 @@ private fun Preview_ControlsBottom_Recording() { zoomLevel = 1.3f, showZoomLevel = true, isQuickSettingsOpen = false, - currentCameraSettings = CameraAppSettings(), + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, videoRecordingState = VideoRecordingState.ACTIVE, previewMode = PreviewMode.StandardMode {} ) 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 28c13b91..6896a95e 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 @@ -68,7 +68,6 @@ import com.google.jetpackcamera.feature.preview.R import com.google.jetpackcamera.feature.preview.VideoRecordingState import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.Stabilization -import com.google.jetpackcamera.settings.model.SupportedStabilizationMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -235,14 +234,11 @@ class BlinkState( @Composable fun StabilizationIcon( - supportedStabilizationMode: List, videoStabilization: Stabilization, previewStabilization: Stabilization, modifier: Modifier = Modifier ) { - if (supportedStabilizationMode.isNotEmpty() && - (videoStabilization == Stabilization.ON || previewStabilization == Stabilization.ON) - ) { + if (videoStabilization == Stabilization.ON || previewStabilization == Stabilization.ON) { val descriptionText = if (videoStabilization == Stabilization.ON) { stringResource(id = R.string.stabilization_icon_description_preview_and_video) } else { diff --git a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt index f4f163e5..c00fbe01 100644 --- a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt +++ b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt @@ -18,8 +18,10 @@ package com.google.jetpackcamera.feature.preview import android.content.ContentResolver import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.domain.camera.test.FakeCameraUseCase +import com.google.jetpackcamera.settings.SettableConstraintsRepositoryImpl import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing +import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -35,12 +37,15 @@ import org.mockito.Mockito.mock class PreviewViewModelTest { private val cameraUseCase = FakeCameraUseCase() + private val constraintsRepository = SettableConstraintsRepositoryImpl().apply { + updateSystemConstraints(TYPICAL_SYSTEM_CONSTRAINTS) + } private lateinit var previewViewModel: PreviewViewModel @Before fun setup() = runTest(StandardTestDispatcher()) { Dispatchers.setMain(StandardTestDispatcher()) - previewViewModel = PreviewViewModel(cameraUseCase) + previewViewModel = PreviewViewModel(cameraUseCase, constraintsRepository) advanceUntilIdle() } @@ -48,7 +53,7 @@ class PreviewViewModelTest { fun getPreviewUiState() = runTest(StandardTestDispatcher()) { advanceUntilIdle() val uiState = previewViewModel.previewUiState.value - assertThat(uiState.cameraState).isEqualTo(CameraState.READY) + assertThat(uiState).isInstanceOf(PreviewUiState.Ready::class.java) } @Test @@ -97,22 +102,26 @@ class PreviewViewModelTest { previewViewModel.startCamera() previewViewModel.setFlash(FlashMode.AUTO) advanceUntilIdle() - assertThat(previewViewModel.previewUiState.value.currentCameraSettings.flashMode) - .isEqualTo(FlashMode.AUTO) + + assertIsReady(previewViewModel.previewUiState.value).also { + assertThat(it.currentCameraSettings.flashMode).isEqualTo(FlashMode.AUTO) + } } @Test fun flipCamera() = runTest(StandardTestDispatcher()) { // initial default value should be back previewViewModel.startCamera() - assertThat(previewViewModel.previewUiState.value.currentCameraSettings.cameraLensFacing) - .isEqualTo(LensFacing.BACK) + assertIsReady(previewViewModel.previewUiState.value).also { + assertThat(it.currentCameraSettings.cameraLensFacing).isEqualTo(LensFacing.BACK) + } previewViewModel.setLensFacing(LensFacing.FRONT) advanceUntilIdle() // ui state and camera should both be true now - assertThat(previewViewModel.previewUiState.value.currentCameraSettings.cameraLensFacing) - .isEqualTo(LensFacing.FRONT) + assertIsReady(previewViewModel.previewUiState.value).also { + assertThat(it.currentCameraSettings.cameraLensFacing).isEqualTo(LensFacing.FRONT) + } assertThat(cameraUseCase.isLensFacingFront).isTrue() } @@ -122,3 +131,12 @@ class PreviewViewModelTest { advanceUntilIdle() } } + +private fun assertIsReady(previewUiState: PreviewUiState): PreviewUiState.Ready { + return when (previewUiState) { + is PreviewUiState.Ready -> previewUiState + else -> throw AssertionError( + "PreviewUiState expected to be Ready, but was ${previewUiState::class}" + ) + } +} 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 a6176bd6..a93281d0 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 @@ -57,6 +57,9 @@ import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing +import com.google.jetpackcamera.settings.model.SystemConstraints +import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS +import com.google.jetpackcamera.settings.model.forCurrentLens /** * The UI component for quick settings. @@ -64,6 +67,7 @@ import com.google.jetpackcamera.settings.model.LensFacing @Composable fun QuickSettingsScreenOverlay( currentCameraSettings: CameraAppSettings, + systemConstraints: SystemConstraints, toggleIsOpen: () -> Unit, onLensFaceClick: (lensFace: LensFacing) -> Unit, onFlashModeClick: (flashMode: FlashMode) -> Unit, @@ -110,6 +114,7 @@ fun QuickSettingsScreenOverlay( ) { ExpandedQuickSettingsUi( currentCameraSettings = currentCameraSettings, + systemConstraints = systemConstraints, shouldShowQuickSetting = shouldShowQuickSetting, setVisibleQuickSetting = { enum: IsExpandedQuickSetting -> shouldShowQuickSetting = enum @@ -138,6 +143,7 @@ private enum class IsExpandedQuickSetting { @Composable private fun ExpandedQuickSettingsUi( currentCameraSettings: CameraAppSettings, + systemConstraints: SystemConstraints, onLensFaceClick: (newLensFace: LensFacing) -> Unit, onFlashModeClick: (flashMode: FlashMode) -> Unit, onAspectRatioClick: (aspectRation: AspectRatio) -> Unit, @@ -204,7 +210,8 @@ private fun ExpandedQuickSettingsUi( onClick = { d: DynamicRange -> onDynamicRangeClick(d) }, selectedDynamicRange = currentCameraSettings.dynamicRange, hdrDynamicRange = currentCameraSettings.defaultHdrDynamicRange, - enabled = currentCameraSettings.supportedDynamicRanges.size > 1 + enabled = systemConstraints.forCurrentLens(currentCameraSettings) + ?.let { it.supportedDynamicRanges.size > 1 } ?: false ) } } @@ -227,6 +234,7 @@ fun ExpandedQuickSettingsUiPreview() { MaterialTheme { ExpandedQuickSettingsUi( currentCameraSettings = CameraAppSettings(), + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, onLensFaceClick = { }, onFlashModeClick = { }, shouldShowQuickSetting = IsExpandedQuickSetting.NONE, @@ -243,10 +251,8 @@ fun ExpandedQuickSettingsUiPreview() { fun ExpandedQuickSettingsUiPreview_WithHdr() { MaterialTheme { ExpandedQuickSettingsUi( - currentCameraSettings = CameraAppSettings( - supportedDynamicRanges = listOf(DynamicRange.SDR, DynamicRange.HLG10), - dynamicRange = DynamicRange.HLG10 - ), + currentCameraSettings = CameraAppSettings(dynamicRange = DynamicRange.HLG10), + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS_WITH_HDR, onLensFaceClick = { }, onFlashModeClick = { }, shouldShowQuickSetting = IsExpandedQuickSetting.NONE, @@ -257,3 +263,13 @@ fun ExpandedQuickSettingsUiPreview_WithHdr() { ) } } + +private val TYPICAL_SYSTEM_CONSTRAINTS_WITH_HDR = + TYPICAL_SYSTEM_CONSTRAINTS.copy( + perLensConstraints = TYPICAL_SYSTEM_CONSTRAINTS.perLensConstraints.entries.associate { + (lensFacing, constraints) -> + lensFacing to constraints.copy( + supportedDynamicRanges = setOf(DynamicRange.SDR, DynamicRange.HLG10) + ) + } + ) diff --git a/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt b/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt index 27772f30..ae1a0b02 100644 --- a/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt +++ b/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt @@ -25,6 +25,7 @@ import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import com.google.jetpackcamera.settings.model.DarkMode import com.google.jetpackcamera.settings.model.LensFacing +import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS import java.io.File import junit.framework.TestCase.assertEquals import kotlinx.coroutines.CoroutineScope @@ -32,7 +33,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -46,7 +47,6 @@ internal class CameraAppSettingsViewModelTest { private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext private lateinit var testDataStore: DataStore private lateinit var datastoreScope: CoroutineScope - private lateinit var repository: LocalSettingsRepository private lateinit var settingsViewModel: SettingsViewModel @Before @@ -60,8 +60,11 @@ internal class CameraAppSettingsViewModelTest { ) { testContext.dataStoreFile("test_jca_settings.pb") } - repository = LocalSettingsRepository(testDataStore) - settingsViewModel = SettingsViewModel(repository) + val settingsRepository = LocalSettingsRepository(testDataStore) + val constraintsRepository = SettableConstraintsRepositoryImpl().apply { + updateSystemConstraints(TYPICAL_SYSTEM_CONSTRAINTS) + } + settingsViewModel = SettingsViewModel(settingsRepository, constraintsRepository) advanceUntilIdle() } @@ -77,20 +80,27 @@ internal class CameraAppSettingsViewModelTest { @Test fun getSettingsUiState() = runTest(StandardTestDispatcher()) { - // giving ViewModel time to call init, otherwise settings will stay disabled - delay(100) - val uiState = settingsViewModel.settingsUiState.value - advanceUntilIdle() - assertEquals( - uiState, - SettingsUiState(cameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS, disabled = false) + val uiState = settingsViewModel.settingsUiState.first { + it is SettingsUiState.Enabled + } + + assertThat(uiState).isEqualTo( + SettingsUiState.Enabled( + cameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS, + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS + ) ) } @Test fun setDefaultToFrontCamera() = runTest(StandardTestDispatcher()) { - val initialCameraLensFacing = - settingsViewModel.settingsUiState.value.cameraAppSettings.cameraLensFacing + // Wait for first Enabled state + val initialState = settingsViewModel.settingsUiState.first { + it is SettingsUiState.Enabled + } + + val initialCameraLensFacing = assertIsEnabled(initialState) + .cameraAppSettings.cameraLensFacing val nextCameraLensFacing = if (initialCameraLensFacing == LensFacing.FRONT) { LensFacing.BACK } else { @@ -100,19 +110,37 @@ internal class CameraAppSettingsViewModelTest { advanceUntilIdle() - assertThat(settingsViewModel.settingsUiState.value.cameraAppSettings.cameraLensFacing) - .isEqualTo(nextCameraLensFacing) + assertIsEnabled(settingsViewModel.settingsUiState.value).also { + assertThat(it.cameraAppSettings.cameraLensFacing).isEqualTo(nextCameraLensFacing) + } } @Test fun setDarkMode() = runTest(StandardTestDispatcher()) { - val initialDarkMode = settingsViewModel.settingsUiState.value.cameraAppSettings.darkMode + // Wait for first Enabled state + val initialState = settingsViewModel.settingsUiState.first { + it is SettingsUiState.Enabled + } + + val initialDarkMode = assertIsEnabled(initialState).cameraAppSettings.darkMode + settingsViewModel.setDarkMode(DarkMode.DARK) + advanceUntilIdle() - val newDarkMode = settingsViewModel.settingsUiState.value.cameraAppSettings.darkMode + val newDarkMode = assertIsEnabled(settingsViewModel.settingsUiState.value) + .cameraAppSettings.darkMode assertEquals(initialDarkMode, DarkMode.SYSTEM) assertEquals(DarkMode.DARK, newDarkMode) } } + +private fun assertIsEnabled(settingsUiState: SettingsUiState): SettingsUiState.Enabled { + return when (settingsUiState) { + is SettingsUiState.Enabled -> settingsUiState + else -> throw AssertionError( + "SettingsUiState expected to be Enabled, but was ${settingsUiState::class}" + ) + } +} diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt index 63b39a90..d5ebc0e2 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt @@ -35,6 +35,7 @@ import com.google.jetpackcamera.settings.model.DarkMode import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.Stabilization +import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS import com.google.jetpackcamera.settings.ui.AspectRatioSetting import com.google.jetpackcamera.settings.ui.CaptureModeSetting import com.google.jetpackcamera.settings.ui.DarkModeSetting @@ -96,24 +97,26 @@ private fun SettingsScreen( title = stringResource(id = R.string.settings_title), navBack = onNavigateBack ) - SettingsList( - uiState = uiState, - versionInfo = versionInfo, - setDefaultLensFacing = setDefaultLensFacing, - setFlashMode = setFlashMode, - setTargetFrameRate = setTargetFrameRate, - setAspectRatio = setAspectRatio, - setCaptureMode = setCaptureMode, - setVideoStabilization = setVideoStabilization, - setPreviewStabilization = setPreviewStabilization, - setDarkMode = setDarkMode - ) + if (uiState is SettingsUiState.Enabled) { + SettingsList( + uiState = uiState, + versionInfo = versionInfo, + setDefaultLensFacing = setDefaultLensFacing, + setFlashMode = setFlashMode, + setTargetFrameRate = setTargetFrameRate, + setAspectRatio = setAspectRatio, + setCaptureMode = setCaptureMode, + setVideoStabilization = setVideoStabilization, + setPreviewStabilization = setPreviewStabilization, + setDarkMode = setDarkMode + ) + } } } @Composable fun SettingsList( - uiState: SettingsUiState, + uiState: SettingsUiState.Enabled, versionInfo: VersionInfoHolder, setDefaultLensFacing: (LensFacing) -> Unit = {}, setFlashMode: (FlashMode) -> Unit = {}, @@ -127,7 +130,10 @@ fun SettingsList( SectionHeader(title = stringResource(id = R.string.section_title_camera_settings)) DefaultCameraFacing( - cameraAppSettings = uiState.cameraAppSettings, + settingValue = (uiState.cameraAppSettings.cameraLensFacing == LensFacing.FRONT), + enabled = with(uiState.systemConstraints.availableLenses) { + size > 1 && contains(LensFacing.FRONT) + }, setDefaultLensFacing = setDefaultLensFacing ) @@ -138,7 +144,10 @@ fun SettingsList( TargetFpsSetting( currentTargetFps = uiState.cameraAppSettings.targetFrameRate, - supportedFps = uiState.cameraAppSettings.supportedFixedFrameRates, + supportedFps = uiState.systemConstraints.perLensConstraints.values.fold(emptySet()) { + union, constraints -> + union + constraints.supportedFixedFrameRates + }, setTargetFps = setTargetFrameRate ) @@ -156,7 +165,12 @@ fun SettingsList( currentVideoStabilization = uiState.cameraAppSettings.videoCaptureStabilization, currentPreviewStabilization = uiState.cameraAppSettings.previewStabilization, currentTargetFps = uiState.cameraAppSettings.targetFrameRate, - supportedStabilizationMode = uiState.cameraAppSettings.supportedStabilizationModes, + supportedStabilizationMode = uiState.systemConstraints.perLensConstraints.values.fold( + emptySet() + ) { + union, constraints -> + union + constraints.supportedStabilizationModes + }, setVideoStabilization = setVideoStabilization, setPreviewStabilization = setPreviewStabilization ) @@ -187,7 +201,10 @@ data class VersionInfoHolder( private fun Preview_SettingsScreen() { SettingsPreviewTheme { SettingsScreen( - uiState = SettingsUiState(DEFAULT_CAMERA_APP_SETTINGS), + uiState = SettingsUiState.Enabled( + DEFAULT_CAMERA_APP_SETTINGS, + TYPICAL_SYSTEM_CONSTRAINTS + ), versionInfo = VersionInfoHolder( versionName = "1.0.0", buildType = "release" diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt index 85901e2f..13cf5f04 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt @@ -16,11 +16,15 @@ package com.google.jetpackcamera.settings import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.SystemConstraints /** * Defines the current state of the [SettingsScreen]. */ -data class SettingsUiState( - val cameraAppSettings: CameraAppSettings, - var disabled: Boolean = false -) +sealed interface SettingsUiState { + object Disabled : SettingsUiState + data class Enabled( + val cameraAppSettings: CameraAppSettings, + val systemConstraints: SystemConstraints + ) : SettingsUiState +} diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt index 7df58411..ea8caf7d 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt @@ -20,15 +20,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CaptureMode -import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import com.google.jetpackcamera.settings.model.DarkMode import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.Stabilization import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch private const val TAG = "SettingsViewModel" @@ -38,121 +40,78 @@ private const val TAG = "SettingsViewModel" */ @HiltViewModel class SettingsViewModel @Inject constructor( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, + constraintsRepository: ConstraintsRepository ) : ViewModel() { - private val _settingsUiState: MutableStateFlow = - MutableStateFlow( - SettingsUiState( - DEFAULT_CAMERA_APP_SETTINGS, - disabled = true + val settingsUiState: StateFlow = + combine( + settingsRepository.defaultCameraAppSettings, + constraintsRepository.systemConstraints.filterNotNull() + ) { updatedSettings, constraints -> + SettingsUiState.Enabled( + cameraAppSettings = updatedSettings, + systemConstraints = constraints ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = SettingsUiState.Disabled ) - val settingsUiState: StateFlow = _settingsUiState - - init { - // updates our view model as soon as datastore is updated - viewModelScope.launch { - settingsRepository.cameraAppSettings.collect { updatedSettings -> - _settingsUiState.emit( - settingsUiState.value.copy( - cameraAppSettings = updatedSettings, - disabled = false - ) - ) - - Log.d( - TAG, - "updated setting ${settingsRepository.getCameraAppSettings().captureMode}" - ) - } - } - viewModelScope.launch { - _settingsUiState.emit( - settingsUiState.value.copy( - disabled = false - ) - ) - } - } fun setDefaultLensFacing(lensFacing: LensFacing) { viewModelScope.launch { settingsRepository.updateDefaultLensFacing(lensFacing) - Log.d( - TAG, - "set camera default facing: " + - "${settingsRepository.getCameraAppSettings().cameraLensFacing}" - ) + Log.d(TAG, "set camera default facing: $lensFacing") } } fun setDarkMode(darkMode: DarkMode) { viewModelScope.launch { settingsRepository.updateDarkModeStatus(darkMode) - Log.d( - TAG, - "set dark mode theme: ${settingsRepository.getCameraAppSettings().darkMode}" - ) + Log.d(TAG, "set dark mode theme: $darkMode") } } fun setFlashMode(flashMode: FlashMode) { viewModelScope.launch { settingsRepository.updateFlashModeStatus(flashMode) + Log.d(TAG, "set flash mode: $flashMode") } } fun setTargetFrameRate(targetFrameRate: Int) { viewModelScope.launch { settingsRepository.updateTargetFrameRate(targetFrameRate) + Log.d(TAG, "set target frame rate: $targetFrameRate") } } fun setAspectRatio(aspectRatio: AspectRatio) { viewModelScope.launch { settingsRepository.updateAspectRatio(aspectRatio) - Log.d( - TAG, - "set aspect ratio: " + - "${settingsRepository.getCameraAppSettings().aspectRatio}" - ) + Log.d(TAG, "set aspect ratio: $aspectRatio") } } fun setCaptureMode(captureMode: CaptureMode) { viewModelScope.launch { settingsRepository.updateCaptureMode(captureMode) - - Log.d( - TAG, - "set default capture mode: " + - "${settingsRepository.getCameraAppSettings().captureMode}" - ) + Log.d(TAG, "set default capture mode: $captureMode") } } fun setPreviewStabilization(stabilization: Stabilization) { viewModelScope.launch { settingsRepository.updatePreviewStabilization(stabilization) - - Log.d( - TAG, - "set preview stabilization: " + - "${settingsRepository.getCameraAppSettings().previewStabilization}" - ) + Log.d(TAG, "set preview stabilization: $stabilization") } } fun setVideoStabilization(stabilization: Stabilization) { viewModelScope.launch { settingsRepository.updateVideoStabilization(stabilization) - - Log.d( - TAG, - "set video stabilization: " + - "${settingsRepository.getCameraAppSettings().previewStabilization}" - ) + Log.d(TAG, "set video stabilization: $stabilization") } } } diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt index 2db7571b..559f24f0 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt @@ -54,7 +54,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.google.jetpackcamera.settings.R import com.google.jetpackcamera.settings.model.AspectRatio -import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.DarkMode import com.google.jetpackcamera.settings.model.FlashMode @@ -108,7 +107,8 @@ fun SectionHeader(title: String, modifier: Modifier = Modifier) { @Composable fun DefaultCameraFacing( - cameraAppSettings: CameraAppSettings, + settingValue: Boolean, + enabled: Boolean, setDefaultLensFacing: (LensFacing) -> Unit, modifier: Modifier = Modifier ) { @@ -120,9 +120,8 @@ fun DefaultCameraFacing( onSwitchChanged = { on -> setDefaultLensFacing(if (on) LensFacing.FRONT else LensFacing.BACK) }, - settingValue = cameraAppSettings.cameraLensFacing == LensFacing.FRONT, - enabled = cameraAppSettings.isBackCameraAvailable && - cameraAppSettings.isFrontCameraAvailable + settingValue = settingValue, + enabled = enabled ) } @@ -276,7 +275,7 @@ fun CaptureModeSetting( @Composable fun TargetFpsSetting( currentTargetFps: Int, - supportedFps: List, + supportedFps: Set, setTargetFps: (Int) -> Unit, modifier: Modifier = Modifier ) { @@ -360,7 +359,7 @@ fun StabilizationSetting( currentPreviewStabilization: Stabilization, currentVideoStabilization: Stabilization, currentTargetFps: Int, - supportedStabilizationMode: List, + supportedStabilizationMode: Set, setVideoStabilization: (Stabilization) -> Unit, setPreviewStabilization: (Stabilization) -> Unit, modifier: Modifier = Modifier