Skip to content

Commit 4928abd

Browse files
committed
🎯 Critical: Wire database to ViewModel for persistent history
This is the FINAL MILE fix - history now survives app restarts. ## SharedViewModel - Now takes HistoryRepository as constructor parameter - processScanResult persists to SQLite database - scanHistory is now an observable Flow from DB - Added statistics method for dashboard display - Added deleteScan for individual removal ## Android (Koin DI) - Added DatabaseDriverFactory provider - Added QRShieldDatabase singleton - Added SqlDelightHistoryRepository binding - ViewModel now receives repository via DI ## Desktop - Initialized SQLite database directly - ViewModel now has persistent storage cross-platform ## What this means for users - Scan history persists after app restart - History is stored locally (SQLite) - Search and filter work on real data - Ready for production use
1 parent 365764c commit 4928abd

3 files changed

Lines changed: 148 additions & 25 deletions

File tree

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package com.qrshield.android.di
22

33
import com.qrshield.core.PhishingEngine
4+
import com.qrshield.data.DatabaseDriverFactory
5+
import com.qrshield.data.HistoryRepository
6+
import com.qrshield.data.SqlDelightHistoryRepository
7+
import com.qrshield.db.QRShieldDatabase
48
import com.qrshield.scanner.QrScanner
59
import com.qrshield.scanner.QrScannerFactory
610
import com.qrshield.ui.SharedViewModel
@@ -12,18 +16,46 @@ import org.koin.dsl.module
1216

1317
/**
1418
* Koin dependency injection module for Android.
19+
*
20+
* Provides all dependencies needed for the Android app including:
21+
* - Database and repository
22+
* - Scanner implementation
23+
* - ViewModel with persistence
24+
*
25+
* @author QR-SHIELD Security Team
26+
* @since 1.0.0
1527
*/
1628
val androidModule = module {
1729

18-
// Application scope
30+
// Application scope for coroutines
1931
single { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
2032

2133
// Core engines
2234
single { PhishingEngine() }
2335

24-
// Scanner
36+
// Database driver factory (Android-specific)
37+
single { DatabaseDriverFactory(androidContext()) }
38+
39+
// SQLDelight database
40+
single {
41+
val driver = get<DatabaseDriverFactory>().createDriver()
42+
QRShieldDatabase(driver)
43+
}
44+
45+
// History repository with persistence
46+
single<HistoryRepository> {
47+
SqlDelightHistoryRepository(get())
48+
}
49+
50+
// Scanner (Android-specific using CameraX + ML Kit)
2551
single<QrScanner> { QrScannerFactory(androidContext()).create() }
2652

27-
// ViewModel
28-
factory { SharedViewModel(get(), get()) }
53+
// ViewModel with injected repository
54+
factory {
55+
SharedViewModel(
56+
phishingEngine = get(),
57+
historyRepository = get(),
58+
coroutineScope = get()
59+
)
60+
}
2961
}
Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,57 @@
11
package com.qrshield.ui
22

33
import com.qrshield.core.PhishingEngine
4+
import com.qrshield.data.HistoryRepository
45
import com.qrshield.model.RiskAssessment
56
import com.qrshield.model.ScanHistoryItem
67
import com.qrshield.model.ScanResult
78
import com.qrshield.model.ScanSource
89
import com.qrshield.model.Verdict
910
import kotlinx.coroutines.CoroutineScope
1011
import kotlinx.coroutines.flow.MutableStateFlow
12+
import kotlinx.coroutines.flow.SharingStarted
1113
import kotlinx.coroutines.flow.StateFlow
1214
import kotlinx.coroutines.flow.asStateFlow
15+
import kotlinx.coroutines.flow.stateIn
1316
import kotlinx.coroutines.launch
1417
import kotlinx.datetime.Clock
1518

1619
/**
1720
* Shared ViewModel for QR-SHIELD UI
1821
*
1922
* Manages UI state across all platforms using Kotlin Coroutines Flow.
23+
* Now with persistent history storage via HistoryRepository.
24+
*
25+
* @param phishingEngine Engine for analyzing URLs for phishing threats
26+
* @param historyRepository Repository for persisting scan history
27+
* @param coroutineScope Scope for launching coroutines
28+
*
29+
* @author QR-SHIELD Security Team
30+
* @since 1.0.0
2031
*/
2132
class SharedViewModel(
2233
private val phishingEngine: PhishingEngine = PhishingEngine(),
34+
private val historyRepository: HistoryRepository,
2335
private val coroutineScope: CoroutineScope
2436
) {
2537
private val _uiState = MutableStateFlow<UiState>(UiState.Idle)
2638
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
2739

28-
private val _scanHistory = MutableStateFlow<List<ScanHistoryItem>>(emptyList())
29-
val scanHistory: StateFlow<List<ScanHistoryItem>> = _scanHistory.asStateFlow()
40+
/**
41+
* Scan history as an observable Flow from the database.
42+
* Automatically updates when new scans are added.
43+
*/
44+
val scanHistory: StateFlow<List<ScanHistoryItem>> = historyRepository
45+
.observe()
46+
.stateIn(
47+
scope = coroutineScope,
48+
started = SharingStarted.WhileSubscribed(5000),
49+
initialValue = emptyList()
50+
)
3051

3152
/**
32-
* Process a scan result from camera or gallery
53+
* Process a scan result from camera or gallery.
54+
* Analyzes the content and persists to database.
3355
*/
3456
fun processScanResult(result: ScanResult, source: ScanSource) {
3557
coroutineScope.launch {
@@ -41,8 +63,8 @@ class SharedViewModel(
4163

4264
_uiState.value = UiState.Result(assessment)
4365

44-
// Add to history
45-
addToHistory(result.content, assessment, source)
66+
// Persist to database
67+
saveToHistory(result.content, assessment, source)
4668
}
4769
is ScanResult.Error -> {
4870
_uiState.value = UiState.Error(result.message)
@@ -55,7 +77,8 @@ class SharedViewModel(
5577
}
5678

5779
/**
58-
* Analyze a URL directly (e.g., from clipboard)
80+
* Analyze a URL directly (e.g., from clipboard).
81+
* Analyzes and persists to database.
5982
*/
6083
fun analyzeUrl(url: String, source: ScanSource = ScanSource.CLIPBOARD) {
6184
coroutineScope.launch {
@@ -65,64 +88,94 @@ class SharedViewModel(
6588

6689
_uiState.value = UiState.Result(assessment)
6790

68-
addToHistory(url, assessment, source)
91+
// Persist to database
92+
saveToHistory(url, assessment, source)
6993
}
7094
}
7195

7296
/**
73-
* Start scanning mode
97+
* Start scanning mode.
7498
*/
7599
fun startScanning() {
76100
_uiState.value = UiState.Scanning
77101
}
78102

79103
/**
80-
* Return to idle state
104+
* Return to idle state.
81105
*/
82106
fun resetToIdle() {
83107
_uiState.value = UiState.Idle
84108
}
85109

86110
/**
87-
* Clear scan history
111+
* Clear scan history from database.
88112
*/
89113
fun clearHistory() {
90-
_scanHistory.value = emptyList()
114+
coroutineScope.launch {
115+
historyRepository.clearAll()
116+
}
117+
}
118+
119+
/**
120+
* Delete a specific scan from history.
121+
*/
122+
fun deleteScan(id: String) {
123+
coroutineScope.launch {
124+
historyRepository.delete(id)
125+
}
91126
}
92127

93128
/**
94-
* Get scan by ID from history
129+
* Get scan by ID from history.
95130
*/
96-
fun getScanById(id: String): ScanHistoryItem? {
97-
return _scanHistory.value.find { it.id == id }
131+
suspend fun getScanById(id: String): ScanHistoryItem? {
132+
return historyRepository.getById(id)
98133
}
99134

100-
private fun addToHistory(
135+
/**
136+
* Get history statistics (for dashboard display).
137+
*/
138+
suspend fun getStatistics(): HistoryStatistics {
139+
val all = historyRepository.getAll()
140+
141+
return HistoryStatistics(
142+
totalScans = all.size,
143+
safeCount = all.count { it.verdict == Verdict.SAFE },
144+
suspiciousCount = all.count { it.verdict == Verdict.SUSPICIOUS },
145+
maliciousCount = all.count { it.verdict == Verdict.MALICIOUS },
146+
averageScore = if (all.isEmpty()) 0.0 else all.map { it.score }.average()
147+
)
148+
}
149+
150+
/**
151+
* Save scan result to persistent database.
152+
*/
153+
private suspend fun saveToHistory(
101154
url: String,
102155
assessment: RiskAssessment,
103156
source: ScanSource
104157
) {
105158
val item = ScanHistoryItem(
106159
id = generateId(),
107-
url = url,
108-
score = assessment.score,
160+
url = url.take(2048), // Bound URL length for security
161+
score = assessment.score.coerceIn(0, 100),
109162
verdict = assessment.verdict,
110163
scannedAt = currentTimeMillis(),
111164
source = source
112165
)
113-
_scanHistory.value = listOf(item) + _scanHistory.value.take(99)
166+
167+
historyRepository.insert(item)
114168
}
115169

116170
private fun generateId(): String {
117171
return "scan_${currentTimeMillis()}_${(0..9999).random()}"
118172
}
119173

120-
// Expect function to be implemented per platform
121174
private fun currentTimeMillis(): Long = Clock.System.now().toEpochMilliseconds()
122175
}
123176

124177
/**
125-
* UI State sealed class
178+
* UI State sealed class for managing screen states.
126179
*/
127180
sealed class UiState {
128181
data object Idle : UiState()
@@ -131,3 +184,20 @@ sealed class UiState {
131184
data class Result(val assessment: RiskAssessment) : UiState()
132185
data class Error(val message: String) : UiState()
133186
}
187+
188+
/**
189+
* History statistics for dashboard display.
190+
*/
191+
data class HistoryStatistics(
192+
val totalScans: Int,
193+
val safeCount: Int,
194+
val suspiciousCount: Int,
195+
val maliciousCount: Int,
196+
val averageScore: Double
197+
) {
198+
val safePercentage: Double
199+
get() = if (totalScans > 0) safeCount.toDouble() / totalScans * 100 else 0.0
200+
201+
val threatPercentage: Double
202+
get() = if (totalScans > 0) (suspiciousCount + maliciousCount).toDouble() / totalScans * 100 else 0.0
203+
}

‎desktopApp/src/main/kotlin/com/qrshield/desktop/Main.kt‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import androidx.compose.ui.window.Window
1313
import androidx.compose.ui.window.application
1414
import androidx.compose.ui.window.rememberWindowState
1515
import com.qrshield.core.PhishingEngine
16+
import com.qrshield.data.DatabaseDriverFactory
17+
import com.qrshield.data.SqlDelightHistoryRepository
18+
import com.qrshield.db.QRShieldDatabase
1619
import com.qrshield.model.Verdict
1720
import com.qrshield.ui.SharedViewModel
1821
import kotlinx.coroutines.CoroutineScope
@@ -27,6 +30,7 @@ import java.util.Properties
2730
* Features:
2831
* - Window size/position persistence
2932
* - Cross-platform preferences storage
33+
* - Persistent scan history via SQLDelight
3034
*/
3135
fun main() = application {
3236
// Load saved window preferences
@@ -130,7 +134,24 @@ object WindowPreferences {
130134
@Preview
131135
fun QRShieldDesktopApp() {
132136
val coroutineScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
133-
val viewModel = remember { SharedViewModel(PhishingEngine(), coroutineScope) }
137+
138+
// Initialize database and repository
139+
val historyRepository = remember {
140+
val driverFactory = DatabaseDriverFactory()
141+
val driver = driverFactory.createDriver()
142+
val database = QRShieldDatabase(driver)
143+
SqlDelightHistoryRepository(database)
144+
}
145+
146+
// ViewModel with persistence
147+
val viewModel = remember {
148+
SharedViewModel(
149+
phishingEngine = PhishingEngine(),
150+
historyRepository = historyRepository,
151+
coroutineScope = coroutineScope
152+
)
153+
}
154+
134155
var urlInput by remember { mutableStateOf("") }
135156
var analysisResult by remember { mutableStateOf<AnalysisResult?>(null) }
136157

0 commit comments

Comments
 (0)