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