diff options
author | Kimberly Crevecoeur <kcrevecoeur@google.com> | 2024-01-31 18:28:15 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-31 18:28:15 -0800 |
commit | aa8801977aa9586d9c7693d09c410201d5a1743e (patch) | |
tree | 77212ff65a35cc1593b5ac140d92b22269f39dfe | |
parent | 75b6e3fb5fd1dfee19db651d5e428a61c4ad446e (diff) | |
download | jetpack-camera-app-aa8801977aa9586d9c7693d09c410201d5a1743e.tar.gz |
Settings to enable preview/video stabilization (#83)
Add stabilization setting datastore and ui
* Popup dialog setting in settings screen; options enabled/disabled based on device capability
* UI icon on preview screen indicating when stabilization is active
20 files changed, 603 insertions, 39 deletions
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 e607a6b..2530a2b 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 @@ -31,6 +31,10 @@ object JcaSettingsSerializer : Serializer<JcaSettings> { .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) .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 fb03ba9..f54a1a9 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 @@ -20,11 +20,15 @@ import com.google.jetpackcamera.settings.AspectRatio as AspectRatioProto import com.google.jetpackcamera.settings.CaptureMode as CaptureModeProto import com.google.jetpackcamera.settings.DarkMode as DarkModeProto import com.google.jetpackcamera.settings.FlashMode as FlashModeProto +import com.google.jetpackcamera.settings.PreviewStabilization as PreviewStabilizationProto +import com.google.jetpackcamera.settings.VideoStabilization as VideoStabilizationProto 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 +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 @@ -55,6 +59,12 @@ class LocalSettingsRepository @Inject constructor( 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 + ), captureMode = when (it.captureModeStatus) { CaptureModeProto.CAPTURE_MODE_SINGLE_STREAM -> CaptureMode.SINGLE_STREAM CaptureModeProto.CAPTURE_MODE_MULTI_STREAM -> CaptureMode.MULTI_STREAM @@ -143,4 +153,60 @@ class LocalSettingsRepository @Inject constructor( .build() } } + + override suspend fun updatePreviewStabilization(stabilization: Stabilization) { + val newStatus = when (stabilization) { + Stabilization.ON -> PreviewStabilizationProto.PREVIEW_STABILIZATION_ON + Stabilization.OFF -> PreviewStabilizationProto.PREVIEW_STABILIZATION_OFF + else -> PreviewStabilizationProto.PREVIEW_STABILIZATION_UNDEFINED + } + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setStabilizePreview(newStatus) + .build() + } + } + + override suspend fun updateVideoStabilization(stabilization: Stabilization) { + val newStatus = when (stabilization) { + Stabilization.ON -> VideoStabilizationProto.VIDEO_STABILIZATION_ON + Stabilization.OFF -> VideoStabilizationProto.VIDEO_STABILIZATION_OFF + else -> VideoStabilizationProto.VIDEO_STABILIZATION_UNDEFINED + } + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setStabilizeVideo(newStatus) + .build() + } + } + + 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() + .setStabilizeVideoSupported(isSupported) + .build() + } + } + + private fun getSupportedStabilization( + previewSupport: Boolean, + videoSupport: Boolean + ): List<SupportedStabilizationMode> { + return buildList { + if (previewSupport && videoSupport) { + add(SupportedStabilizationMode.ON) + } + if (!previewSupport && videoSupport) { + add(SupportedStabilizationMode.HIGH_QUALITY) + } + } + } } 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 637a1ab..0f622d1 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 @@ -20,6 +20,7 @@ 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 +import com.google.jetpackcamera.settings.model.Stabilization import kotlinx.coroutines.flow.Flow /** @@ -42,5 +43,12 @@ interface SettingsRepository { suspend fun updateCaptureMode(captureMode: CaptureMode) + suspend fun updatePreviewStabilization(stabilization: Stabilization) + suspend fun updateVideoStabilization(stabilization: Stabilization) + + suspend fun updateVideoStabilizationSupported(isSupported: Boolean) + + suspend fun updatePreviewStabilizationSupported(isSupported: Boolean) + 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 e16f332..e135089 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 @@ -25,7 +25,10 @@ data class CameraAppSettings( val darkMode: DarkMode = DarkMode.SYSTEM, val flashMode: FlashMode = FlashMode.OFF, val captureMode: CaptureMode = CaptureMode.MULTI_STREAM, - val aspectRatio: AspectRatio = AspectRatio.NINE_SIXTEEN + val aspectRatio: AspectRatio = AspectRatio.NINE_SIXTEEN, + val previewStabilization: Stabilization = Stabilization.UNDEFINED, + val videoCaptureStabilization: Stabilization = Stabilization.UNDEFINED, + val supportedStabilizationModes: List<SupportedStabilizationMode> = emptyList() ) val DEFAULT_CAMERA_APP_SETTINGS = CameraAppSettings() diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Stabilization.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Stabilization.kt new file mode 100644 index 0000000..b0b599e --- /dev/null +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Stabilization.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.settings.model + +import com.google.jetpackcamera.settings.PreviewStabilization as PreviewStabilizationProto +import com.google.jetpackcamera.settings.VideoStabilization as VideoStabilizationProto + +enum class Stabilization { + UNDEFINED, + OFF, + ON; + + companion object { + /** returns the Stabilization enum equivalent of a provided [PreviewStabilizationProto]. */ + fun fromProto(stabilizationProto: PreviewStabilizationProto): Stabilization { + return when (stabilizationProto) { + PreviewStabilizationProto.PREVIEW_STABILIZATION_UNDEFINED -> UNDEFINED + PreviewStabilizationProto.PREVIEW_STABILIZATION_OFF -> OFF + PreviewStabilizationProto.PREVIEW_STABILIZATION_ON -> ON + else -> UNDEFINED + } + } + + /** returns the Stabilization enum equivalent of a provided [VideoStabilizationProto]. */ + + fun fromProto(stabilizationProto: VideoStabilizationProto): Stabilization { + return when (stabilizationProto) { + VideoStabilizationProto.VIDEO_STABILIZATION_UNDEFINED -> UNDEFINED + VideoStabilizationProto.VIDEO_STABILIZATION_OFF -> OFF + VideoStabilizationProto.VIDEO_STABILIZATION_ON -> ON + else -> UNDEFINED + } + } + } +} diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/SupportedStabilizationMode.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/SupportedStabilizationMode.kt new file mode 100644 index 0000000..9cdc8f7 --- /dev/null +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/SupportedStabilizationMode.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.settings.model + +/** Enum class representing the device's supported video stabilization configurations. */ +enum class SupportedStabilizationMode { + /** Device supports Preview stabilization. */ + ON, + + /** Device supports Video stabilization.*/ + HIGH_QUALITY +} 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 2193922..c2033bd 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 @@ -17,8 +17,13 @@ package com.google.jetpackcamera.settings.test import androidx.datastore.core.CorruptionException 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.FlashMode import com.google.jetpackcamera.settings.JcaSettings +import com.google.jetpackcamera.settings.PreviewStabilization +import com.google.jetpackcamera.settings.VideoStabilization import com.google.protobuf.InvalidProtocolBufferException import java.io.IOException import java.io.InputStream @@ -31,6 +36,15 @@ class FakeJcaSettingsSerializer( override val defaultValue: JcaSettings = JcaSettings.newBuilder() .setDarkModeStatus(DarkMode.DARK_MODE_SYSTEM) .setDefaultFrontCamera(false) + .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) .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 fbc40f5..2bb4295 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 @@ -22,11 +22,15 @@ 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.Stabilization +import com.google.jetpackcamera.settings.model.SupportedStabilizationMode import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow object FakeSettingsRepository : SettingsRepository { var currentCameraSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS + private var isPreviewStabilizationSupported: Boolean = false + private var isVideoStabilizationSupported: Boolean = false override val cameraAppSettings: Flow<CameraAppSettings> = flow { emit(currentCameraSettings) } @@ -35,8 +39,8 @@ object FakeSettingsRepository : SettingsRepository { currentCameraSettings = currentCameraSettings.copy(isFrontCameraFacing = newLensFacing) } - override suspend fun updateDarkModeStatus(darkmode: DarkMode) { - currentCameraSettings = currentCameraSettings.copy(darkMode = darkmode) + override suspend fun updateDarkModeStatus(darkMode: DarkMode) { + currentCameraSettings = currentCameraSettings.copy(darkMode = darkMode) } override suspend fun updateFlashModeStatus(flashMode: FlashMode) { @@ -62,7 +66,43 @@ object FakeSettingsRepository : SettingsRepository { currentCameraSettings.copy(captureMode = captureMode) } + override suspend fun updatePreviewStabilization(stabilization: Stabilization) { + currentCameraSettings = + currentCameraSettings.copy(previewStabilization = stabilization) + } + + override suspend fun updateVideoStabilization(stabilization: Stabilization) { + currentCameraSettings = + 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 updateAspectRatio(aspectRatio: AspectRatio) { - TODO("Not yet implemented") + currentCameraSettings = + currentCameraSettings.copy(aspectRatio = aspectRatio) } } 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 7e27529..288d501 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 @@ -20,6 +20,10 @@ import "com/google/jetpackcamera/settings/aspect_ratio.proto"; import "com/google/jetpackcamera/settings/capture_mode.proto"; import "com/google/jetpackcamera/settings/dark_mode.proto"; import "com/google/jetpackcamera/settings/flash_mode.proto"; +import "com/google/jetpackcamera/settings/preview_stabilization.proto"; +import "com/google/jetpackcamera/settings/video_stabilization.proto"; + + option java_package = "com.google.jetpackcamera.settings"; @@ -33,4 +37,8 @@ message JcaSettings { 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; }
\ No newline at end of file diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/preview_stabilization.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/preview_stabilization.proto new file mode 100644 index 0000000..f0cf902 --- /dev/null +++ b/data/settings/src/main/proto/com/google/jetpackcamera/settings/preview_stabilization.proto @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +option java_package = "com.google.jetpackcamera.settings"; +option java_multiple_files = true; + +enum PreviewStabilization { + PREVIEW_STABILIZATION_UNDEFINED = 0; + PREVIEW_STABILIZATION_OFF = 1; + PREVIEW_STABILIZATION_ON = 2; +}
\ No newline at end of file diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/video_stabilization.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/video_stabilization.proto new file mode 100644 index 0000000..66e1a8b --- /dev/null +++ b/data/settings/src/main/proto/com/google/jetpackcamera/settings/video_stabilization.proto @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +option java_package = "com.google.jetpackcamera.settings"; +option java_multiple_files = true; + +enum VideoStabilization { + VIDEO_STABILIZATION_UNDEFINED = 0; + VIDEO_STABILIZATION_OFF = 1; + VIDEO_STABILIZATION_ON = 2; +}
\ 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 3187a2b..aa0f372 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 @@ -51,6 +51,8 @@ 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.FlashMode +import com.google.jetpackcamera.settings.model.Stabilization +import com.google.jetpackcamera.settings.model.SupportedStabilizationMode import dagger.hilt.android.scopes.ViewModelScoped import java.io.FileNotFoundException import java.lang.RuntimeException @@ -91,7 +93,7 @@ constructor( private val recorder = Recorder.Builder().setExecutor( defaultDispatcher.asExecutor() ).build() - private val videoCaptureUseCase = VideoCapture.withOutput(recorder) + private lateinit var videoCaptureUseCase: VideoCapture<Recorder> private var recording: Recording? = null private lateinit var previewUseCase: Preview @@ -99,7 +101,10 @@ constructor( private lateinit var aspectRatio: AspectRatio private lateinit var captureMode: CaptureMode + private lateinit var stabilizePreviewMode: Stabilization + private lateinit var stabilizeVideoMode: Stabilization private lateinit var surfaceProvider: Preview.SurfaceProvider + private lateinit var supportedStabilizationModes: List<SupportedStabilizationMode> private var isFrontFacing = true private val screenFlashEvents: MutableSharedFlow<CameraUseCase.ScreenFlashEvent> = @@ -108,10 +113,11 @@ constructor( override suspend fun initialize(currentCameraSettings: CameraAppSettings): List<Int> { this.aspectRatio = currentCameraSettings.aspectRatio this.captureMode = currentCameraSettings.captureMode + this.stabilizePreviewMode = currentCameraSettings.previewStabilization + this.stabilizeVideoMode = currentCameraSettings.videoCaptureStabilization + this.supportedStabilizationModes = currentCameraSettings.supportedStabilizationModes setFlashMode(currentCameraSettings.flashMode, currentCameraSettings.isFrontCameraFacing) - cameraProvider = ProcessCameraProvider.getInstance(application).await() - updateUseCaseGroup() val availableCameraLens = listOf( @@ -120,15 +126,16 @@ constructor( ).filter { lensFacing -> cameraProvider.hasCamera(cameraLensToSelector(lensFacing)) } - // updates values for available camera lens if necessary coroutineScope { settingsRepository.updateAvailableCameraLens( availableCameraLens.contains(CameraSelector.LENS_FACING_FRONT), availableCameraLens.contains(CameraSelector.LENS_FACING_BACK) ) + settingsRepository.updateVideoStabilizationSupported(isStabilizationSupported()) } - + videoCaptureUseCase = createVideoUseCase() + updateUseCaseGroup() return availableCameraLens } @@ -410,25 +417,78 @@ constructor( useCaseGroup = useCaseGroupBuilder.build() } - private fun createPreviewUseCase(): Preview { + /** + * Checks if video stabilization is supported by the device. + * + */ + private fun isStabilizationSupported(): Boolean { val availableCameraInfo = cameraProvider.availableCameraInfos val cameraSelector = if (isFrontFacing) { CameraSelector.DEFAULT_FRONT_CAMERA } else { CameraSelector.DEFAULT_BACK_CAMERA } - val isPreviewStabilizationSupported = + val isVideoStabilizationSupported = cameraSelector.filter(availableCameraInfo).firstOrNull()?.let { - Preview.getPreviewCapabilities(it).isStabilizationSupported + Recorder.getVideoCapabilities(it).isStabilizationSupported } ?: false + return isVideoStabilizationSupported + } + + private fun createVideoUseCase(): VideoCapture<Recorder> { + val videoCaptureBuilder = VideoCapture.Builder(recorder) + + // set video stabilization + + if (shouldVideoBeStabilized()) { + val isStabilized = when (stabilizeVideoMode) { + Stabilization.ON -> true + Stabilization.OFF, Stabilization.UNDEFINED -> false + } + videoCaptureBuilder.setVideoStabilizationEnabled(isStabilized) + } + return videoCaptureBuilder.build() + } + + private fun shouldVideoBeStabilized(): Boolean { + // video is supported by the device AND + // video is on OR preview is on + return (supportedStabilizationModes.contains(SupportedStabilizationMode.HIGH_QUALITY)) && + ( + // high quality (video only) selected + ( + stabilizeVideoMode == Stabilization.ON && + stabilizePreviewMode == Stabilization.UNDEFINED + ) || + // or on is selected + ( + stabilizePreviewMode == Stabilization.ON && + stabilizeVideoMode != Stabilization.OFF + ) + ) + } + + private fun createPreviewUseCase(): Preview { val previewUseCaseBuilder = Preview.Builder() - if (isPreviewStabilizationSupported) { - previewUseCaseBuilder.setPreviewStabilizationEnabled(true) + // set preview stabilization + if (shouldPreviewBeStabilized()) { + val isStabilized = when (stabilizePreviewMode) { + Stabilization.ON -> true + else -> false + } + previewUseCaseBuilder.setPreviewStabilizationEnabled(isStabilized) } return previewUseCaseBuilder.build() } + private fun shouldPreviewBeStabilized(): Boolean { + return ( + supportedStabilizationModes.contains(SupportedStabilizationMode.ON) && + stabilizePreviewMode == Stabilization.ON + ) + } + // converts LensFacing from datastore to @LensFacing Int value private fun getLensFacing(isFrontFacing: Boolean): Int = when (isFrontFacing) { true -> CameraSelector.LENS_FACING_FRONT 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 d8d90b2..c5cfdd2 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 @@ -64,6 +64,7 @@ import com.google.jetpackcamera.feature.preview.ui.PreviewDisplay import com.google.jetpackcamera.feature.preview.ui.ScreenFlashScreen import com.google.jetpackcamera.feature.preview.ui.SettingsNavButton import com.google.jetpackcamera.feature.preview.ui.ShowTestableToast +import com.google.jetpackcamera.feature.preview.ui.StabilizationIcon import com.google.jetpackcamera.feature.preview.ui.TestingButton import com.google.jetpackcamera.feature.preview.ui.ZoomScaleText import com.google.jetpackcamera.feature.quicksettings.QuickSettingsScreenOverlay @@ -206,7 +207,7 @@ fun PreviewScreen( modifier = Modifier .weight(1f) .fillMaxHeight(), - horizontalArrangement = Arrangement.Center, + horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { TestingButton( @@ -223,6 +224,14 @@ fun PreviewScreen( } ) ) + StabilizationIcon( + supportedStabilizationMode = previewUiState + .currentCameraSettings.supportedStabilizationModes, + videoStabilization = previewUiState + .currentCameraSettings.videoCaptureStabilization, + previewStabilization = previewUiState + .currentCameraSettings.previewStabilization + ) } } } 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 5c7b5ab..01f09c8 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 @@ -45,7 +45,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -55,12 +54,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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 com.google.jetpackcamera.viewfinder.CameraPreview import kotlinx.coroutines.CompletableDeferred @@ -182,6 +184,29 @@ fun PreviewDisplay( } } +@Composable +fun StabilizationIcon( + supportedStabilizationMode: List<SupportedStabilizationMode>, + videoStabilization: Stabilization, + previewStabilization: Stabilization +) { + if (supportedStabilizationMode.isNotEmpty() && + (videoStabilization == Stabilization.ON || previewStabilization == Stabilization.ON) + ) { + val descriptionText = if (videoStabilization == Stabilization.ON) { + stringResource(id = R.string.stabilization_icon_description_preview_and_video) + } else { + // previewStabilization will not be on for high quality + stringResource(id = R.string.stabilization_icon_description_video_only) + } + Icon( + painter = painterResource(id = R.drawable.baseline_video_stable_24), + contentDescription = descriptionText, + tint = Color.White + ) + } +} + /** * A temporary button that can be added to preview for quick testing purposes */ diff --git a/feature/preview/src/main/res/drawable/baseline_video_stable_24.xml b/feature/preview/src/main/res/drawable/baseline_video_stable_24.xml new file mode 100644 index 0000000..54f9651 --- /dev/null +++ b/feature/preview/src/main/res/drawable/baseline_video_stable_24.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M20,4H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.1,4 20,4zM4,18V6h2.95l-2.33,8.73L16.82,18H4zM20,18h-2.95l2.34,-8.73L7.18,6H20V18z"/> + +</vector> diff --git a/feature/preview/src/main/res/values/strings.xml b/feature/preview/src/main/res/values/strings.xml index b16f190..b2713f5 100644 --- a/feature/preview/src/main/res/values/strings.xml +++ b/feature/preview/src/main/res/values/strings.xml @@ -23,4 +23,7 @@ <string name="toast_image_capture_success">Image Capture Success</string> <string name="toast_capture_failure">Image Capture Failure</string> + <string name="stabilization_icon_description_preview_and_video">Preview is Stabilized</string> + <string name="stabilization_icon_description_video_only">Only Video is Stabilized</string> + </resources> 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 c28a558..fd8b32c 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 @@ -31,6 +31,7 @@ import com.google.jetpackcamera.settings.ui.DefaultCameraFacing import com.google.jetpackcamera.settings.ui.FlashModeSetting import com.google.jetpackcamera.settings.ui.SectionHeader import com.google.jetpackcamera.settings.ui.SettingsPageHeader +import com.google.jetpackcamera.settings.ui.StabilizationSetting /** * Screen used for the Settings feature. @@ -78,6 +79,16 @@ fun SettingsList(uiState: SettingsUiState, viewModel: SettingsViewModel) { setCaptureMode = viewModel::setCaptureMode ) + // todo: b/313647247 - query device and disable setting if preview stabilization isn't supported. + // todo: b/313647809 - query device and disable setting if video stabilization isn't supported. + StabilizationSetting( + currentVideoStabilization = uiState.cameraAppSettings.videoCaptureStabilization, + currentPreviewStabilization = uiState.cameraAppSettings.previewStabilization, + supportedStabilizationMode = uiState.cameraAppSettings.supportedStabilizationModes, + setVideoStabilization = viewModel::setVideoStabilization, + setPreviewStabilization = viewModel::setPreviewStabilization + ) + SectionHeader(title = stringResource(id = R.string.section_title_app_settings)) DarkModeSetting( 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 c0005db..ca55517 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 @@ -23,6 +23,7 @@ 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.Stabilization import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -61,8 +62,7 @@ class SettingsViewModel @Inject constructor( Log.d( TAG, - "updated setting" + - settingsRepository.getCameraAppSettings().captureMode + "updated setting ${settingsRepository.getCameraAppSettings().captureMode}" ) } } @@ -82,7 +82,7 @@ class SettingsViewModel @Inject constructor( Log.d( TAG, "set camera default facing: " + - settingsRepository.getCameraAppSettings().isFrontCameraFacing + "${settingsRepository.getCameraAppSettings().isFrontCameraFacing}" ) } } @@ -92,8 +92,7 @@ class SettingsViewModel @Inject constructor( settingsRepository.updateDarkModeStatus(darkMode) Log.d( TAG, - "set dark mode theme: " + - settingsRepository.getCameraAppSettings().darkMode + "set dark mode theme: ${settingsRepository.getCameraAppSettings().darkMode}" ) } } @@ -109,7 +108,7 @@ class SettingsViewModel @Inject constructor( settingsRepository.updateAspectRatio(aspectRatio) Log.d( TAG, - "set aspect ratio " + + "set aspect ratio: " + "${settingsRepository.getCameraAppSettings().aspectRatio}" ) } @@ -121,8 +120,32 @@ class SettingsViewModel @Inject constructor( Log.d( TAG, - "set default capture mode " + - settingsRepository.getCameraAppSettings().captureMode + "set default capture mode: " + + "${settingsRepository.getCameraAppSettings().captureMode}" + ) + } + } + + fun setPreviewStabilization(stabilization: Stabilization) { + viewModelScope.launch { + settingsRepository.updatePreviewStabilization(stabilization) + + Log.d( + TAG, + "set preview stabilization: " + + "${settingsRepository.getCameraAppSettings().previewStabilization}" + ) + } + } + + fun setVideoStabilization(stabilization: Stabilization) { + viewModelScope.launch { + settingsRepository.updateVideoStabilization(stabilization) + + Log.d( + TAG, + "set video stabilization: " + + "${settingsRepository.getCameraAppSettings().previewStabilization}" ) } } 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 c38dd58..e970216 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 @@ -20,8 +20,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.toggleable @@ -32,6 +32,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Switch @@ -52,6 +53,8 @@ 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 +import com.google.jetpackcamera.settings.model.Stabilization +import com.google.jetpackcamera.settings.model.SupportedStabilizationMode /** * MAJOR SETTING UI COMPONENTS @@ -211,7 +214,6 @@ fun AspectRatioSetting(currentAspectRatio: AspectRatio, setAspectRatio: (AspectR @Composable fun CaptureModeSetting(currentCaptureMode: CaptureMode, setCaptureMode: (CaptureMode) -> Unit) { - // todo: string resources BasicPopupSetting( title = stringResource(R.string.capture_mode_title), leadingIcon = null, @@ -219,6 +221,7 @@ fun CaptureModeSetting(currentCaptureMode: CaptureMode, setCaptureMode: (Capture CaptureMode.MULTI_STREAM -> stringResource( id = R.string.capture_mode_description_multi_stream ) + CaptureMode.SINGLE_STREAM -> stringResource( id = R.string.capture_mode_description_single_stream ) @@ -241,6 +244,110 @@ fun CaptureModeSetting(currentCaptureMode: CaptureMode, setCaptureMode: (Capture } /** + * Returns the description text depending on the preview/video stabilization configuration. + * On - preview is on and video is NOT off. + * High Quality - preview is unspecified and video is ON. + * Off - Every other configuration. + */ +private fun getStabilizationStringRes( + previewStabilization: Stabilization, + videoStabilization: Stabilization +): Int { + return if (previewStabilization == Stabilization.ON && + videoStabilization != Stabilization.OFF + ) { + R.string.stabilization_description_on + } else if (previewStabilization == Stabilization.UNDEFINED && + videoStabilization == Stabilization.ON + ) { + R.string.stabilization_description_high_quality + } else { + R.string.stabilization_description_off + } +} + +/** + * A Setting to set preview and video stabilization. + * + * ON - Both preview and video are stabilized. + * HIGH_QUALITY - Video will be stabilized, preview might be stabilized, depending on the device. + * OFF - Preview and video stabilization is disabled. + * + * @param supportedStabilizationMode the enabled condition for this setting. + */ +@Composable +fun StabilizationSetting( + currentPreviewStabilization: Stabilization, + currentVideoStabilization: Stabilization, + supportedStabilizationMode: List<SupportedStabilizationMode>, + setVideoStabilization: (Stabilization) -> Unit, + setPreviewStabilization: (Stabilization) -> Unit +) { + BasicPopupSetting( + title = stringResource(R.string.video_stabilization_title), + leadingIcon = null, + enabled = supportedStabilizationMode.isNotEmpty(), + description = if (supportedStabilizationMode.isEmpty()) { + stringResource(id = R.string.stabilization_description_unsupported) + } else { + stringResource( + id = getStabilizationStringRes( + previewStabilization = currentPreviewStabilization, + videoStabilization = currentVideoStabilization + ) + ) + }, + popupContents = { + Column(Modifier.selectableGroup()) { + Spacer(modifier = Modifier.height(10.dp)) + + // on selector + SingleChoiceSelector( + text = stringResource(id = R.string.stabilization_selector_on), + secondaryText = stringResource(id = R.string.stabilization_selector_on_info), + enabled = supportedStabilizationMode.contains(SupportedStabilizationMode.ON), + selected = (currentPreviewStabilization == Stabilization.ON) && + (currentVideoStabilization != Stabilization.OFF), + onClick = { + setVideoStabilization(Stabilization.UNDEFINED) + setPreviewStabilization(Stabilization.ON) + } + ) + + // high quality selector + SingleChoiceSelector( + text = stringResource(id = R.string.stabilization_selector_high_quality), + secondaryText = stringResource( + id = R.string.stabilization_selector_high_quality_info + ), + enabled = supportedStabilizationMode.contains( + SupportedStabilizationMode.HIGH_QUALITY + ), + + selected = (currentPreviewStabilization == Stabilization.UNDEFINED) && + (currentVideoStabilization == Stabilization.ON), + onClick = { + setVideoStabilization(Stabilization.ON) + setPreviewStabilization(Stabilization.UNDEFINED) + } + ) + + // off selector + SingleChoiceSelector( + text = stringResource(id = R.string.stabilization_selector_off), + selected = (currentPreviewStabilization != Stabilization.ON) && + (currentVideoStabilization != Stabilization.ON), + onClick = { + setVideoStabilization(Stabilization.OFF) + setPreviewStabilization(Stabilization.OFF) + } + ) + } + } + ) +} + +/* * Setting UI sub-Components * small and whimsical :) * don't use these directly, use them to build the ready-to-use setting components @@ -261,6 +368,7 @@ fun BasicPopupSetting( SettingUI( modifier = modifier.clickable(enabled = enabled) { popupStatus.value = true }, title = title, + enabled = enabled, description = description, leadingIcon = leadingIcon, trailingContent = null @@ -303,6 +411,7 @@ fun SwitchSettingUI( value = settingValue, onValueChange = { _ -> onClick() } ), + enabled = enabled, title = title, description = description, leadingIcon = leadingIcon, @@ -325,17 +434,30 @@ fun SwitchSettingUI( fun SettingUI( modifier: Modifier = Modifier, title: String, + enabled: Boolean = true, description: String? = null, leadingIcon: @Composable (() -> Unit)?, trailingContent: @Composable (() -> Unit)? ) { ListItem( modifier = modifier, - headlineContent = { Text(title) }, - supportingContent = when (description) { - null -> null - else -> { - { Text(description) } + headlineContent = { + when (enabled) { + true -> Text(title) + false -> { + Text(text = title, color = LocalContentColor.current.copy(alpha = .7f)) + } + } + }, + supportingContent = { + if (description != null) { + when (enabled) { + true -> Text(description) + false -> Text( + text = description, + color = LocalContentColor.current.copy(alpha = .7f) + ) + } } }, leadingContent = leadingIcon, @@ -350,6 +472,7 @@ fun SettingUI( fun SingleChoiceSelector( modifier: Modifier = Modifier, text: String, + secondaryText: String? = null, selected: Boolean, onClick: () -> Unit, enabled: Boolean = true @@ -360,16 +483,23 @@ fun SingleChoiceSelector( .selectable( selected = selected, role = Role.RadioButton, - onClick = onClick + onClick = onClick, + enabled = enabled ), verticalAlignment = Alignment.CenterVertically ) { - RadioButton( - selected = selected, - onClick = onClick, - enabled = enabled + SettingUI( + title = text, + description = secondaryText, + enabled = enabled, + leadingIcon = { + RadioButton( + selected = selected, + onClick = onClick, + enabled = enabled + ) + }, + trailingContent = null ) - Spacer(Modifier.width(8.dp)) - Text(text) } } diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index 3df861f..44a0bbc 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -59,7 +59,7 @@ <string name="capture_mode_description_multi_stream">Multi Stream</string> <string name="capture_mode_description_single_stream">Single Stream</string> - <!-- Aspect Ratio setting strings --> + <!-- Aspect Ratio setting strings --> <string name="aspect_ratio_title">Set Aspect Ratio</string> <string name="aspect_ratio_selector_9_16">9:16</string> @@ -70,4 +70,18 @@ <string name="aspect_ratio_description_3_4">Aspect Ratio is 3:4</string> <string name="aspect_ratio_description_1_1">Aspect Ratio is 1:1</string> + <!-- Stabilization setting strings --> + <string name="video_stabilization_title">Set Video Stabilization</string> + + <string name="stabilization_selector_on">On</string> + <string name="stabilization_selector_high_quality">High Quality</string> + <string name="stabilization_selector_off">Off</string> + + <string name="stabilization_selector_on_info">Both preview and video streams will be stabilized</string> + <string name="stabilization_selector_high_quality_info">Video stream will be stabilized, but preview might not be. This mode ensures highest-quality video stream.</string> + + <string name="stabilization_description_on">Stabilization On</string> + <string name="stabilization_description_high_quality">Stabilization High Quality</string> + <string name="stabilization_description_off">Stabilization Off</string> + <string name="stabilization_description_unsupported">Stabilization unsupported</string> </resources>
\ No newline at end of file |