diff --git a/app/src/main/java/io/timelimit/android/data/dao/SessionDurationDao.kt b/app/src/main/java/io/timelimit/android/data/dao/SessionDurationDao.kt index c2ad8af..4a9df66 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/SessionDurationDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/SessionDurationDao.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann * * 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 @@ -35,6 +35,9 @@ interface SessionDurationDao { @Query("SELECT * FROM session_duration WHERE category_id = :categoryId") fun getSessionDurationItemsByCategoryIdSync(categoryId: String): List + @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 + @Insert fun insertSessionDurationItemSync(item: SessionDuration) diff --git a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt index 41e4a31..668191a 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt @@ -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") 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 + @Query("DELETE FROM used_time WHERE category_id = :categoryId") abstract fun deleteUsedTimeItems(categoryId: String) diff --git a/app/src/main/java/io/timelimit/android/logic/RemainingSessionDuration.kt b/app/src/main/java/io/timelimit/android/logic/RemainingSessionDuration.kt index f2dabe4..a6a130b 100644 --- a/app/src/main/java/io/timelimit/android/logic/RemainingSessionDuration.kt +++ b/app/src/main/java/io/timelimit/android/logic/RemainingSessionDuration.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019- 2020 Jonas Lochmann + * TimeLimit Copyright 2019- 2023 Jonas Lochmann * * 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 @@ -43,15 +43,15 @@ object RemainingSessionDuration { rule.dayMask.toInt() and (1 shl dayOfWeek) != 0 && rule.startMinuteOfDay <= minuteOfDay && rule.endMinuteOfDay >= minuteOfDay ) { - val remaining = durationsOfCategory.find { - it.startMinuteOfDay == rule.startMinuteOfDay && - it.endMinuteOfDay == rule.endMinuteOfDay && - it.maxSessionDuration == rule.sessionDurationMilliseconds && - it.sessionPauseDuration == rule.sessionPauseMilliseconds && - it.lastUsage + it.sessionPauseDuration > timestamp - }?.let { durationItem -> + val remaining = durationsOfCategory.filter { + it.startMinuteOfDay >= rule.startMinuteOfDay && + it.endMinuteOfDay <= rule.endMinuteOfDay && + it.maxSessionDuration >= rule.sessionDurationMilliseconds && + it.sessionPauseDuration <= rule.sessionPauseMilliseconds && + it.lastUsage + rule.sessionPauseMilliseconds > timestamp + }.map { durationItem -> (durationItem.maxSessionDuration - durationItem.lastSessionDuration).coerceAtLeast(0) - } ?: rule.sessionDurationMilliseconds.toLong() + }.minOrNull() ?: rule.sessionDurationMilliseconds.toLong() result = min(result, remaining) } diff --git a/app/src/main/java/io/timelimit/android/logic/RemainingTime.kt b/app/src/main/java/io/timelimit/android/logic/RemainingTime.kt index b5b4ba2..0847e7e 100644 --- a/app/src/main/java/io/timelimit/android/logic/RemainingTime.kt +++ b/app/src/main/java/io/timelimit/android/logic/RemainingTime.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019- 2020 Jonas Lochmann + * TimeLimit Copyright 2019- 2023 Jonas Lochmann * * 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 @@ -88,31 +88,39 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) { private fun getRemainingTime(usedTimes: List, relatedRules: List, assumeMaximalExtraTime: Boolean, firstDayOfWeekAsEpochDay: Int, dayOfWeek: Int): Long? { return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map { rule -> - var usedTime = 0L - - 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 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 usedTime = getUsedTime( + usedTimes = usedTimes, + rule = rule, + firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay, + dayOfWeekForDailyRule = if (rule.perDay) dayOfWeek else null + ) val maxTime = rule.maximumTimeInMillis - val remaining = Math.max(0, maxTime - usedTime) - remaining + (maxTime - usedTime).coerceAtLeast(0) }.minOrNull() } + + fun getUsedTime(usedTimes: List, 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] + } } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt index 5658e04..14475c6 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt @@ -55,10 +55,14 @@ object LocalDatabaseAppLogicActionDispatcher { if (updatedRows == 0) { // create new entry + val oldTime = database.usedTimes().getUsedTimeItemsSyncIncludingSmaller( + item.categoryId, action.dayOfEpoch, start, end + ).map { it.usedMillis }.maxOrNull() ?: 0 + database.usedTimes().insertUsedTime(UsedTimeItem( categoryId = item.categoryId, dayOfEpoch = action.dayOfEpoch, - usedMillis = item.timeToAdd.coerceAtMost(lengthInMs).toLong(), + usedMillis = (oldTime + item.timeToAdd).coerceAtMost(lengthInMs.toLong()), startTimeOfDay = start, endTimeOfDay = end )) @@ -80,6 +84,24 @@ object LocalDatabaseAppLogicActionDispatcher { 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) { Log.d(LOG_TAG, "handle session duration limit $limit") Log.d(LOG_TAG, "timestamp: ${action.trustedTimestamp}") @@ -117,18 +139,19 @@ object LocalDatabaseAppLogicActionDispatcher { oldItem.copy( 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, maxSessionDuration = limit.maxSessionDuration, sessionPauseDuration = limit.sessionPauseDuration, startMinuteOfDay = limit.startMinuteOfDay, endMinuteOfDay = limit.endMinuteOfDay, - lastSessionDuration = item.timeToAdd.toLong(), - // this will cause a small loss of session durations - lastUsage = if (hasTrustedTimestamp) action.trustedTimestamp else 0 - ) + lastSessionDuration = oldDuration() + item.timeToAdd.toLong(), + lastUsage = action.trustedTimestamp // can be zero + ) + } if (BuildConfig.DEBUG) { Log.d(LOG_TAG, "newItem: $newItem") diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/appsandrules/AppAndRuleAdapter.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/appsandrules/AppAndRuleAdapter.kt index ecf7fde..f064243 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/category/appsandrules/AppAndRuleAdapter.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/appsandrules/AppAndRuleAdapter.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann * * 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 @@ -27,6 +27,7 @@ import io.timelimit.android.date.DateInTimezone import io.timelimit.android.extensions.MinuteOfDay import io.timelimit.android.logic.DefaultAppLogic 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.util.DayNameUtil import io.timelimit.android.util.TimeTextUtil @@ -156,20 +157,12 @@ class AppAndRuleAdapter: RecyclerView.Adapter() { val binding = holder.itemView.tag as FragmentCategoryTimeLimitRuleItemBinding val context = binding.root.context val usedTime = date?.let { date -> - usedTimes.filter { usedTime -> - val usedTimeDayOfWeek = usedTime.dayOfEpoch - date.firstDayOfWeekAsEpochDay - val matchingWeek = usedTimeDayOfWeek in 0..6 - - if (matchingWeek) { - val matchingSlot = usedTime.startTimeOfDay == rule.startMinuteOfDay && usedTime.endTimeOfDay == rule.endMinuteOfDay - 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() + RemainingTime.getUsedTime( + usedTimes = usedTimes, + rule = rule, + firstDayOfWeekAsEpochDay = date.firstDayOfWeekAsEpochDay, + dayOfWeekForDailyRule = if (rule.perDay) date.dayOfWeek else null + ).toInt() } ?: 0 binding.maxTimeString = rule.maximumTimeInMillis.let { time ->