mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add workaround for session duration change logic bug
This commit is contained in:
parent
c80690f76e
commit
40b45457d4
6 changed files with 84 additions and 54 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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")
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue