Improve time counting with nested categories

This commit is contained in:
Jonas Lochmann 2020-01-27 01:00:00 +01:00
parent 192b871ae5
commit a81bc687fb
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
9 changed files with 325 additions and 306 deletions

View file

@ -69,9 +69,6 @@ abstract class UsedTimeDao {
@Query("SELECT * FROM used_time WHERE category_id = :categoryId ORDER BY day_of_epoch DESC") @Query("SELECT * FROM used_time WHERE category_id = :categoryId ORDER BY day_of_epoch DESC")
abstract fun getUsedTimesByCategoryId(categoryId: String): DataSource.Factory<Int, UsedTimeItem> abstract fun getUsedTimesByCategoryId(categoryId: String): DataSource.Factory<Int, UsedTimeItem>
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
abstract fun getUsedTimesByCategoryIdAndDayOfEpoch(categoryId: String, dayOfEpoch: Int): LiveData<UsedTimeItem?>
@Query("SELECT * FROM used_time WHERE category_id IN (:categoryIds) AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch") @Query("SELECT * FROM used_time WHERE category_id IN (:categoryIds) AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>> abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>

View file

@ -109,8 +109,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
private val shouldDoAutomaticSignOut = cache.shouldDoAutomaticSignOut private val shouldDoAutomaticSignOut = cache.shouldDoAutomaticSignOut
private val liveDataCaches = cache.liveDataCaches private val liveDataCaches = cache.liveDataCaches
private var usedTimeUpdateHelperForegroundApp: UsedTimeItemBatchUpdateHelper? = null private var usedTimeUpdateHelper: UsedTimeUpdateHelper? = null
private var usedTimeUpdateHelperBackgroundPlayback: UsedTimeItemBatchUpdateHelper? = null
private var previousMainLogicExecutionTime = 0 private var previousMainLogicExecutionTime = 0
private var previousMainLoopEndTime = 0L private var previousMainLoopEndTime = 0L
private val dayChangeTracker = DayChangeTracker( private val dayChangeTracker = DayChangeTracker(
@ -140,8 +139,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
val audioPlaybackHandling = BackgroundTaskRestrictionLogicResult() val audioPlaybackHandling = BackgroundTaskRestrictionLogicResult()
private suspend fun commitUsedTimeUpdaters() { private suspend fun commitUsedTimeUpdaters() {
usedTimeUpdateHelperForegroundApp?.commit(appLogic) usedTimeUpdateHelper?.forceCommit(appLogic)
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic)
} }
private suspend fun backgroundServiceLoop() { private suspend fun backgroundServiceLoop() {
@ -281,84 +279,55 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
result = audioPlaybackHandling result = audioPlaybackHandling
) )
// update used time helper if date does not match
if (usedTimeUpdateHelper?.date != nowDate) {
usedTimeUpdateHelper?.forceCommit(appLogic)
usedTimeUpdateHelper = UsedTimeUpdateHelper(nowDate)
}
val usedTimeUpdateHelper = usedTimeUpdateHelper!!
// check times // check times
fun buildUsedTimesSparseArray(items: SparseArray<UsedTimeItem>, categoryId: String): SparseLongArray { fun buildUsedTimesSparseArray(items: SparseArray<UsedTimeItem>, categoryId: String): SparseLongArray {
val a = usedTimeUpdateHelperForegroundApp
val b = usedTimeUpdateHelperBackgroundPlayback
val result = SparseLongArray() val result = SparseLongArray()
for (i in 0..6) { for (i in 0..6) {
val usedTimesItem = items[i]?.usedMillis val usedTimesItem = items[i]?.usedMillis ?: 0
val timeToAddButNotCommited = usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0
if (a?.date?.dayOfWeek == i && a.childCategoryId == categoryId) { result.put(i, usedTimesItem + timeToAddButNotCommited)
result.put(i, a.getTotalUsedTimeChild())
} else if (a?.date?.dayOfWeek == i && a.parentCategoryId == categoryId) {
result.put(i, a.getTotalUsedTimeParent())
} else if (b?.date?.dayOfWeek == i && b.childCategoryId == categoryId) {
result.put(i, b.getTotalUsedTimeChild())
} else if (b?.date?.dayOfWeek == i && b.parentCategoryId == categoryId) {
result.put(i, b.getTotalUsedTimeParent())
} else {
result.put(i, usedTimesItem ?: 0)
}
} }
return result return result
} }
fun getCachedExtraTimeToSubstract(categoryId: String): Int { suspend fun getRemainingTime(categoryId: String?): RemainingTime? {
val a = usedTimeUpdateHelperForegroundApp
val b = usedTimeUpdateHelperBackgroundPlayback
if (a?.childCategoryId == categoryId || a?.parentCategoryId == categoryId) {
return a.getCachedExtraTimeToSubtract()
}
if (b?.childCategoryId == categoryId || b?.parentCategoryId == categoryId) {
return b.getCachedExtraTimeToSubtract()
}
return 0
}
suspend fun getRemainingTime(categoryId: String?, parentCategoryId: String?): RemainingTime? {
categoryId ?: return null categoryId ?: return null
val category = categories.find { it.id == categoryId } ?: return null val category = categories.find { it.id == categoryId } ?: return null
val parentCategory = categories.find { it.id == parentCategoryId }
val rules = timeLimitRules.get(category.id).waitForNonNullValue() val rules = timeLimitRules.get(category.id).waitForNonNullValue()
val parentRules = parentCategory?.let {
timeLimitRules.get(it.id).waitForNonNullValue() if (rules.isEmpty()) {
} ?: emptyList() return null
}
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue() val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
val parentUsedTimes = parentCategory?.let {
usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(it.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
} ?: SparseArray()
val remainingChild = RemainingTime.getRemainingTime( return RemainingTime.getRemainingTime(
nowDate.dayOfWeek, nowDate.dayOfWeek,
buildUsedTimesSparseArray(usedTimes, categoryId), buildUsedTimesSparseArray(usedTimes, categoryId),
rules, rules,
Math.max(0, category.extraTimeInMillis - getCachedExtraTimeToSubstract(category.id)) Math.max(0, category.extraTimeInMillis - (usedTimeUpdateHelper.extraTimeToSubtract.get(categoryId) ?: 0))
) )
val remainingParent = parentCategory?.let {
RemainingTime.getRemainingTime(
nowDate.dayOfWeek,
buildUsedTimesSparseArray(parentUsedTimes, parentCategory.id),
parentRules,
Math.max(0, parentCategory.extraTimeInMillis - getCachedExtraTimeToSubstract(parentCategory.id))
)
}
return RemainingTime.min(remainingChild, remainingParent)
} }
val remainingTimeForegroundApp = getRemainingTime(foregroundAppHandling.categoryId, foregroundAppHandling.parentCategoryId) val remainingTimeForegroundAppChild = getRemainingTime(foregroundAppHandling.categoryId)
val remainingTimeBackgroundApp = getRemainingTime(audioPlaybackHandling.categoryId, audioPlaybackHandling.parentCategoryId) val remainingTimeForegroundAppParent = getRemainingTime(foregroundAppHandling.parentCategoryId)
val remainingTimeForegroundApp = RemainingTime.min(remainingTimeForegroundAppChild, remainingTimeForegroundAppParent)
val remainingTimeBackgroundAppChild = getRemainingTime(audioPlaybackHandling.categoryId)
val remainingTimeBackgroundAppParent = getRemainingTime(audioPlaybackHandling.parentCategoryId)
val remainingTimeBackgroundApp = RemainingTime.min(remainingTimeBackgroundAppChild, remainingTimeBackgroundAppParent)
// eventually block // eventually block
if (remainingTimeForegroundApp?.hasRemainingTime == false) { if (remainingTimeForegroundApp?.hasRemainingTime == false) {
@ -370,126 +339,75 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
// update times // update times
suspend fun getUsedTimeItem(categoryId: String?): UsedTimeItem? { val timeToSubtract = Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND)
categoryId ?: return null
return cache.usedTimesOfCategoryAndDayOfEpoch.get(categoryId to nowDate.dayOfEpoch).waitForNullableValue()
}
val shouldCountForegroundApp = remainingTimeForegroundApp != null && isScreenOn && remainingTimeForegroundApp.hasRemainingTime val shouldCountForegroundApp = remainingTimeForegroundApp != null && isScreenOn && remainingTimeForegroundApp.hasRemainingTime
val shouldCountBackgroundApp = remainingTimeBackgroundApp != null && remainingTimeBackgroundApp.hasRemainingTime val shouldCountBackgroundApp = remainingTimeBackgroundApp != null && remainingTimeBackgroundApp.hasRemainingTime
val countTwoTypes = shouldCountForegroundApp && shouldCountBackgroundApp
val doCategoriesMatch = (
foregroundAppHandling.categoryId != null && (
foregroundAppHandling.categoryId == audioPlaybackHandling.categoryId ||
foregroundAppHandling.categoryId == audioPlaybackHandling.parentCategoryId
)
) || (
foregroundAppHandling.parentCategoryId != null && (
foregroundAppHandling.parentCategoryId == audioPlaybackHandling.categoryId ||
foregroundAppHandling.parentCategoryId == audioPlaybackHandling.parentCategoryId
)
)
val useSingleCounter = countTwoTypes && doCategoriesMatch
if (useSingleCounter) { val categoriesToCount = mutableSetOf<String>()
val isBackgroundCategoryHigher = audioPlaybackHandling.categoryId != null && val categoriesToCountExtraTime = mutableSetOf<String>()
audioPlaybackHandling.categoryId == foregroundAppHandling.parentCategoryId
if (usedTimeUpdateHelperForegroundApp !== usedTimeUpdateHelperBackgroundPlayback) { if (shouldCountForegroundApp) {
if (isBackgroundCategoryHigher) { remainingTimeForegroundAppChild?.let { remainingTime ->
usedTimeUpdateHelperForegroundApp?.commit(appLogic) foregroundAppHandling.categoryId?.let { categoryId ->
usedTimeUpdateHelperForegroundApp = null categoriesToCount.add(categoryId)
} else {
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic) if (remainingTime.usingExtraTime) {
usedTimeUpdateHelperBackgroundPlayback = null categoriesToCountExtraTime.add(categoryId)
}
} }
} }
if (isBackgroundCategoryHigher) { remainingTimeForegroundAppParent?.let { remainingTime ->
usedTimeUpdateHelperBackgroundPlayback = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance( foregroundAppHandling.parentCategoryId?.let {
date = nowDate, categoriesToCount.add(it)
childCategoryId = audioPlaybackHandling.categoryId!!,
parentCategoryId = audioPlaybackHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperBackgroundPlayback,
usedTimeItemForDayChild = getUsedTimeItem(audioPlaybackHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(audioPlaybackHandling.parentCategoryId),
logic = appLogic
)
usedTimeUpdateHelperForegroundApp = usedTimeUpdateHelperBackgroundPlayback if (remainingTime.usingExtraTime) {
} else { categoriesToCountExtraTime.add(it)
usedTimeUpdateHelperForegroundApp = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance( }
date = nowDate, }
childCategoryId = foregroundAppHandling.categoryId!!,
parentCategoryId = foregroundAppHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperForegroundApp,
usedTimeItemForDayChild = getUsedTimeItem(foregroundAppHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(foregroundAppHandling.parentCategoryId),
logic = appLogic
)
usedTimeUpdateHelperBackgroundPlayback = usedTimeUpdateHelperForegroundApp
}
} else {
if (shouldCountForegroundApp) {
usedTimeUpdateHelperForegroundApp = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
date = nowDate,
childCategoryId = foregroundAppHandling.categoryId!!,
parentCategoryId = foregroundAppHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperForegroundApp,
usedTimeItemForDayChild = getUsedTimeItem(foregroundAppHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(foregroundAppHandling.parentCategoryId),
logic = appLogic
)
} else {
usedTimeUpdateHelperForegroundApp?.commit(appLogic)
usedTimeUpdateHelperForegroundApp = null
}
if (shouldCountBackgroundApp) {
usedTimeUpdateHelperBackgroundPlayback = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
date = nowDate,
childCategoryId = audioPlaybackHandling.categoryId!!,
parentCategoryId = audioPlaybackHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperBackgroundPlayback,
usedTimeItemForDayChild = getUsedTimeItem(audioPlaybackHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(audioPlaybackHandling.parentCategoryId),
logic = appLogic
)
} else {
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic)
usedTimeUpdateHelperBackgroundPlayback = null
} }
} }
// count times if (shouldCountBackgroundApp) {
// never save more than a second of used time remainingTimeBackgroundAppChild?.let { remainingTime ->
// FIXME: currently, this uses extra time if the parent or the child category needs it and it subtracts it from both audioPlaybackHandling.categoryId?.let {
val timeToSubtract = Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND) categoriesToCount.add(it)
if (usedTimeUpdateHelperForegroundApp === usedTimeUpdateHelperBackgroundPlayback) { if (remainingTime.usingExtraTime) {
usedTimeUpdateHelperForegroundApp?.addUsedTime( categoriesToCountExtraTime.add(it)
time = timeToSubtract, }
subtractExtraTime = remainingTimeForegroundApp!!.usingExtraTime or remainingTimeBackgroundApp!!.usingExtraTime, }
appLogic = appLogic }
)
} else {
usedTimeUpdateHelperForegroundApp?.addUsedTime(
time = timeToSubtract,
subtractExtraTime = remainingTimeForegroundApp!!.usingExtraTime,
appLogic = appLogic
)
usedTimeUpdateHelperBackgroundPlayback?.addUsedTime( remainingTimeBackgroundAppParent?.let { remainingTime ->
time = timeToSubtract, audioPlaybackHandling.parentCategoryId?.let {
subtractExtraTime = remainingTimeBackgroundApp!!.usingExtraTime, categoriesToCount.add(it)
appLogic = appLogic
) if (remainingTime.usingExtraTime) {
categoriesToCountExtraTime.add(it)
}
}
}
}
if (categoriesToCount.isNotEmpty()) {
categoriesToCount.forEach { categoryId ->
usedTimeUpdateHelper.add(
categoryId = categoryId,
time = timeToSubtract,
includingExtraTime = categoriesToCountExtraTime.contains(categoryId)
)
}
}
usedTimeUpdateHelper.reportCurrentCategories(categoriesToCount)
if (usedTimeUpdateHelper.shouldDoAutoCommit) {
usedTimeUpdateHelper.forceCommit(appLogic)
} }
// trigger time warnings // trigger time warnings
// FIXME: this uses the resulting time, not the time per category
fun eventuallyTriggerTimeWarning(remaining: RemainingTime, categoryId: String?) { fun eventuallyTriggerTimeWarning(remaining: RemainingTime, categoryId: String?) {
val category = categories.find { it.id == categoryId } ?: return val category = categories.find { it.id == categoryId } ?: return
val oldRemainingTime = remaining.includingExtraTime val oldRemainingTime = remaining.includingExtraTime
@ -509,13 +427,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
} }
if (remainingTimeForegroundApp != null) { remainingTimeForegroundAppChild?.let { eventuallyTriggerTimeWarning(it, foregroundAppHandling.categoryId) }
eventuallyTriggerTimeWarning(remainingTimeForegroundApp, foregroundAppHandling.categoryId) remainingTimeForegroundAppParent?.let { eventuallyTriggerTimeWarning(it, foregroundAppHandling.parentCategoryId) }
} remainingTimeBackgroundAppChild?.let { eventuallyTriggerTimeWarning(it, audioPlaybackHandling.categoryId) }
remainingTimeBackgroundAppParent?.let { eventuallyTriggerTimeWarning(it, audioPlaybackHandling.parentCategoryId) }
if (remainingTimeBackgroundApp != null) {
eventuallyTriggerTimeWarning(remainingTimeBackgroundApp, foregroundAppHandling.categoryId)
}
// show notification // show notification
fun buildStatusMessageWithCurrentAppTitle( fun buildStatusMessageWithCurrentAppTitle(
@ -633,7 +548,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock) { if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock) {
openLockscreen(foregroundAppPackageName!!, foregroundAppActivityName) openLockscreen(foregroundAppPackageName!!, foregroundAppActivityName)
usedTimeUpdateHelperForegroundApp?.commit(appLogic) commitUsedTimeUpdaters()
} else { } else {
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
} }
@ -641,7 +556,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock && audioPlaybackPackageName != null) { if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock && audioPlaybackPackageName != null) {
appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName) appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic) commitUsedTimeUpdaters()
} }
} catch (ex: SecurityException) { } catch (ex: SecurityException) {
// this is handled by an other main loop (with a delay) // this is handled by an other main loop (with a delay)

View file

@ -54,11 +54,6 @@ class BackgroundTaskLogicCache (private val appLogic: AppLogic) {
return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second) return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second)
} }
} }
val usedTimesOfCategoryAndDayOfEpoch = object: MultiKeyLiveDataCache<UsedTimeItem?, Pair<String, Int>>() {
override fun createValue(key: Pair<String, Int>): LiveData<UsedTimeItem?> {
return appLogic.database.usedTimes().getUsedTimesByCategoryIdAndDayOfEpoch(key.first, key.second)
}
}
val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()} val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
val liveDataCaches = LiveDataCaches(arrayOf( val liveDataCaches = LiveDataCaches(arrayOf(
@ -68,7 +63,6 @@ class BackgroundTaskLogicCache (private val appLogic: AppLogic) {
appCategories, appCategories,
timeLimitRules, timeLimitRules,
usedTimesOfCategoryAndWeekByFirstDayOfWeek, usedTimesOfCategoryAndWeekByFirstDayOfWeek,
usedTimesOfCategoryAndDayOfEpoch,
shouldDoAutomaticSignOut shouldDoAutomaticSignOut
)) ))
} }

View file

@ -1,123 +0,0 @@
/*
* TimeLimit Copyright <C> 2019 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
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.logic
import io.timelimit.android.data.Database
import io.timelimit.android.data.model.UsedTimeItem
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.livedata.waitForNullableValue
import io.timelimit.android.sync.actions.AddUsedTimeAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
class UsedTimeItemBatchUpdateHelper(
val date: DateInTimezone,
val childCategoryId: String,
val parentCategoryId: String?,
var cachedItemChild: UsedTimeItem?,
var cachedItemParent: UsedTimeItem?
) {
companion object {
suspend fun eventuallyUpdateInstance(
date: DateInTimezone,
childCategoryId: String,
parentCategoryId: String?,
oldInstance: UsedTimeItemBatchUpdateHelper?,
usedTimeItemForDayChild: UsedTimeItem?,
usedTimeItemForDayParent: UsedTimeItem?,
logic: AppLogic
): UsedTimeItemBatchUpdateHelper {
if (
oldInstance != null &&
oldInstance.date == date &&
oldInstance.childCategoryId == childCategoryId &&
oldInstance.parentCategoryId == parentCategoryId
) {
if (oldInstance.cachedItemChild != usedTimeItemForDayChild) {
oldInstance.cachedItemChild = usedTimeItemForDayChild
}
if (oldInstance.cachedItemParent != usedTimeItemForDayParent) {
oldInstance.cachedItemParent = usedTimeItemForDayParent
}
return oldInstance
} else {
if (oldInstance != null) {
oldInstance.commit(logic)
}
return UsedTimeItemBatchUpdateHelper(
date = date,
childCategoryId = childCategoryId,
parentCategoryId = parentCategoryId,
cachedItemChild = usedTimeItemForDayChild,
cachedItemParent = usedTimeItemForDayParent
)
}
}
}
private var timeToAdd = 0
private var extraTimeToSubtract = 0
suspend fun addUsedTime(time: Int, subtractExtraTime: Boolean, appLogic: AppLogic) {
timeToAdd += time
if (subtractExtraTime) {
extraTimeToSubtract += time
}
if (Math.max(timeToAdd, extraTimeToSubtract) > 1000 * 10 /* 10 seconds */) {
commit(appLogic)
}
}
fun getTotalUsedTimeChild(): Long = (cachedItemChild?.usedMillis ?: 0) + timeToAdd
fun getTotalUsedTimeParent(): Long = (cachedItemParent?.usedMillis ?: 0) + timeToAdd
fun getCachedExtraTimeToSubtract(): Int {
return extraTimeToSubtract
}
suspend fun queryCurrentStatusFromDatabase(database: Database) {
cachedItemChild = database.usedTimes().getUsedTimeItem(childCategoryId, date.dayOfEpoch).waitForNullableValue()
cachedItemParent = parentCategoryId?.let {
database.usedTimes().getUsedTimeItem(parentCategoryId, date.dayOfEpoch).waitForNullableValue()
}
}
suspend fun commit(logic: AppLogic) {
if (timeToAdd == 0) {
// do nothing
} else {
ApplyActionUtil.applyAppLogicAction(
action = AddUsedTimeAction(
categoryId = childCategoryId,
timeToAdd = timeToAdd,
dayOfEpoch = date.dayOfEpoch,
extraTimeToSubtract = extraTimeToSubtract
),
appLogic = logic,
ignoreIfDeviceIsNotConfigured = true
)
timeToAdd = 0
extraTimeToSubtract = 0
queryCurrentStatusFromDatabase(logic.database)
}
}
}

View file

@ -0,0 +1,83 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 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
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.logic
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.sync.actions.AddUsedTimeActionItem
import io.timelimit.android.sync.actions.AddUsedTimeActionVersion2
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
class UsedTimeUpdateHelper (val date: DateInTimezone) {
val timeToAdd = mutableMapOf<String, Int>()
val extraTimeToSubtract = mutableMapOf<String, Int>()
var shouldDoAutoCommit = false
suspend fun add(categoryId: String, time: Int, includingExtraTime: Boolean) {
if (time < 0) {
throw IllegalArgumentException()
}
if (time == 0) {
return
}
timeToAdd[categoryId] = (timeToAdd[categoryId] ?: 0) + time
if (includingExtraTime) {
extraTimeToSubtract[categoryId] = (extraTimeToSubtract[categoryId] ?: 0) + time
}
if (timeToAdd[categoryId]!! >= 1000 * 10) {
shouldDoAutoCommit = true
}
}
fun reportCurrentCategories(categories: Set<String>) {
if (!categories.containsAll(timeToAdd.keys)) {
shouldDoAutoCommit = true
}
if (!categories.containsAll(extraTimeToSubtract.keys)) {
shouldDoAutoCommit = true
}
}
suspend fun forceCommit(appLogic: AppLogic) {
if (timeToAdd.isEmpty() && extraTimeToSubtract.isEmpty()) {
return
}
val categoryIds = timeToAdd.keys + extraTimeToSubtract.keys
ApplyActionUtil.applyAppLogicAction(
action = AddUsedTimeActionVersion2(
dayOfEpoch = date.dayOfEpoch,
items = categoryIds.map { categoryId ->
AddUsedTimeActionItem(
categoryId = categoryId,
timeToAdd = timeToAdd[categoryId] ?: 0,
extraTimeToSubtract = extraTimeToSubtract[categoryId] ?: 0
)
}
),
appLogic = appLogic,
ignoreIfDeviceIsNotConfigured = true
)
timeToAdd.clear()
extraTimeToSubtract.clear()
}
}

View file

@ -142,6 +142,82 @@ data class AddUsedTimeAction(val categoryId: String, val dayOfEpoch: Int, val ti
} }
} }
data class AddUsedTimeActionVersion2(val dayOfEpoch: Int, val items: List<AddUsedTimeActionItem>): AppLogicAction() {
companion object {
const val TYPE_VALUE = "ADD_USED_TIME_V2"
private const val DAY_OF_EPOCH = "d"
private const val ITEMS = "i"
fun parse(action: JSONObject): AddUsedTimeActionVersion2 = AddUsedTimeActionVersion2(
dayOfEpoch = action.getInt(DAY_OF_EPOCH),
items = ParseUtils.readObjectArray(action.getJSONArray(ITEMS)).map { AddUsedTimeActionItem.parse(it) }
)
}
init {
if (dayOfEpoch < 0) {
throw IllegalArgumentException()
}
if (items.isEmpty()) {
throw IllegalArgumentException()
}
if (items.distinctBy { it.categoryId }.size != items.size) {
throw IllegalArgumentException()
}
}
override fun serialize(writer: JsonWriter) {
writer.beginObject()
writer.name(TYPE).value(TYPE_VALUE)
writer.name(DAY_OF_EPOCH).value(dayOfEpoch)
writer.name(ITEMS).beginArray()
items.forEach { it.serialize(writer) }
writer.endArray()
writer.endObject()
}
}
data class AddUsedTimeActionItem(val categoryId: String, val timeToAdd: Int, val extraTimeToSubtract: Int) {
companion object {
private const val CATEGORY_ID = "categoryId"
private const val TIME_TO_ADD = "tta"
private const val EXTRA_TIME_TO_SUBTRACT = "etts"
fun parse(item: JSONObject): AddUsedTimeActionItem = AddUsedTimeActionItem(
categoryId = item.getString(CATEGORY_ID),
timeToAdd = item.getInt(TIME_TO_ADD),
extraTimeToSubtract = item.getInt(EXTRA_TIME_TO_SUBTRACT)
)
}
init {
IdGenerator.assertIdValid(categoryId)
if (timeToAdd < 0) {
throw IllegalArgumentException()
}
if (extraTimeToSubtract < 0) {
throw IllegalArgumentException()
}
}
fun serialize(writer: JsonWriter) {
writer.beginObject()
writer.name(CATEGORY_ID).value(categoryId)
writer.name(TIME_TO_ADD).value(timeToAdd)
writer.name(EXTRA_TIME_TO_SUBTRACT).value(extraTimeToSubtract)
writer.endObject()
}
}
// data class ClearTemporarilyAllowedAppsAction(val deviceId: String): AppLogicAction(), LocalOnlyAction // data class ClearTemporarilyAllowedAppsAction(val deviceId: String): AppLogicAction(), LocalOnlyAction
data class InstalledApp(val packageName: String, val title: String, val isLaunchable: Boolean, val recommendation: AppRecommendation) { data class InstalledApp(val packageName: String, val title: String, val isLaunchable: Boolean, val recommendation: AppRecommendation) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2020 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
@ -26,6 +26,7 @@ object ActionParser {
SignOutAtDeviceAction.TYPE_VALUE -> SignOutAtDeviceAction SignOutAtDeviceAction.TYPE_VALUE -> SignOutAtDeviceAction
UpdateAppActivitiesAction.TYPE_VALUE -> UpdateAppActivitiesAction.parse(action) UpdateAppActivitiesAction.TYPE_VALUE -> UpdateAppActivitiesAction.parse(action)
UpdateDeviceStatusAction.TYPE_VALUE -> UpdateDeviceStatusAction.parse(action) UpdateDeviceStatusAction.TYPE_VALUE -> UpdateDeviceStatusAction.parse(action)
AddUsedTimeActionVersion2.TYPE_VALUE -> AddUsedTimeActionVersion2.parse(action)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2020 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
@ -106,6 +106,51 @@ object ApplyActionUtil {
database.setTransactionSuccessful() database.setTransactionSuccessful()
syncUtil.requestVeryUnimportantSync() syncUtil.requestVeryUnimportantSync()
return@executeAndWait
}
}
} else if (action is AddUsedTimeActionVersion2) {
val previousAction = database.pendingSyncAction().getLatestUnscheduledActionSync()
if (previousAction != null && previousAction.type == PendingSyncActionType.AppLogic) {
val parsed = ActionParser.parseAppLogicAction(JSONObject(previousAction.encodedAction))
if (parsed is AddUsedTimeActionVersion2 && parsed.dayOfEpoch == action.dayOfEpoch) {
var updatedAction: AddUsedTimeActionVersion2 = parsed
action.items.forEach { newItem ->
val oldItem = updatedAction.items.find { it.categoryId == newItem.categoryId }
if (oldItem == null) {
updatedAction = updatedAction.copy(
items = updatedAction.items + listOf(newItem)
)
} else {
val mergedItem = AddUsedTimeActionItem(
timeToAdd = oldItem.timeToAdd + newItem.timeToAdd,
extraTimeToSubtract = oldItem.extraTimeToSubtract + newItem.extraTimeToSubtract,
categoryId = newItem.categoryId
)
updatedAction = updatedAction.copy(
items = updatedAction.items.filter { it.categoryId != mergedItem.categoryId } + listOf(mergedItem)
)
}
}
// update the previous action
database.pendingSyncAction().updateEncodedActionSync(
sequenceNumber = previousAction.sequenceNumber,
action = StringWriter().apply {
JsonWriter(this).apply {
updatedAction.serialize(this)
}
}.toString()
)
database.setTransactionSuccessful()
syncUtil.requestVeryUnimportantSync()
return@executeAndWait return@executeAndWait
} }
} }
@ -126,7 +171,7 @@ object ApplyActionUtil {
userId = "" userId = ""
)) ))
if (action is AddUsedTimeAction) { if (action is AddUsedTimeAction || action is AddUsedTimeActionVersion2) {
syncUtil.requestVeryUnimportantSync() syncUtil.requestVeryUnimportantSync()
} else { } else {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2020 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
@ -76,6 +76,37 @@ object LocalDatabaseAppLogicActionDispatcher {
null null
} }
is AddUsedTimeActionVersion2 -> {
action.items.forEach { item ->
database.category().getCategoryByIdSync(item.categoryId)!!
val updatedRows = database.usedTimes().addUsedTime(
categoryId = item.categoryId,
timeToAdd = item.timeToAdd,
dayOfEpoch = action.dayOfEpoch
)
if (updatedRows == 0) {
// create new entry
database.usedTimes().insertUsedTime(UsedTimeItem(
categoryId = item.categoryId,
dayOfEpoch = action.dayOfEpoch,
usedMillis = item.timeToAdd.toLong()
))
}
if (item.extraTimeToSubtract != 0) {
database.category().subtractCategoryExtraTime(
categoryId = item.categoryId,
removedExtraTime = item.extraTimeToSubtract
)
}
}
null
}
is AddInstalledAppsAction -> { is AddInstalledAppsAction -> {
database.app().addAppsSync( database.app().addAppsSync(
action.apps.map { action.apps.map {