1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.jetpackcamera.settings 17 18 import android.util.Log 19 import androidx.lifecycle.ViewModel 20 import androidx.lifecycle.viewModelScope 21 import com.google.jetpackcamera.settings.DisabledRationale.DeviceUnsupportedRationale 22 import com.google.jetpackcamera.settings.DisabledRationale.FpsUnsupportedRationale 23 import com.google.jetpackcamera.settings.DisabledRationale.StabilizationUnsupportedRationale 24 import com.google.jetpackcamera.settings.model.AspectRatio 25 import com.google.jetpackcamera.settings.model.CameraAppSettings 26 import com.google.jetpackcamera.settings.model.CaptureMode 27 import com.google.jetpackcamera.settings.model.DarkMode 28 import com.google.jetpackcamera.settings.model.FlashMode 29 import com.google.jetpackcamera.settings.model.LensFacing 30 import com.google.jetpackcamera.settings.model.Stabilization 31 import com.google.jetpackcamera.settings.model.SupportedStabilizationMode 32 import com.google.jetpackcamera.settings.model.SystemConstraints 33 import com.google.jetpackcamera.settings.ui.FPS_15 34 import com.google.jetpackcamera.settings.ui.FPS_30 35 import com.google.jetpackcamera.settings.ui.FPS_60 36 import com.google.jetpackcamera.settings.ui.FPS_AUTO 37 import dagger.hilt.android.lifecycle.HiltViewModel 38 import javax.inject.Inject 39 import kotlinx.coroutines.flow.SharingStarted 40 import kotlinx.coroutines.flow.StateFlow 41 import kotlinx.coroutines.flow.combine 42 import kotlinx.coroutines.flow.filterNotNull 43 import kotlinx.coroutines.flow.stateIn 44 import kotlinx.coroutines.launch 45 46 private const val TAG = "SettingsViewModel" 47 private val fpsOptions = setOf(FPS_15, FPS_30, FPS_60) 48 49 /** 50 * [ViewModel] for [SettingsScreen]. 51 */ 52 @HiltViewModel 53 class SettingsViewModel @Inject constructor( 54 private val settingsRepository: SettingsRepository, 55 constraintsRepository: ConstraintsRepository 56 ) : ViewModel() { 57 58 val settingsUiState: StateFlow<SettingsUiState> = 59 combine( 60 settingsRepository.defaultCameraAppSettings, 61 constraintsRepository.systemConstraints.filterNotNull() 62 ) { updatedSettings, constraints -> 63 SettingsUiState.Enabled( 64 aspectRatioUiState = AspectRatioUiState.Enabled(updatedSettings.aspectRatio), 65 captureModeUiState = CaptureModeUiState.Enabled(updatedSettings.captureMode), 66 darkModeUiState = DarkModeUiState.Enabled(updatedSettings.darkMode), 67 flashUiState = FlashUiState.Enabled(updatedSettings.flashMode), 68 fpsUiState = getFpsUiState(constraints, updatedSettings), 69 lensFlipUiState = getLensFlipUiState(constraints, updatedSettings), 70 stabilizationUiState = getStabilizationUiState(constraints, updatedSettings) 71 72 ) 73 }.stateIn( 74 scope = viewModelScope, 75 started = SharingStarted.WhileSubscribed(5_000), 76 initialValue = SettingsUiState.Disabled 77 ) 78 79 private fun getStabilizationUiState( 80 systemConstraints: SystemConstraints, 81 cameraAppSettings: CameraAppSettings 82 ): StabilizationUiState { 83 val deviceStabilizations: Set<SupportedStabilizationMode> = 84 systemConstraints 85 .perLensConstraints[cameraAppSettings.cameraLensFacing] 86 ?.supportedStabilizationModes 87 ?: emptySet() 88 89 // if no lens supports 90 if (deviceStabilizations.isEmpty()) { 91 return StabilizationUiState.Disabled( 92 DeviceUnsupportedRationale( 93 R.string.stabilization_rationale_prefix 94 ) 95 ) 96 } 97 98 // if a lens supports but it isn't the current 99 if (systemConstraints.perLensConstraints[cameraAppSettings.cameraLensFacing] 100 ?.supportedStabilizationModes?.isEmpty() == true 101 ) { 102 return StabilizationUiState.Disabled( 103 getLensUnsupportedRationale( 104 cameraAppSettings.cameraLensFacing, 105 R.string.stabilization_rationale_prefix 106 ) 107 ) 108 } 109 110 // if fps is too high for any stabilization 111 if (cameraAppSettings.targetFrameRate >= TARGET_FPS_60) { 112 return StabilizationUiState.Disabled( 113 FpsUnsupportedRationale( 114 R.string.stabilization_rationale_prefix, 115 FPS_60 116 ) 117 ) 118 } 119 120 return StabilizationUiState.Enabled( 121 currentPreviewStabilization = cameraAppSettings.previewStabilization, 122 currentVideoStabilization = cameraAppSettings.videoCaptureStabilization, 123 stabilizationOnState = getPreviewStabilizationState( 124 currentFrameRate = cameraAppSettings.targetFrameRate, 125 defaultLensFacing = cameraAppSettings.cameraLensFacing, 126 deviceStabilizations = deviceStabilizations, 127 currentLensStabilizations = systemConstraints 128 .perLensConstraints[cameraAppSettings.cameraLensFacing] 129 ?.supportedStabilizationModes 130 ), 131 stabilizationHighQualityState = 132 getVideoStabilizationState( 133 currentFrameRate = cameraAppSettings.targetFrameRate, 134 deviceStabilizations = deviceStabilizations, 135 defaultLensFacing = cameraAppSettings.cameraLensFacing, 136 currentLensStabilizations = systemConstraints 137 .perLensConstraints[cameraAppSettings.cameraLensFacing] 138 ?.supportedStabilizationModes 139 ) 140 ) 141 } 142 143 private fun getPreviewStabilizationState( 144 currentFrameRate: Int, 145 defaultLensFacing: LensFacing, 146 deviceStabilizations: Set<SupportedStabilizationMode>, 147 currentLensStabilizations: Set<SupportedStabilizationMode>? 148 ): SingleSelectableState { 149 // if unsupported by device 150 if (!deviceStabilizations.contains(SupportedStabilizationMode.ON)) { 151 return SingleSelectableState.Disabled( 152 disabledRationale = 153 DeviceUnsupportedRationale(R.string.stabilization_rationale_prefix) 154 ) 155 } 156 157 // if unsupported by by current lens 158 if (currentLensStabilizations?.contains(SupportedStabilizationMode.ON) == false) { 159 return SingleSelectableState.Disabled( 160 getLensUnsupportedRationale( 161 defaultLensFacing, 162 R.string.stabilization_rationale_prefix 163 ) 164 ) 165 } 166 167 // if fps is unsupported by preview stabilization 168 if (currentFrameRate == TARGET_FPS_60 || currentFrameRate == TARGET_FPS_15) { 169 return SingleSelectableState.Disabled( 170 FpsUnsupportedRationale( 171 R.string.stabilization_rationale_prefix, 172 currentFrameRate 173 ) 174 ) 175 } 176 177 return SingleSelectableState.Selectable 178 } 179 180 private fun getVideoStabilizationState( 181 currentFrameRate: Int, 182 defaultLensFacing: LensFacing, 183 deviceStabilizations: Set<SupportedStabilizationMode>, 184 currentLensStabilizations: Set<SupportedStabilizationMode>? 185 ): SingleSelectableState { 186 // if unsupported by device 187 if (!deviceStabilizations.contains(SupportedStabilizationMode.ON)) { 188 return SingleSelectableState.Disabled( 189 disabledRationale = 190 DeviceUnsupportedRationale(R.string.stabilization_rationale_prefix) 191 ) 192 } 193 194 // if unsupported by by current lens 195 if (currentLensStabilizations?.contains(SupportedStabilizationMode.HIGH_QUALITY) == false) { 196 return SingleSelectableState.Disabled( 197 getLensUnsupportedRationale( 198 defaultLensFacing, 199 R.string.stabilization_rationale_prefix 200 ) 201 ) 202 } 203 // if fps is unsupported by preview stabilization 204 if (currentFrameRate == TARGET_FPS_60) { 205 return SingleSelectableState.Disabled( 206 FpsUnsupportedRationale( 207 R.string.stabilization_rationale_prefix, 208 currentFrameRate 209 ) 210 ) 211 } 212 213 return SingleSelectableState.Selectable 214 } 215 216 /** 217 * Enables or disables default camera switch based on: 218 * - number of cameras available 219 * - if there is a front and rear camera, the camera that the setting would switch to must also 220 * support the other settings 221 * */ 222 private fun getLensFlipUiState( 223 systemConstraints: SystemConstraints, 224 currentSettings: CameraAppSettings 225 ): FlipLensUiState { 226 // if there is only one lens, stop here 227 if (!with(systemConstraints.availableLenses) { 228 size > 1 && contains(com.google.jetpackcamera.settings.model.LensFacing.FRONT) 229 } 230 ) { 231 return FlipLensUiState.Disabled( 232 currentLensFacing = currentSettings.cameraLensFacing, 233 disabledRationale = 234 DeviceUnsupportedRationale( 235 // display the lens that isnt supported 236 when (currentSettings.cameraLensFacing) { 237 LensFacing.BACK -> R.string.front_lens_rationale_prefix 238 LensFacing.FRONT -> R.string.rear_lens_rationale_prefix 239 } 240 ) 241 ) 242 } 243 244 // If multiple lens available, continue 245 val newLensFacing = if (currentSettings.cameraLensFacing == LensFacing.FRONT) { 246 LensFacing.BACK 247 } else { 248 LensFacing.FRONT 249 } 250 val newLensConstraints = systemConstraints.perLensConstraints[newLensFacing]!! 251 // make sure all current settings wont break constraint when changing new default lens 252 253 // if new lens won't support current fps 254 if (currentSettings.targetFrameRate != FPS_AUTO && 255 !newLensConstraints.supportedFixedFrameRates 256 .contains(currentSettings.targetFrameRate) 257 ) { 258 return FlipLensUiState.Disabled( 259 currentLensFacing = currentSettings.cameraLensFacing, 260 disabledRationale = FpsUnsupportedRationale( 261 when (currentSettings.cameraLensFacing) { 262 LensFacing.BACK -> R.string.front_lens_rationale_prefix 263 LensFacing.FRONT -> R.string.rear_lens_rationale_prefix 264 }, 265 currentSettings.targetFrameRate 266 ) 267 ) 268 } 269 270 // if preview stabilization is currently on and the other lens won't support it 271 if (currentSettings.previewStabilization == Stabilization.ON) { 272 if (!newLensConstraints.supportedStabilizationModes.contains( 273 SupportedStabilizationMode.ON 274 ) 275 ) { 276 return FlipLensUiState.Disabled( 277 currentLensFacing = currentSettings.cameraLensFacing, 278 disabledRationale = StabilizationUnsupportedRationale( 279 when (currentSettings.cameraLensFacing) { 280 LensFacing.BACK -> R.string.front_lens_rationale_prefix 281 LensFacing.FRONT -> R.string.rear_lens_rationale_prefix 282 } 283 ) 284 ) 285 } 286 } 287 // if video stabilization is currently on and the other lens won't support it 288 if (currentSettings.videoCaptureStabilization == Stabilization.ON) { 289 if (!newLensConstraints.supportedStabilizationModes 290 .contains(SupportedStabilizationMode.HIGH_QUALITY) 291 ) { 292 return FlipLensUiState.Disabled( 293 currentLensFacing = currentSettings.cameraLensFacing, 294 disabledRationale = StabilizationUnsupportedRationale( 295 when (currentSettings.cameraLensFacing) { 296 LensFacing.BACK -> R.string.front_lens_rationale_prefix 297 LensFacing.FRONT -> R.string.rear_lens_rationale_prefix 298 } 299 ) 300 ) 301 } 302 } 303 304 return FlipLensUiState.Enabled(currentLensFacing = currentSettings.cameraLensFacing) 305 } 306 307 private fun getFpsUiState( 308 systemConstraints: SystemConstraints, 309 cameraAppSettings: CameraAppSettings 310 ): FpsUiState { 311 val optionConstraintRationale: MutableMap<Int, SingleSelectableState> = mutableMapOf() 312 313 val currentLensFrameRates: Set<Int> = systemConstraints 314 .perLensConstraints[cameraAppSettings.cameraLensFacing] 315 ?.supportedFixedFrameRates ?: emptySet() 316 317 // if device supports no fixed frame rates, disable 318 if (currentLensFrameRates.isEmpty()) { 319 return FpsUiState.Disabled( 320 DeviceUnsupportedRationale(R.string.no_fixed_fps_rationale_prefix) 321 ) 322 } 323 324 // provide selectable states for each of the fps options 325 fpsOptions.forEach { fpsOption -> 326 val fpsUiState = isFpsOptionEnabled( 327 fpsOption, 328 cameraAppSettings.cameraLensFacing, 329 currentLensFrameRates, 330 systemConstraints.perLensConstraints[cameraAppSettings.cameraLensFacing] 331 ?.supportedFixedFrameRates ?: emptySet(), 332 cameraAppSettings.previewStabilization, 333 cameraAppSettings.videoCaptureStabilization 334 ) 335 if (fpsUiState is SingleSelectableState.Disabled) { 336 Log.d(TAG, "fps option $fpsOption disabled. ${fpsUiState.disabledRationale::class}") 337 } 338 optionConstraintRationale[fpsOption] = fpsUiState 339 } 340 return FpsUiState.Enabled( 341 currentSelection = cameraAppSettings.targetFrameRate, 342 fpsAutoState = SingleSelectableState.Selectable, 343 fpsFifteenState = optionConstraintRationale[FPS_15]!!, 344 fpsThirtyState = optionConstraintRationale[FPS_30]!!, 345 fpsSixtyState = optionConstraintRationale[FPS_60]!! 346 ) 347 } 348 349 /** 350 * Auxiliary function to determine if an FPS option should be disabled or not 351 */ 352 private fun isFpsOptionEnabled( 353 fpsOption: Int, 354 defaultLensFacing: LensFacing, 355 deviceFrameRates: Set<Int>, 356 lensFrameRates: Set<Int>, 357 previewStabilization: Stabilization, 358 videoStabilization: Stabilization 359 ): SingleSelectableState { 360 // if device doesnt support the fps option, disable 361 if (!deviceFrameRates.contains(fpsOption)) { 362 return SingleSelectableState.Disabled( 363 disabledRationale = DeviceUnsupportedRationale(R.string.fps_rationale_prefix) 364 ) 365 } 366 // if the current lens doesnt support the fps, disable 367 if (!lensFrameRates.contains(fpsOption)) { 368 Log.d(TAG, "FPS disabled for current lens") 369 370 return SingleSelectableState.Disabled( 371 getLensUnsupportedRationale(defaultLensFacing, R.string.fps_rationale_prefix) 372 ) 373 } 374 375 // if stabilization is on and the option is incompatible, disable 376 if (( 377 previewStabilization == Stabilization.ON && 378 (fpsOption == FPS_15 || fpsOption == FPS_60) 379 ) || 380 (videoStabilization == Stabilization.ON && fpsOption == FPS_60) 381 ) { 382 return SingleSelectableState.Disabled( 383 StabilizationUnsupportedRationale(R.string.fps_rationale_prefix) 384 ) 385 } 386 387 return SingleSelectableState.Selectable 388 } 389 390 fun setDefaultLensFacing(lensFacing: LensFacing) { 391 viewModelScope.launch { 392 settingsRepository.updateDefaultLensFacing(lensFacing) 393 Log.d(TAG, "set camera default facing: $lensFacing") 394 } 395 } 396 397 fun setDarkMode(darkMode: DarkMode) { 398 viewModelScope.launch { 399 settingsRepository.updateDarkModeStatus(darkMode) 400 Log.d(TAG, "set dark mode theme: $darkMode") 401 } 402 } 403 404 fun setFlashMode(flashMode: FlashMode) { 405 viewModelScope.launch { 406 settingsRepository.updateFlashModeStatus(flashMode) 407 Log.d(TAG, "set flash mode: $flashMode") 408 } 409 } 410 411 fun setTargetFrameRate(targetFrameRate: Int) { 412 viewModelScope.launch { 413 settingsRepository.updateTargetFrameRate(targetFrameRate) 414 Log.d(TAG, "set target frame rate: $targetFrameRate") 415 } 416 } 417 418 fun setAspectRatio(aspectRatio: AspectRatio) { 419 viewModelScope.launch { 420 settingsRepository.updateAspectRatio(aspectRatio) 421 Log.d(TAG, "set aspect ratio: $aspectRatio") 422 } 423 } 424 425 fun setCaptureMode(captureMode: CaptureMode) { 426 viewModelScope.launch { 427 settingsRepository.updateCaptureMode(captureMode) 428 Log.d(TAG, "set default capture mode: $captureMode") 429 } 430 } 431 432 fun setPreviewStabilization(stabilization: Stabilization) { 433 viewModelScope.launch { 434 settingsRepository.updatePreviewStabilization(stabilization) 435 Log.d(TAG, "set preview stabilization: $stabilization") 436 } 437 } 438 439 fun setVideoStabilization(stabilization: Stabilization) { 440 viewModelScope.launch { 441 settingsRepository.updateVideoStabilization(stabilization) 442 Log.d(TAG, "set video stabilization: $stabilization") 443 } 444 } 445 } 446