Add workaround for session duration change logic bug

This commit is contained in:
Jonas Lochmann 2024-01-01 01:00:00 +01:00
parent c80690f76e
commit 40b45457d4
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
6 changed files with 84 additions and 54 deletions

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -35,6 +35,9 @@ interface SessionDurationDao {
@Query("SELECT * FROM session_duration WHERE category_id = :categoryId") @Query("SELECT * FROM session_duration WHERE category_id = :categoryId")
fun getSessionDurationItemsByCategoryIdSync(categoryId: String): List<SessionDuration> fun getSessionDurationItemsByCategoryIdSync(categoryId: String): List<SessionDuration>
@Query("SELECT * FROM session_duration WHERE category_id = :categoryId AND start_minute_of_day >= :startMinuteOfDay AND end_minute_of_day <= :endMinuteOfDay AND max_session_duration >= :maxSessionDuration AND session_pause_duration <= :sessionPauseDuration")
fun getFittingSessionDurationItemsSync(categoryId: String, startMinuteOfDay: Int, endMinuteOfDay: Int, maxSessionDuration: Int, sessionPauseDuration: Int): List<SessionDuration>
@Insert @Insert
fun insertSessionDurationItemSync(item: SessionDuration) fun insertSessionDurationItemSync(item: SessionDuration)

View file

@ -45,6 +45,9 @@ abstract class UsedTimeDao {
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch AND start_time_of_day = :start AND end_time_of_day = :end") @Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch AND start_time_of_day = :start AND end_time_of_day = :end")
abstract fun getUsedTimeItemSync(categoryId: String, dayOfEpoch: Int, start: Int, end: Int): UsedTimeItem? abstract fun getUsedTimeItemSync(categoryId: String, dayOfEpoch: Int, start: Int, end: Int): UsedTimeItem?
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch AND start_time_of_day >= :start AND end_time_of_day <= :end")
abstract fun getUsedTimeItemsSyncIncludingSmaller(categoryId: String, dayOfEpoch: Int, start: Int, end: Int): List<UsedTimeItem>
@Query("DELETE FROM used_time WHERE category_id = :categoryId") @Query("DELETE FROM used_time WHERE category_id = :categoryId")
abstract fun deleteUsedTimeItems(categoryId: String) abstract fun deleteUsedTimeItems(categoryId: String)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019- 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019- 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -43,15 +43,15 @@ object RemainingSessionDuration {
rule.dayMask.toInt() and (1 shl dayOfWeek) != 0 && rule.dayMask.toInt() and (1 shl dayOfWeek) != 0 &&
rule.startMinuteOfDay <= minuteOfDay && rule.endMinuteOfDay >= minuteOfDay rule.startMinuteOfDay <= minuteOfDay && rule.endMinuteOfDay >= minuteOfDay
) { ) {
val remaining = durationsOfCategory.find { val remaining = durationsOfCategory.filter {
it.startMinuteOfDay == rule.startMinuteOfDay && it.startMinuteOfDay >= rule.startMinuteOfDay &&
it.endMinuteOfDay == rule.endMinuteOfDay && it.endMinuteOfDay <= rule.endMinuteOfDay &&
it.maxSessionDuration == rule.sessionDurationMilliseconds && it.maxSessionDuration >= rule.sessionDurationMilliseconds &&
it.sessionPauseDuration == rule.sessionPauseMilliseconds && it.sessionPauseDuration <= rule.sessionPauseMilliseconds &&
it.lastUsage + it.sessionPauseDuration > timestamp it.lastUsage + rule.sessionPauseMilliseconds > timestamp
}?.let { durationItem -> }.map { durationItem ->
(durationItem.maxSessionDuration - durationItem.lastSessionDuration).coerceAtLeast(0) (durationItem.maxSessionDuration - durationItem.lastSessionDuration).coerceAtLeast(0)
} ?: rule.sessionDurationMilliseconds.toLong() }.minOrNull() ?: rule.sessionDurationMilliseconds.toLong()
result = min(result, remaining) result = min(result, remaining)
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019- 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019- 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -88,31 +88,39 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) {
private fun getRemainingTime(usedTimes: List<UsedTimeItem>, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean, firstDayOfWeekAsEpochDay: Int, dayOfWeek: Int): Long? { private fun getRemainingTime(usedTimes: List<UsedTimeItem>, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean, firstDayOfWeekAsEpochDay: Int, dayOfWeek: Int): Long? {
return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map { rule -> return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map { rule ->
var usedTime = 0L val usedTime = getUsedTime(
usedTimes = usedTimes,
usedTimes.forEach { usedTimeItem -> rule = rule,
val doesWeekMatch = usedTimeItem.dayOfEpoch >= firstDayOfWeekAsEpochDay && usedTimeItem.dayOfEpoch <= firstDayOfWeekAsEpochDay + 6 firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay,
dayOfWeekForDailyRule = if (rule.perDay) dayOfWeek else null
if (doesWeekMatch) { )
val usedTimeItemDayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeekAsEpochDay
val doesDayMaskMatch = (rule.dayMask.toInt() and (1 shl usedTimeItemDayOfWeek)) != 0
val doesCurrentDayMatch = dayOfWeek == usedTimeItemDayOfWeek
val usedTimeItemMatching = if (rule.perDay) doesCurrentDayMatch else doesDayMaskMatch
if (usedTimeItemMatching) {
if (rule.startMinuteOfDay == usedTimeItem.startTimeOfDay && rule.endMinuteOfDay == usedTimeItem.endTimeOfDay) {
usedTime += usedTimeItem.usedMillis
}
}
}
}
val maxTime = rule.maximumTimeInMillis val maxTime = rule.maximumTimeInMillis
val remaining = Math.max(0, maxTime - usedTime)
remaining (maxTime - usedTime).coerceAtLeast(0)
}.minOrNull() }.minOrNull()
} }
fun getUsedTime(usedTimes: List<UsedTimeItem>, rule: TimeLimitRule, firstDayOfWeekAsEpochDay: Int, dayOfWeekForDailyRule: Int?): Long {
val usedTimeByDay = longArrayOf(0, 0, 0, 0, 0, 0, 0)
usedTimes.forEach { usedTimeItem ->
val doesWeekMatch = usedTimeItem.dayOfEpoch >= firstDayOfWeekAsEpochDay && usedTimeItem.dayOfEpoch <= firstDayOfWeekAsEpochDay + 6
if (doesWeekMatch) {
val usedTimeItemDayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeekAsEpochDay
val doesDayMaskMatch = (rule.dayMask.toInt() and (1 shl usedTimeItemDayOfWeek)) != 0
val dayMatch = rule.perDay or doesDayMaskMatch
val hourMatch = rule.startMinuteOfDay <= usedTimeItem.startTimeOfDay && rule.endMinuteOfDay >= usedTimeItem.endTimeOfDay
if (dayMatch && hourMatch)
usedTimeByDay[usedTimeItemDayOfWeek] = usedTimeByDay[usedTimeItemDayOfWeek].coerceAtLeast(usedTimeItem.usedMillis)
}
}
return if (dayOfWeekForDailyRule == null) usedTimeByDay.sum()
else usedTimeByDay[dayOfWeekForDailyRule]
}
} }
} }

View file

@ -55,10 +55,14 @@ object LocalDatabaseAppLogicActionDispatcher {
if (updatedRows == 0) { if (updatedRows == 0) {
// create new entry // create new entry
val oldTime = database.usedTimes().getUsedTimeItemsSyncIncludingSmaller(
item.categoryId, action.dayOfEpoch, start, end
).map { it.usedMillis }.maxOrNull() ?: 0
database.usedTimes().insertUsedTime(UsedTimeItem( database.usedTimes().insertUsedTime(UsedTimeItem(
categoryId = item.categoryId, categoryId = item.categoryId,
dayOfEpoch = action.dayOfEpoch, dayOfEpoch = action.dayOfEpoch,
usedMillis = item.timeToAdd.coerceAtMost(lengthInMs).toLong(), usedMillis = (oldTime + item.timeToAdd).coerceAtMost(lengthInMs.toLong()),
startTimeOfDay = start, startTimeOfDay = start,
endTimeOfDay = end endTimeOfDay = end
)) ))
@ -80,6 +84,24 @@ object LocalDatabaseAppLogicActionDispatcher {
endMinuteOfDay = limit.endMinuteOfDay endMinuteOfDay = limit.endMinuteOfDay
) )
fun oldDuration(): Long {
val fittingDurationItems = database.sessionDuration().getFittingSessionDurationItemsSync(
categoryId = item.categoryId,
startMinuteOfDay = limit.startMinuteOfDay,
endMinuteOfDay = limit.endMinuteOfDay,
maxSessionDuration = limit.maxSessionDuration,
sessionPauseDuration = limit.sessionPauseDuration
// this ignores the last usage that is checked later
)
val fittingDurationItemsLastUsageFiltered =
if (hasTrustedTimestamp) fittingDurationItems.filter {
action.trustedTimestamp - item.timeToAdd <= it.lastUsage + it.sessionPauseDuration - BackgroundTaskLogic.EXTEND_SESSION_TOLERANCE
} else fittingDurationItems
return fittingDurationItemsLastUsageFiltered.map { it.lastSessionDuration }.maxOrNull() ?: 0
}
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "handle session duration limit $limit") Log.d(LOG_TAG, "handle session duration limit $limit")
Log.d(LOG_TAG, "timestamp: ${action.trustedTimestamp}") Log.d(LOG_TAG, "timestamp: ${action.trustedTimestamp}")
@ -117,18 +139,19 @@ object LocalDatabaseAppLogicActionDispatcher {
oldItem.copy( oldItem.copy(
lastUsage = action.trustedTimestamp.coerceAtLeast(oldItem.lastUsage), lastUsage = action.trustedTimestamp.coerceAtLeast(oldItem.lastUsage),
lastSessionDuration = if (extendSession) oldItem.lastSessionDuration + item.timeToAdd.toLong() else item.timeToAdd.toLong() lastSessionDuration = if (extendSession) oldItem.lastSessionDuration + item.timeToAdd.toLong() else oldDuration() + item.timeToAdd.toLong()
) )
} else SessionDuration( } else {
SessionDuration(
categoryId = item.categoryId, categoryId = item.categoryId,
maxSessionDuration = limit.maxSessionDuration, maxSessionDuration = limit.maxSessionDuration,
sessionPauseDuration = limit.sessionPauseDuration, sessionPauseDuration = limit.sessionPauseDuration,
startMinuteOfDay = limit.startMinuteOfDay, startMinuteOfDay = limit.startMinuteOfDay,
endMinuteOfDay = limit.endMinuteOfDay, endMinuteOfDay = limit.endMinuteOfDay,
lastSessionDuration = item.timeToAdd.toLong(), lastSessionDuration = oldDuration() + item.timeToAdd.toLong(),
// this will cause a small loss of session durations lastUsage = action.trustedTimestamp // can be zero
lastUsage = if (hasTrustedTimestamp) action.trustedTimestamp else 0 )
) }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "newItem: $newItem") Log.d(LOG_TAG, "newItem: $newItem")

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -27,6 +27,7 @@ import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.extensions.MinuteOfDay import io.timelimit.android.extensions.MinuteOfDay
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.logic.DummyApps import io.timelimit.android.logic.DummyApps
import io.timelimit.android.logic.RemainingTime
import io.timelimit.android.ui.manage.category.timelimit_rules.TimeLimitRulesHandlers import io.timelimit.android.ui.manage.category.timelimit_rules.TimeLimitRulesHandlers
import io.timelimit.android.util.DayNameUtil import io.timelimit.android.util.DayNameUtil
import io.timelimit.android.util.TimeTextUtil import io.timelimit.android.util.TimeTextUtil
@ -156,20 +157,12 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
val binding = holder.itemView.tag as FragmentCategoryTimeLimitRuleItemBinding val binding = holder.itemView.tag as FragmentCategoryTimeLimitRuleItemBinding
val context = binding.root.context val context = binding.root.context
val usedTime = date?.let { date -> val usedTime = date?.let { date ->
usedTimes.filter { usedTime -> RemainingTime.getUsedTime(
val usedTimeDayOfWeek = usedTime.dayOfEpoch - date.firstDayOfWeekAsEpochDay usedTimes = usedTimes,
val matchingWeek = usedTimeDayOfWeek in 0..6 rule = rule,
firstDayOfWeekAsEpochDay = date.firstDayOfWeekAsEpochDay,
if (matchingWeek) { dayOfWeekForDailyRule = if (rule.perDay) date.dayOfWeek else null
val matchingSlot = usedTime.startTimeOfDay == rule.startMinuteOfDay && usedTime.endTimeOfDay == rule.endMinuteOfDay ).toInt()
val maskToCheck = if (rule.perDay && rule.appliesToMultipleDays) {
rule.dayMask.takeHighestOneBit().toInt().coerceAtMost(1 shl date.dayOfWeek)
} else rule.dayMask.toInt()
val matchingMask = (maskToCheck and (1 shl usedTimeDayOfWeek) != 0)
matchingSlot && matchingMask
} else false
}.map { it.usedMillis }.sum().toInt()
} ?: 0 } ?: 0
binding.maxTimeString = rule.maximumTimeInMillis.let { time -> binding.maxTimeString = rule.maximumTimeInMillis.let { time ->