Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.timilehinaregbesola.mathalarm.framework.app.di.appModule
import com.timilehinaregbesola.mathalarm.platform.applyPlatformNightMode
import com.timilehinaregbesola.mathalarm.presentation.appsettings.AlarmPreferences
import com.timilehinaregbesola.mathalarm.presentation.appsettings.AlarmPreferencesImpl
import com.timilehinaregbesola.mathalarm.presentation.appsettings.DataStoreTheme
import kotlinx.coroutines.InternalCoroutinesApi
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
Expand All @@ -34,13 +33,9 @@ class AlarmApplication : Application() {
private fun applySavedNightMode() {
val themeId = Settings().getInt(
AlarmPreferencesImpl.APP_THEME_OPTION,
DataStoreTheme.SYSTEM.id
AlarmPreferences.Theme.SYSTEM.storageId
)
val theme = when (themeId) {
DataStoreTheme.LIGHT.id -> AlarmPreferences.Theme.LIGHT
DataStoreTheme.DARK.id -> AlarmPreferences.Theme.DARK
else -> AlarmPreferences.Theme.SYSTEM
}
val theme = AlarmPreferences.Theme.fromStorageId(themeId)
applyPlatformNightMode(this, theme)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ fun NavGraph(
announcementIds = null
when (feature) {
AnnouncementFeature.MATH_CHALLENGES, AnnouncementFeature.SNOOZE_SETTINGS -> {
val alarmJson = Json.encodeToString(AlarmMapper().mapFromDomainModel(Alarm()))
val alarmJson = Json.encodeToString(AlarmMapper().mapFromDomainModel(Alarm.newAlarm()))
backStack.add(SettingsSheet(alarmJson))
}
AnnouncementFeature.SKIP_NEXT -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import kotlinx.coroutines.flow.Flow
interface AlarmDataSource {
suspend fun addAlarm(alarm: Alarm): Long

suspend fun deleteAlarm(alarm: Alarm)

suspend fun deleteAlarmFromId(id: Long)

suspend fun updateAlarm(alarm: Alarm)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import com.timilehinaregbesola.mathalarm.domain.model.Alarm
class AlarmRepository(private val dataSource: AlarmDataSource) {
suspend fun addAlarm(alarm: Alarm) = dataSource.addAlarm(alarm)

suspend fun deleteAlarm(alarm: Alarm) = dataSource.deleteAlarm(alarm)

suspend fun deleteAlarmWithId(id: Long) = dataSource.deleteAlarmFromId(id)

suspend fun updateAlarm(alarm: Alarm) = dataSource.updateAlarm(alarm)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import kotlin.time.Clock

data class Alarm(
val alarmId: Long = 0L,
val newDateTime: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()),
val newHour: Int = newDateTime.hour,
val newMinute: Int = newDateTime.minute,
val hour: Int = newHour,
val minute: Int = newMinute,
val hour: Int = 0,
val minute: Int = 0,
val repeat: Boolean = false,
val repeatDays: String = "FFFFFFF",
val isOn: Boolean = false,
Expand Down Expand Up @@ -53,6 +50,9 @@ data class Alarm(
get() = isOn && skippedDate == null && (repeat || repeatDays.count { it == 'T' } > 1)

companion object {
fun newAlarm(now: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())): Alarm =
Alarm(hour = now.hour, minute = now.minute)

// Retain the persisted value for alarms saved by schema version 5.
const val SCHEDULING_IN_PROGRESS = "Scheduling has not completed"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ class ScheduleAlarm(
private val onCleanupSuccess: (Long) -> Unit = {},
) {
suspend operator fun invoke(alarm: Alarm, reschedule: Boolean) {
val saved = if (alarm.alarmId == 0L) {
alarmRepository.getLatestAlarm()
} else {
alarmRepository.findAlarm(alarm.alarmId)
} ?: return
require(alarm.alarmId != 0L) { "Save an alarm before scheduling it" }
val saved = alarmRepository.findAlarm(alarm.alarmId) ?: return
val times = alarmTimeCalculator.calculateAlarmTimes(saved.copy(skippedDate = null)).sorted()
val planned = saved.copy(
isOn = times.isNotEmpty(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.timilehinaregbesola.mathalarm.domain.model

import kotlinx.datetime.LocalDateTime
import kotlin.test.Test
import kotlin.test.assertEquals

class AlarmValueTest {
@Test
fun `creation date does not participate in alarm equality`() {
val first = Alarm.newAlarm(LocalDateTime(2030, 1, 6, 7, 30))
val later = Alarm.newAlarm(LocalDateTime(2040, 6, 1, 7, 30))
assertEquals(first, later)
assertEquals(first.hashCode(), later.hashCode())
assertEquals(Alarm(hour = 7, minute = 30), first)
}

@Test
fun `new draft takes hour and minute from one supplied instant`() {
for (now in listOf(LocalDateTime(2030, 12, 31, 23, 59), LocalDateTime(2031, 1, 1, 0, 0))) {
val draft = Alarm.newAlarm(now)
assertEquals(now.hour, draft.hour)
assertEquals(now.minute, draft.minute)
assertEquals(0L, draft.alarmId)
assertEquals(false, draft.isSaved)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ class AlarmRepositoryFake : AlarmDataSource {
return id
}

override suspend fun deleteAlarm(alarm: Alarm) {
alarmMap.remove(alarm.alarmId)
}

override suspend fun deleteAlarmFromId(id: Long) {
alarmMap.remove(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ class ScheduleAlarmTest {
alarmInteractor.clear()
}

@Test
fun `unsaved identity cannot schedule or change another stored alarm`() = runTest {
val first = Alarm(alarmId = 11, hour = 6, minute = 30, isSaved = true)
val latest = Alarm(alarmId = 12, hour = 8, minute = 45, isSaved = true)
addAlarmUseCase(first)
addAlarmUseCase(latest)
kotlin.test.assertFailsWith<IllegalArgumentException> {
scheduleAlarmUseCase(Alarm(hour = 9, minute = 15), reschedule = true)
}
assertEquals(first, findAlarmUseCase(first.alarmId))
assertEquals(latest, findAlarmUseCase(latest.alarmId))
assertEquals(emptyMap(), alarmInteractor.getScheduledAlarms())
}

@Test
fun `negative fixture identity schedules only its own persisted row`() = runTest {
val fixture = Alarm(alarmId = -42, hour = 6, minute = 30, isSaved = true)
val unrelated = Alarm(alarmId = 12, hour = 8, minute = 45, isSaved = true)
addAlarmUseCase(fixture)
addAlarmUseCase(unrelated)
scheduleAlarmUseCase(fixture, reschedule = false)
assertEquals(setOf(fixture.alarmId), alarmInteractor.getScheduledAlarms().keys)
assertEquals(alarmTimeCalculator.calculateAlarmTimes(fixture).sorted(), findAlarmUseCase(-42)?.pendingTimes)
assertEquals(unrelated, findAlarmUseCase(unrelated.alarmId))
}

@Test
fun `test if alarm is scheduled`() = runTest {
val newAlarm = Alarm(alarmId = 2, vibrate = true)
Expand Down
3 changes: 1 addition & 2 deletions iosApp/iosApp/NativeSettingsVerification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ enum NativeSettingsVerification {
let seedResult = first.model.state.results.first { $0.event is AlarmSettingsViewModel.UiEventTestAlarm }!
let seed = (seedResult.event as! AlarmSettingsViewModel.UiEventTestAlarm).alarm
first.model.acknowledgeResult(id: seedResult.id)
let secondAlarm = seed.doCopy(alarmId: -6001, newDateTime: seed.newDateTime,
newHour: seed.newHour, newMinute: seed.newMinute, hour: seed.hour, minute: seed.minute,
let secondAlarm = seed.doCopy(alarmId: -6001, hour: seed.hour, minute: seed.minute,
repeat: seed.repeat, repeatDays: seed.repeatDays, isOn: seed.isOn, difficulty: seed.difficulty,
questionCount: seed.questionCount, challengeOperations: seed.challengeOperations,
additionRange: seed.additionRange, factorRange: seed.factorRange, difficultyMix: seed.difficultyMix,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.timilehinaregbesola.mathalarm.presentation.appsettings

import co.touchlab.kermit.Logger
import com.russhwolf.settings.MapSettings
import io.kotest.matchers.shouldBe
import kotlin.test.Test
Expand All @@ -10,13 +9,13 @@ class AlarmPreferencesMigrationTest {
fun `legacy migration marks only the recorded feature and persists the result`() {
for (legacyId in listOf("math-challenges-v1", "skip-next-alarm-v1")) {
val settings = MapSettings().apply { putString("mathalarm_last_announcement", legacyId) }
val preferences = AlarmPreferencesImpl(AppThemeOptionsMapper(), Logger.withTag("test"), settings)
val preferences = AlarmPreferencesImpl(settings)
preferences.hasSeenAnnouncement(legacyId) shouldBe true
val other = if (legacyId == "math-challenges-v1") "skip-next-alarm-v1" else "math-challenges-v1"
preferences.hasSeenAnnouncement(other) shouldBe false
settings.hasKey("mathalarm_last_announcement") shouldBe false

val recreated = AlarmPreferencesImpl(AppThemeOptionsMapper(), Logger.withTag("test"), settings)
val recreated = AlarmPreferencesImpl(settings)
recreated.hasSeenAnnouncement(legacyId) shouldBe true
recreated.hasSeenAnnouncement(other) shouldBe false
}
Expand All @@ -28,7 +27,7 @@ class AlarmPreferencesMigrationTest {
putBoolean("mathalarm_seen_announcement_math-challenges-v1", true)
putString("mathalarm_last_announcement", "skip-next-alarm-v1")
}
val preferences = AlarmPreferencesImpl(AppThemeOptionsMapper(), Logger.withTag("test"), settings)
val preferences = AlarmPreferencesImpl(settings)
preferences.hasSeenAnnouncement("math-challenges-v1") shouldBe true
preferences.hasSeenAnnouncement("skip-next-alarm-v1") shouldBe true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.timilehinaregbesola.mathalarm.presentation.whatsnew

import co.touchlab.kermit.Logger
import com.russhwolf.settings.MapSettings
import com.timilehinaregbesola.mathalarm.presentation.appsettings.AlarmPreferencesImpl
import com.timilehinaregbesola.mathalarm.presentation.appsettings.AppThemeOptionsMapper
import kotlin.test.Test
import kotlin.test.assertEquals

class AnnouncementMigrationTest {
private fun preferences(settings: MapSettings) =
AlarmPreferencesImpl(AppThemeOptionsMapper(), Logger.withTag("test"), settings)
AlarmPreferencesImpl(settings)

@Test
fun oldMathAcknowledgementShowsLaterFeatures() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import com.timilehinaregbesola.mathalarm.presentation.alarmlist.AlarmListViewMod
import com.timilehinaregbesola.mathalarm.presentation.alarmmath.AlarmMathViewModel
import com.timilehinaregbesola.mathalarm.presentation.alarmsettings.AlarmSettingsViewModel
import com.timilehinaregbesola.mathalarm.presentation.appsettings.AlarmPreferencesImpl
import com.timilehinaregbesola.mathalarm.presentation.appsettings.AppThemeOptionsMapper
import com.timilehinaregbesola.mathalarm.provider.AlarmTimeCalculator
import com.timilehinaregbesola.mathalarm.provider.AlarmTimeCalculatorImpl
import com.timilehinaregbesola.mathalarm.provider.DateTimeProvider
Expand Down Expand Up @@ -47,7 +46,6 @@ import org.koin.dsl.module
val commonModule = module {
// Mappers
single { AlarmMapper() }
single { AppThemeOptionsMapper() }

// Coroutine Scope - replaces GlobalScope usage
single { AppCoroutineScope(kotlinx.coroutines.Dispatchers.Main.immediate) }
Expand All @@ -70,8 +68,6 @@ val commonModule = module {
single { ReviewEligibilityStore(get()) }
single {
AlarmPreferencesImpl(
get<AppThemeOptionsMapper>(),
getWith("AlarmPreferencesImpl"),
get<Settings>()
)
}
Expand Down Expand Up @@ -105,7 +101,6 @@ val commonModule = module {
}, onCleanupFailure = cleanupFailure,
onCleanupSuccess = { com.timilehinaregbesola.mathalarm.application.AlarmApplicationStatus.clearRecoveryFailure(it) }),
rescheduleFutureAlarms = rescheduleFutureAlarms,
scheduleNextAlarm = get(),
showAlarm = ShowAlarm(get(), get(), get()),
snoozeAlarm = SnoozeAlarm(get(), get(), get(), get(), onSnoozed = {
runCatching { analytics.trackSafely(AnalyticsEvents.alarmSnoozed) }
Expand All @@ -122,7 +117,7 @@ val commonModule = module {
single { ChallengeProgressStore(get()) }
viewModel { AlarmSettingsViewModel(get(), get(), get()) }
single { com.timilehinaregbesola.mathalarm.application.ChallengeCoordinator(get(), get(), getWith("ChallengeCoordinator"), get(), get(), { com.timilehinaregbesola.mathalarm.platform.getDefaultAlarmTone() }, { com.timilehinaregbesola.mathalarm.platform.PlatformPreviewVibration() }) }
viewModel { AlarmMathViewModel(get(), get(), getWith("AlarmMathViewModel"), get(), get(), get()) }
viewModel { AlarmMathViewModel(get()) }
viewModel { com.timilehinaregbesola.mathalarm.presentation.appsettings.AppSettingsViewModel(get<AlarmPreferencesImpl>(),
listOf("math-challenges-v1") + (if (com.timilehinaregbesola.mathalarm.platform.supportsSkipNext()) listOf("skip-next-alarm-v1") else emptyList()) + listOf("snooze-settings-v1")) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ class RoomAlarmDataSource(
return alarmDao.addAlarm(mapper.mapFromDomainModel(alarm))
}

override suspend fun deleteAlarm(alarm: Alarm) =
alarmDao.deleteAlarm(mapper.mapFromDomainModel(alarm))

override suspend fun deleteAlarmFromId(id: Long) {
alarmDao.deleteAlarmWithId(id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import com.timilehinaregbesola.mathalarm.usecases.FindAlarm
import com.timilehinaregbesola.mathalarm.usecases.GetSavedAlarms
import com.timilehinaregbesola.mathalarm.usecases.RescheduleFutureAlarms
import com.timilehinaregbesola.mathalarm.usecases.ScheduleAlarm
import com.timilehinaregbesola.mathalarm.usecases.ScheduleNextAlarm
import com.timilehinaregbesola.mathalarm.usecases.ShowAlarm
import com.timilehinaregbesola.mathalarm.usecases.SkipNextAlarm
import com.timilehinaregbesola.mathalarm.usecases.SnoozeAlarm
Expand All @@ -34,7 +33,6 @@ data class Usecases(
val scheduleAlarm: ScheduleAlarm,
val completeAlarm: CompleteAlarm,
val rescheduleFutureAlarms: RescheduleFutureAlarms,
val scheduleNextAlarm: ScheduleNextAlarm,
val showAlarm: ShowAlarm,
val snoozeAlarm: SnoozeAlarm,
val cancelAlarm: CancelAlarm,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ interface AlarmDao {
@Update
suspend fun updateAlarm(alarm: AlarmEntity)

@Delete
suspend fun deleteAlarm(alarm: AlarmEntity)

@Query("DELETE FROM alarms WHERE alarmid = :id")
suspend fun deleteAlarmWithId(id: Long?)

Expand All @@ -25,18 +22,12 @@ interface AlarmDao {
@Query("SELECT * FROM alarms WHERE alarmid = :alarmUid LIMIT 1")
suspend fun getAlarm(alarmUid: Long?): AlarmEntity?

@Query("SELECT * FROM alarms WHERE alarmid = :alarmUid LIMIT 1")
suspend fun search(alarmUid: Long?): AlarmEntity?

@Query("SELECT * FROM alarms ORDER BY alarmid DESC LIMIT 1")
suspend fun getLastAlarm(): AlarmEntity?

@Query("SELECT * FROM alarms ORDER BY alarmid DESC")
fun getAlarms(): Flow<List<AlarmEntity>>

@Query("SELECT * FROM alarms WHERE ison = :state")
suspend fun getActiveAlarms(state: Boolean = true): List<AlarmEntity>

@Query("SELECT * FROM alarms WHERE isSaved = :state ORDER BY alarmid DESC")
fun getSavedAlarms(state: Boolean = true): Flow<List<AlarmEntity>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,4 @@ class AlarmMapper : DomainMapper<AlarmEntity, Alarm> {
return initial.map { mapToDomainModel(it) }
}

fun fromDomainList(initial: List<Alarm>): List<AlarmEntity> {
return initial.map { mapFromDomainModel(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ object SharedFeatures {
private val challenges = mutableMapOf<String, AlarmMathViewModel>()
fun list(): AlarmListViewModel = KoinPlatform.getKoin().get()
fun settings(): AppSettingsViewModel = KoinPlatform.getKoin().get()
fun newEditor(sessionId: String): AlarmSettingsViewModel = editor(sessionId, Alarm())
fun newEditor(sessionId: String): AlarmSettingsViewModel = editor(sessionId, Alarm.newAlarm())
fun editor(sessionId: String, alarm: Alarm): AlarmSettingsViewModel {
require(sessionId.isNotBlank())
return editors[sessionId]?.takeUnless { it.isClosed } ?: KoinPlatform.getKoin().get<AlarmSettingsViewModel>().also {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class AlarmListViewModel internal constructor(
if (isClosed) return
when (event) {
is AlarmListEvent.OnEditAlarmClick -> sendUiEvent(Navigate(event.alarm))
is AlarmListEvent.OnAddAlarmClick -> sendUiEvent(Navigate(Alarm()))
is AlarmListEvent.OnAddAlarmClick -> sendUiEvent(Navigate(Alarm.newAlarm()))
is AlarmListEvent.OnAlarmOnChange -> setEnabled(event.alarm, event.isOn)
is AlarmListEvent.OnSkipNextClick -> launchCommand {
if (!skipNextSupported) return@launchCommand
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package com.timilehinaregbesola.mathalarm.presentation.alarmmath

import co.touchlab.kermit.Logger
import com.timilehinaregbesola.mathalarm.analytics.AnalyticsTracker
import com.timilehinaregbesola.mathalarm.analytics.NoopAnalyticsTracker
import com.timilehinaregbesola.mathalarm.application.ChallengeCoordinator
import com.timilehinaregbesola.mathalarm.application.ChallengeSession
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
import com.timilehinaregbesola.mathalarm.framework.Usecases
import com.timilehinaregbesola.mathalarm.interactors.AudioPlayer
import com.timilehinaregbesola.mathalarm.presentation.SharedFeatureViewModel
import com.rickclephas.kmp.observableviewmodel.MutableStateFlow
import com.rickclephas.kmp.observableviewmodel.coroutineScope
Expand All @@ -19,12 +14,7 @@ import kotlinx.coroutines.flow.asStateFlow

/** Observation adapter over the application session; no durable work belongs to its scope. */
class AlarmMathViewModel internal constructor(
usecases: Usecases,
audioPlayer: AudioPlayer,
logger: Logger,
progressStore: ChallengeProgressStore,
analytics: AnalyticsTracker = NoopAnalyticsTracker,
private val coordinator: ChallengeCoordinator = ChallengeCoordinator(usecases, audioPlayer, logger, progressStore, analytics),
private val coordinator: ChallengeCoordinator,
) : SharedFeatureViewModel() {
private val mutable = MutableStateFlow(viewModelScope, ChallengeState())
@NativeCoroutinesState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@ class AlarmSettingsViewModel internal constructor(
else -> AlarmEditorValidation.NONE
}
val dirty = initialDraft?.let { initial ->
createAlarm().copy(newDateTime = initial.newDateTime,
newHour = initial.newHour, newMinute = initial.newMinute) != initial
createAlarm() != initial
} ?: false
mutableState.update { it.copy(validation = validation, hasUnsavedChanges = dirty) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@ interface AlarmPreferences {
*/
fun loadAlarmSortOrder(): AlarmSortOrder

enum class Theme {
LIGHT,
DARK,
SYSTEM
enum class Theme(val storageId: Int) {
LIGHT(0),
DARK(1),
SYSTEM(2);

companion object {
fun fromStorageId(id: Int): Theme = entries.firstOrNull { it.storageId == id } ?: SYSTEM
}
}

enum class AlarmSortOrder {
Expand Down
Loading
Loading