feat(settings): add custom SMS scan start date#516
Conversation
Let users scan SMS from a chosen date through today, with backup support and unit tests for the new scan period logic. Co-authored-by: Cursor <cursoragent@cursor.com>
Greptile SummaryThis PR adds a "Custom date" option to the SMS Scan Period setting, letting users scan from a chosen start date through today. The scan logic, backup/restore, and the limited-data banner in the transactions screen are all updated to respect the new mode, with a new
Confidence Score: 4/5Safe to merge after addressing the banner logic in TransactionsViewModel; no data loss or scan correctness issues were found. The core scan logic in SmsScanParamsCalculator is well-factored and thoroughly tested. The main concern is in TransactionsViewModel: getSmsScanStartDate() returns null when the custom-date flag is set but the date value hasn't been written yet, which silently suppresses the limited-data banner. The scan worker itself handles the null gracefully via a fallback, so no transactions are missed; the issue is purely in the banner display. TransactionsViewModel.kt (getSmsScanStartDate null-return path) and SettingsViewModel.kt (non-atomic DataStore writes in updateSmsScanCustomDate) Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
actor User
participant SettingsScreen
participant SettingsViewModel
participant UserPrefsRepo
participant DataStore
participant Worker as OptimizedSmsReaderWorker
participant Calculator as SmsScanParamsCalculator
User->>SettingsScreen: Select "Custom date"
SettingsScreen->>SettingsScreen: "showSmsScanDatePicker = true"
User->>SettingsScreen: "Pick date & confirm"
SettingsScreen->>SettingsViewModel: updateSmsScanCustomDate(millis)
SettingsViewModel->>Calculator: normalizePickerDateToLocalStartOfDay(millis)
Calculator-->>SettingsViewModel: localMidnightMillis
SettingsViewModel->>UserPrefsRepo: setLastScanTimestamp(0L)
SettingsViewModel->>UserPrefsRepo: updateSmsScanAllTime(false)
SettingsViewModel->>UserPrefsRepo: updateSmsScanUseCustomDate(true)
Note over UserPrefsRepo,DataStore: DataStore emits — useCustomDate=true but date=null window
SettingsViewModel->>UserPrefsRepo: updateSmsScanCustomDate(localMidnightMillis)
UserPrefsRepo->>DataStore: all three keys written sequentially
Worker->>UserPrefsRepo: getSmsScanUseCustomDate()
Worker->>UserPrefsRepo: getSmsScanCustomDate()
Worker->>Calculator: compute(SmsScanParamsInput(...))
Calculator-->>Worker: "SmsScanParams(scanStartTime, needsFullScan=true)"
Worker->>UserPrefsRepo: setLastScanPeriod(SCAN_PERIOD_CUSTOM_DATE)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
actor User
participant SettingsScreen
participant SettingsViewModel
participant UserPrefsRepo
participant DataStore
participant Worker as OptimizedSmsReaderWorker
participant Calculator as SmsScanParamsCalculator
User->>SettingsScreen: Select "Custom date"
SettingsScreen->>SettingsScreen: "showSmsScanDatePicker = true"
User->>SettingsScreen: "Pick date & confirm"
SettingsScreen->>SettingsViewModel: updateSmsScanCustomDate(millis)
SettingsViewModel->>Calculator: normalizePickerDateToLocalStartOfDay(millis)
Calculator-->>SettingsViewModel: localMidnightMillis
SettingsViewModel->>UserPrefsRepo: setLastScanTimestamp(0L)
SettingsViewModel->>UserPrefsRepo: updateSmsScanAllTime(false)
SettingsViewModel->>UserPrefsRepo: updateSmsScanUseCustomDate(true)
Note over UserPrefsRepo,DataStore: DataStore emits — useCustomDate=true but date=null window
SettingsViewModel->>UserPrefsRepo: updateSmsScanCustomDate(localMidnightMillis)
UserPrefsRepo->>DataStore: all three keys written sequentially
Worker->>UserPrefsRepo: getSmsScanUseCustomDate()
Worker->>UserPrefsRepo: getSmsScanCustomDate()
Worker->>Calculator: compute(SmsScanParamsInput(...))
Calculator-->>Worker: "SmsScanParams(scanStartTime, needsFullScan=true)"
Worker->>UserPrefsRepo: setLastScanPeriod(SCAN_PERIOD_CUSTOM_DATE)
|
| private fun getSmsScanStartDate(): LocalDate? { | ||
| if (smsScanUseCustomDate.value) { | ||
| return smsScanCustomDate.value?.let { millis -> | ||
| java.time.Instant.ofEpochMilli(millis) | ||
| .atZone(java.time.ZoneId.systemDefault()) | ||
| .toLocalDate() | ||
| } | ||
| } | ||
|
|
||
| return LocalDate.now().minusMonths(smsScanMonths.value.toLong()) | ||
| } |
There was a problem hiding this comment.
Banner incorrectly hidden when custom-date state is partially written
getSmsScanStartDate() returns null when smsScanUseCustomDate.value = true but smsScanCustomDate.value = null, which causes isShowingLimitedData() to return false and suppress the limited-data banner. This state is reachable in two ways: (1) the two separate DataStore writes in SettingsViewModel.updateSmsScanCustomDate — after updateSmsScanUseCustomDate(true) completes but before updateSmsScanCustomDate(normalizedDate) is written, DataStore emits the update and the active StateFlow flips smsScanUseCustomDate to true while smsScanCustomDate is still null; (2) a backup restored with smsScanUseCustomDate = true but a missing smsScanCustomDate field (the ?.let { } skip in BackupImporter leaves the DataStore key absent).
Consider returning a sensible default scan start (e.g., falling back to the month-based date) instead of null when the custom date has not been written yet.
| fun updateSmsScanCustomDate(selectedDateMillis: Long) { | ||
| viewModelScope.launch { | ||
| val normalizedDate = SmsScanParamsCalculator.normalizePickerDateToLocalStartOfDay(selectedDateMillis) | ||
| val currentDate = userPreferencesRepository.getSmsScanCustomDate() | ||
| val wasUsingCustomDate = userPreferencesRepository.getSmsScanUseCustomDate() | ||
|
|
||
| if (!wasUsingCustomDate || currentDate == null || normalizedDate < currentDate) { | ||
| userPreferencesRepository.setLastScanTimestamp(0L) | ||
| Log.d("SettingsViewModel", "Custom SMS scan date updated - will perform full scan") | ||
| } | ||
|
|
||
| userPreferencesRepository.updateSmsScanAllTime(false) | ||
| userPreferencesRepository.updateSmsScanUseCustomDate(true) | ||
| userPreferencesRepository.updateSmsScanCustomDate(normalizedDate) | ||
| } | ||
| } |
There was a problem hiding this comment.
Three non-atomic DataStore writes can expose inconsistent intermediate state
updateSmsScanCustomDate issues updateSmsScanAllTime(false), updateSmsScanUseCustomDate(true), and updateSmsScanCustomDate(normalizedDate) as three separate sequential suspend calls. Each write causes DataStore to emit to any active collectors immediately. If OptimizedSmsReaderWorker starts between writes 2 and 3, it sees smsScanUseCustomDate = true but smsScanCustomDate = null; scanPeriodStartTime then silently falls back to the month-based boundary instead of the user-chosen date, producing a subtly wrong scan range without any error signal. Grouping all three mutations into a single DataStore.edit { } block would make the transition atomic.
|
Thanks! |
|
Thanks @saadaltabari — the scan-start-date logic is well-factored and nicely tested (Greptile 4/5). It flagged one display-only issue: It's behind Soft housekeeping note: if it's quiet for ~another week we may close it just for tidiness — not a rejection, reopen/ping anytime. 🙏 |
Summary
Adds a Custom date option to Settings → SMS Scan Period so users can scan SMS messages from a chosen start date through today. Scan logic, backup/restore, and the transactions limited-data banner were updated to respect the custom range, with unit tests for the new behavior.
Type of change
How to test
sms_scan_use_custom_dateandsms_scan_custom_dateare preserved../gradlew :app:testStandardDebugUnitTest --tests "com.pennywiseai.tracker.data.manager.SmsScanParamsCalculatorTest" --tests "com.pennywiseai.tracker.data.backup.BackupModelsTest".Breaking changes
Checklist
Made with Cursor