Skip to content

Commit ed5c1b9

Browse files
committed
Refactor IntroductionViewModel for preview
1 parent f8693ae commit ed5c1b9

4 files changed

Lines changed: 252 additions & 142 deletions

File tree

app/src/main/kotlin/com/darkrockstudios/app/securecamera/AppModule.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.darkrockstudios.app.securecamera.camera.ThumbnailCache
88
import com.darkrockstudios.app.securecamera.gallery.GalleryViewModel
99
import com.darkrockstudios.app.securecamera.import.ImportPhotosViewModel
1010
import com.darkrockstudios.app.securecamera.introduction.IntroductionViewModel
11+
import com.darkrockstudios.app.securecamera.introduction.IntroductionViewModelImpl
1112
import com.darkrockstudios.app.securecamera.obfuscation.ObfuscatePhotoViewModel
1213
import com.darkrockstudios.app.securecamera.preferences.AppSettingsDataSource
1314
import com.darkrockstudios.app.securecamera.preferences.PreferencesAppSettingsDataSource
@@ -88,7 +89,7 @@ val appModule = module {
8889
viewModelOf(::ViewPhotoViewModel)
8990
viewModelOf(::GalleryViewModel)
9091
viewModelOf(::SettingsViewModel)
91-
viewModelOf(::IntroductionViewModel)
92+
viewModelOf(::IntroductionViewModelImpl) bind IntroductionViewModel::class
9293
viewModelOf(::PinVerificationViewModel)
9394
viewModelOf(::ImportPhotosViewModel)
9495
}
Lines changed: 21 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,30 @@
11
package com.darkrockstudios.app.securecamera.introduction
22

3-
import android.content.Context
4-
import androidx.compose.material.icons.Icons
5-
import androidx.compose.material.icons.automirrored.filled.Send
6-
import androidx.compose.material.icons.filled.Camera
7-
import androidx.compose.material.icons.filled.LocationOff
8-
import androidx.compose.material.icons.filled.Lock
9-
import androidx.compose.material.icons.filled.MyLocation
10-
import androidx.compose.material.icons.filled.PrivacyTip
11-
import androidx.lifecycle.viewModelScope
12-
import com.darkrockstudios.app.securecamera.BaseViewModel
13-
import com.darkrockstudios.app.securecamera.R
14-
import com.darkrockstudios.app.securecamera.security.HardwareSchemeConfig
153
import com.darkrockstudios.app.securecamera.security.SecurityLevel
16-
import com.darkrockstudios.app.securecamera.security.SecurityLevelDetector
17-
import com.darkrockstudios.app.securecamera.security.SoftwareSchemeConfig
18-
import com.darkrockstudios.app.securecamera.usecases.CreatePinUseCase
19-
import com.darkrockstudios.app.securecamera.usecases.PinSizeUseCase
20-
import com.darkrockstudios.app.securecamera.usecases.PinStrengthCheckUseCase
21-
import kotlinx.coroutines.Dispatchers
22-
import kotlinx.coroutines.flow.MutableSharedFlow
23-
import kotlinx.coroutines.flow.asSharedFlow
24-
import kotlinx.coroutines.flow.update
25-
import kotlinx.coroutines.launch
26-
import kotlin.time.Duration.Companion.minutes
4+
import kotlinx.coroutines.flow.SharedFlow
5+
import kotlinx.coroutines.flow.StateFlow
276

28-
class IntroductionViewModel(
29-
private val appContext: Context,
30-
private val securityLevelDetector: SecurityLevelDetector,
31-
private val pinStrengthCheck: PinStrengthCheckUseCase,
32-
private val createPinUseCase: CreatePinUseCase,
33-
private val pinSizeUseCase: PinSizeUseCase,
34-
) : BaseViewModel<IntroductionUiState>() {
7+
interface IntroductionViewModel {
8+
val uiState: StateFlow<IntroductionUiState>
9+
val skipToPage: SharedFlow<Int>
3510

36-
override fun createState() = IntroductionUiState(
37-
securityLevel = securityLevelDetector.detectSecurityLevel(),
38-
slides = createSlides(),
39-
pinSize = pinSizeUseCase.getPinSizeRange()
40-
)
11+
fun setPage(page: Int)
12+
suspend fun navigateToNextPage()
13+
suspend fun navigateToSecurity()
4114

42-
private val _skipToPage = MutableSharedFlow<Int>()
43-
val skipToPage = _skipToPage.asSharedFlow()
44-
45-
private fun createSlides(): List<IntroductionSlide> {
46-
return listOf(
47-
IntroductionSlide(
48-
icon = Icons.Filled.Camera,
49-
title = appContext.getString(R.string.intro_slide0_title),
50-
description = appContext.getString(R.string.intro_slide0_description)
51-
),
52-
IntroductionSlide(
53-
icon = Icons.Filled.PrivacyTip,
54-
title = appContext.getString(R.string.intro_slide1_title),
55-
description = appContext.getString(R.string.intro_slide1_description)
56-
),
57-
IntroductionSlide(
58-
icon = Icons.Filled.Lock,
59-
title = appContext.getString(R.string.intro_slide2_title),
60-
description = appContext.getString(R.string.intro_slide2_description)
61-
),
62-
IntroductionSlide(
63-
icon = Icons.AutoMirrored.Filled.Send,
64-
title = appContext.getString(R.string.intro_slide3_title),
65-
description = appContext.getString(R.string.intro_slide3_description),
66-
),
67-
IntroductionSlide(
68-
icon = Icons.Filled.LocationOff,
69-
title = appContext.getString(R.string.intro_slide4_title),
70-
description = appContext.getString(R.string.intro_slide4_description),
71-
),
72-
IntroductionSlide(
73-
icon = Icons.Filled.MyLocation,
74-
title = appContext.getString(R.string.intro_slide5_title),
75-
description = appContext.getString(R.string.intro_slide5_description),
76-
),
77-
)
78-
}
79-
80-
fun setPage(page: Int) {
81-
_uiState.update { it.copy(currentPage = page) }
82-
}
83-
84-
suspend fun navigateToNextPage() {
85-
val currentPage = uiState.value.currentPage
86-
val totalPages = uiState.value.slides.size + 2
87-
if (currentPage < totalPages - 1) {
88-
_skipToPage.emit(currentPage + 1)
89-
}
90-
}
91-
92-
suspend fun navigateToSecurity() {
93-
_skipToPage.emit(uiState.value.slides.size)
94-
}
95-
96-
fun createPin(pin: String, confirmPin: String) {
97-
val config = when (uiState.value.securityLevel) {
98-
SecurityLevel.TEE, SecurityLevel.STRONGBOX -> HardwareSchemeConfig(
99-
requireBiometricAttestation = uiState.value.requireBiometrics,
100-
authTimeout = 5.minutes, // Hard coded for now
101-
ephemeralKey = uiState.value.ephemeralKey
102-
)
103-
104-
SecurityLevel.SOFTWARE -> SoftwareSchemeConfig
105-
}
106-
107-
val pinSize = pinSizeUseCase.getPinSizeRange()
108-
if (pin != confirmPin || (pin.length in pinSize).not()) {
109-
_uiState.update { it.copy(errorMessage = appContext.getString(R.string.pin_creation_error)) }
110-
return
111-
}
112-
113-
val strongPin = pinStrengthCheck.isPinStrongEnough(pin)
114-
if (strongPin.not()) {
115-
_uiState.update { it.copy(errorMessage = appContext.getString(R.string.pin_creation_error_weak_pin)) }
116-
return
117-
}
118-
119-
// Set loading state to true before starting PIN creation
120-
_uiState.update { it.copy(isCreatingPin = true, errorMessage = null) }
121-
122-
viewModelScope.launch(Dispatchers.Default) {
123-
if (createPinUseCase.createPin(pin, config)) {
124-
_uiState.update { it.copy(pinCreated = true, isCreatingPin = false) }
125-
} else {
126-
_uiState.update { it.copy(isCreatingPin = false) }
127-
}
128-
}
129-
}
130-
131-
fun toggleBiometricsRequired() {
132-
_uiState.update { it.copy(requireBiometrics = it.requireBiometrics.not()) }
133-
}
134-
135-
fun toggleEphemeralKey() {
136-
_uiState.update { it.copy(ephemeralKey = it.ephemeralKey.not()) }
137-
}
15+
fun createPin(pin: String, confirmPin: String)
16+
fun toggleBiometricsRequired()
17+
fun toggleEphemeralKey()
13818
}
13919

14020
data class IntroductionUiState(
141-
val slides: List<IntroductionSlide> = emptyList(),
142-
val errorMessage: String? = null,
143-
val pinCreated: Boolean = false,
144-
val securityLevel: SecurityLevel,
145-
val requireBiometrics: Boolean = false,
146-
val ephemeralKey: Boolean = false,
147-
val currentPage: Int = 0,
148-
val isCreatingPin: Boolean = false,
149-
val pinSize: IntRange,
150-
)
21+
val slides: List<IntroductionSlide> = emptyList(),
22+
val errorMessage: String? = null,
23+
val pinCreated: Boolean = false,
24+
val securityLevel: SecurityLevel,
25+
val requireBiometrics: Boolean = false,
26+
val ephemeralKey: Boolean = false,
27+
val currentPage: Int = 0,
28+
val isCreatingPin: Boolean = false,
29+
val pinSize: IntRange,
30+
)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package com.darkrockstudios.app.securecamera.introduction
2+
3+
import android.content.Context
4+
import androidx.compose.material.icons.Icons
5+
import androidx.compose.material.icons.automirrored.filled.Send
6+
import androidx.compose.material.icons.filled.*
7+
import androidx.lifecycle.viewModelScope
8+
import com.darkrockstudios.app.securecamera.BaseViewModel
9+
import com.darkrockstudios.app.securecamera.R
10+
import com.darkrockstudios.app.securecamera.security.HardwareSchemeConfig
11+
import com.darkrockstudios.app.securecamera.security.SecurityLevel
12+
import com.darkrockstudios.app.securecamera.security.SecurityLevelDetector
13+
import com.darkrockstudios.app.securecamera.security.SoftwareSchemeConfig
14+
import com.darkrockstudios.app.securecamera.usecases.CreatePinUseCase
15+
import com.darkrockstudios.app.securecamera.usecases.PinSizeUseCase
16+
import com.darkrockstudios.app.securecamera.usecases.PinStrengthCheckUseCase
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.flow.MutableSharedFlow
19+
import kotlinx.coroutines.flow.asSharedFlow
20+
import kotlinx.coroutines.flow.update
21+
import kotlinx.coroutines.launch
22+
import kotlin.time.Duration.Companion.minutes
23+
24+
class IntroductionViewModelImpl(
25+
private val appContext: Context,
26+
private val securityLevelDetector: SecurityLevelDetector,
27+
private val pinStrengthCheck: PinStrengthCheckUseCase,
28+
private val createPinUseCase: CreatePinUseCase,
29+
private val pinSizeUseCase: PinSizeUseCase,
30+
) : BaseViewModel<IntroductionUiState>(), IntroductionViewModel {
31+
32+
override fun createState() = IntroductionUiState(
33+
securityLevel = securityLevelDetector.detectSecurityLevel(),
34+
slides = createSlides(),
35+
pinSize = pinSizeUseCase.getPinSizeRange()
36+
)
37+
38+
private val _skipToPage = MutableSharedFlow<Int>()
39+
override val skipToPage = _skipToPage.asSharedFlow()
40+
41+
private fun createSlides(): List<IntroductionSlide> {
42+
return listOf(
43+
IntroductionSlide(
44+
icon = Icons.Filled.Camera,
45+
title = appContext.getString(R.string.intro_slide0_title),
46+
description = appContext.getString(R.string.intro_slide0_description)
47+
),
48+
IntroductionSlide(
49+
icon = Icons.Filled.PrivacyTip,
50+
title = appContext.getString(R.string.intro_slide1_title),
51+
description = appContext.getString(R.string.intro_slide1_description)
52+
),
53+
IntroductionSlide(
54+
icon = Icons.Filled.Lock,
55+
title = appContext.getString(R.string.intro_slide2_title),
56+
description = appContext.getString(R.string.intro_slide2_description)
57+
),
58+
IntroductionSlide(
59+
icon = Icons.AutoMirrored.Filled.Send,
60+
title = appContext.getString(R.string.intro_slide3_title),
61+
description = appContext.getString(R.string.intro_slide3_description),
62+
),
63+
IntroductionSlide(
64+
icon = Icons.Filled.LocationOff,
65+
title = appContext.getString(R.string.intro_slide4_title),
66+
description = appContext.getString(R.string.intro_slide4_description),
67+
),
68+
IntroductionSlide(
69+
icon = Icons.Filled.MyLocation,
70+
title = appContext.getString(R.string.intro_slide5_title),
71+
description = appContext.getString(R.string.intro_slide5_description),
72+
),
73+
)
74+
}
75+
76+
override fun setPage(page: Int) {
77+
_uiState.update { it.copy(currentPage = page) }
78+
}
79+
80+
override suspend fun navigateToNextPage() {
81+
val currentPage = uiState.value.currentPage
82+
val totalPages = uiState.value.slides.size + 2
83+
if (currentPage < totalPages - 1) {
84+
_skipToPage.emit(currentPage + 1)
85+
}
86+
}
87+
88+
override suspend fun navigateToSecurity() {
89+
_skipToPage.emit(uiState.value.slides.size)
90+
}
91+
92+
override fun createPin(pin: String, confirmPin: String) {
93+
val config = when (uiState.value.securityLevel) {
94+
SecurityLevel.TEE, SecurityLevel.STRONGBOX -> HardwareSchemeConfig(
95+
requireBiometricAttestation = uiState.value.requireBiometrics,
96+
authTimeout = 5.minutes, // Hard coded for now
97+
ephemeralKey = uiState.value.ephemeralKey
98+
)
99+
100+
SecurityLevel.SOFTWARE -> SoftwareSchemeConfig
101+
}
102+
103+
val pinSize = pinSizeUseCase.getPinSizeRange()
104+
if (pin != confirmPin || (pin.length in pinSize).not()) {
105+
_uiState.update { it.copy(errorMessage = appContext.getString(R.string.pin_creation_error)) }
106+
return
107+
}
108+
109+
val strongPin = pinStrengthCheck.isPinStrongEnough(pin)
110+
if (strongPin.not()) {
111+
_uiState.update { it.copy(errorMessage = appContext.getString(R.string.pin_creation_error_weak_pin)) }
112+
return
113+
}
114+
115+
// Set loading state to true before starting PIN creation
116+
_uiState.update { it.copy(isCreatingPin = true, errorMessage = null) }
117+
118+
viewModelScope.launch(Dispatchers.Default) {
119+
if (createPinUseCase.createPin(pin, config)) {
120+
_uiState.update { it.copy(pinCreated = true, isCreatingPin = false) }
121+
} else {
122+
_uiState.update { it.copy(isCreatingPin = false) }
123+
}
124+
}
125+
}
126+
127+
override fun toggleBiometricsRequired() {
128+
_uiState.update { it.copy(requireBiometrics = it.requireBiometrics.not()) }
129+
}
130+
131+
override fun toggleEphemeralKey() {
132+
_uiState.update { it.copy(ephemeralKey = it.ephemeralKey.not()) }
133+
}
134+
}

0 commit comments

Comments
 (0)