Fix granting extra time for another day after task completion

This commit is contained in:
Jonas Lochmann 2022-01-03 01:00:00 +01:00
parent c2e83bc11a
commit cba90b69e3
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
10 changed files with 122 additions and 23 deletions

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -45,7 +45,7 @@ interface ChildTaskDao {
@Query("SELECT child_task.* FROM child_task JOIN category ON (child_task.category_id = category.id) WHERE category.child_id = :userId")
fun getTasksByUserIdSync(userId: String): List<ChildTask>
@Query("SELECT child_task.*, category.title as category_title, user.name as child_name FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1")
@Query("SELECT child_task.*, category.title as category_title, user.name as child_name, user.timezone AS child_timezone FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1")
fun getPendingTasks(): LiveData<List<FullChildTask>>
@Query("SELECT * FROM child_task WHERE category_id = :categoryId")

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -26,5 +26,7 @@ data class FullChildTask(
@ColumnInfo(name = "category_title")
val categoryTitle: String,
@ColumnInfo(name = "child_name")
val childName: String
val childName: String,
@ColumnInfo(name = "child_timezone")
val childTimezone: String
)

View file

@ -88,6 +88,7 @@ class AppLogic(
val backgroundTaskLogic = BackgroundTaskLogic(this)
val appSetupLogic = AppSetupLogic(this)
val syncNotificationLogic = SyncNotificationLogic(this)
val serverApiLevelLogic = ServerApiLevelLogic(this)
private val isConnectedInternal = MutableLiveData<Boolean>().apply { value = false }
val isConnected = isConnectedInternal.castDown()

View file

@ -0,0 +1,43 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 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.livedata.liveDataFromNonNullValue
import io.timelimit.android.livedata.map
import io.timelimit.android.livedata.switchMap
class ServerApiLevelLogic(logic: AppLogic) {
val infoLive = logic.database.config().getDeviceAuthTokenAsync().switchMap { authToken ->
if (authToken.isEmpty())
liveDataFromNonNullValue(ServerApiLevelInfo.Offline)
else
logic.database.config().getServerApiLevelLive().map { apiLevel ->
ServerApiLevelInfo.Online(serverLevel = apiLevel)
}
}
}
sealed class ServerApiLevelInfo {
abstract fun hasLevelOrIsOffline(level: Int): Boolean
data class Online(val serverLevel: Int): ServerApiLevelInfo() {
override fun hasLevelOrIsOffline(level: Int) = serverLevel >= level
}
object Offline: ServerApiLevelInfo() {
override fun hasLevelOrIsOffline(level: Int) = true
}
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -1024,16 +1024,18 @@ data class DeleteChildTaskAction(val taskId: String): ParentAction() {
}
}
data class ReviewChildTaskAction(val taskId: String, val ok: Boolean, val time: Long): ParentAction() {
data class ReviewChildTaskAction(val taskId: String, val ok: Boolean, val time: Long, val day: Int?): ParentAction() {
companion object {
private const val TYPE_VALUE = "REVIEW_CHILD_TASK"
private const val TASK_ID = "taskId"
private const val OK = "ok"
private const val TIME = "time"
private const val DAY = "day"
}
init {
if (time <= 0) throw IllegalArgumentException()
if (day != null && day < 0) throw IllegalArgumentException()
IdGenerator.assertIdValid(taskId)
}
@ -1045,6 +1047,10 @@ data class ReviewChildTaskAction(val taskId: String, val ok: Boolean, val time:
writer.name(OK).value(ok)
writer.name(TIME).value(time)
if (day != null) {
writer.name(DAY).value(day)
}
writer.endObject()
}
}

View file

@ -841,11 +841,21 @@ object LocalDatabaseParentActionDispatcher {
if (action.ok) {
val category = database.category().getCategoryByIdSync(task.categoryId)!!
if (category.extraTimeDay != 0 && category.extraTimeInMillis > 0) {
// if the current time is daily, then extend the daily time only
database.category().updateCategoryExtraTime(categoryId = category.id, extraTimeDay = category.extraTimeDay, newExtraTime = category.extraTimeInMillis + task.extraTimeDuration)
val resetDayBoundExtraTime = category.extraTimeDay != -1 && action.day != null &&
category.extraTimeDay != action.day
if (resetDayBoundExtraTime) {
database.category().updateCategoryExtraTime(
categoryId = category.id,
extraTimeDay = -1,
newExtraTime = task.extraTimeDuration.toLong()
)
} else {
database.category().updateCategoryExtraTime(categoryId = category.id, extraTimeDay = -1, newExtraTime = category.extraTimeInMillis + task.extraTimeDuration)
database.category().updateCategoryExtraTime(
categoryId = category.id,
extraTimeDay = category.extraTimeDay,
newExtraTime = category.extraTimeInMillis + task.extraTimeDuration
)
}
database.childTasks().updateItemSync(task.copy(pendingRequest = false, lastGrantTimestamp = action.time))

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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,14 +27,17 @@ import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.CoroutineFragment
import io.timelimit.android.data.model.*
import io.timelimit.android.databinding.FragmentOverviewBinding
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.logic.ServerApiLevelInfo
import io.timelimit.android.sync.actions.ReviewChildTaskAction
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import kotlinx.coroutines.launch
import java.util.*
class OverviewFragment : CoroutineFragment() {
private val handlers: OverviewFragmentParentHandlers by lazy { parentFragment as OverviewFragmentParentHandlers }
@ -97,13 +100,17 @@ class OverviewFragment : CoroutineFragment() {
model.showMoreDevices(level)
}
override fun onTaskConfirmed(task: ChildTask, hasPremium: Boolean) {
override fun onTaskConfirmed(task: ChildTask, hasPremium: Boolean, timezone: TimeZone, serverApiLevel: ServerApiLevelInfo) {
if (hasPremium) {
val time = logic.timeApi.getCurrentTimeInMillis()
val day = DateInTimezone.newInstance(time, timezone).dayOfEpoch
auth.tryDispatchParentAction(
ReviewChildTaskAction(
taskId = task.taskId,
ok = true,
time = logic.timeApi.getCurrentTimeInMillis()
time = time,
day = if (serverApiLevel.hasLevelOrIsOffline(2)) day else null
)
)
} else RequiresPurchaseDialogFragment().show(parentFragmentManager)
@ -114,7 +121,8 @@ class OverviewFragment : CoroutineFragment() {
ReviewChildTaskAction(
taskId = task.taskId,
ok = false,
time = logic.timeApi.getCurrentTimeInMillis()
time = logic.timeApi.getCurrentTimeInMillis(),
day = null
)
)
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -26,8 +26,10 @@ import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.UserType
import io.timelimit.android.databinding.*
import io.timelimit.android.logic.ServerApiLevelInfo
import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.TimeTextUtil
import java.util.*
import kotlin.properties.Delegates
class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>() {
@ -260,7 +262,15 @@ class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>
it.lastGrant = if (item.task.lastGrantTimestamp == 0L) null else DateUtil.formatAbsoluteDate(it.root.context, item.task.lastGrantTimestamp)
it.taskTitle = item.task.taskTitle
it.yesButton.setOnClickListener { handlers?.onTaskConfirmed(task = item.task, hasPremium = item.hasPremium) }
it.yesButton.setOnClickListener {
handlers?.onTaskConfirmed(
task = item.task,
hasPremium = item.hasPremium,
timezone = item.childTimezone,
serverApiLevel = item.serverApiLevel
)
}
it.noButton.setOnClickListener { handlers?.onTaskRejected(item.task) }
it.skipButton.setOnClickListener { handlers?.onSkipTaskReviewClicked(item.task) }
}
@ -305,6 +315,6 @@ interface OverviewFragmentHandlers {
fun onShowAllUsersClicked()
fun onSetDeviceListVisibility(level: DeviceListItemVisibility)
fun onSkipTaskReviewClicked(task: ChildTask)
fun onTaskConfirmed(task: ChildTask, hasPremium: Boolean)
fun onTaskConfirmed(task: ChildTask, hasPremium: Boolean, timezone: TimeZone, serverApiLevel: ServerApiLevelInfo)
fun onTaskRejected(task: ChildTask)
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -20,6 +20,8 @@ import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.UserType
import io.timelimit.android.integration.platform.RuntimePermissionStatus
import io.timelimit.android.logic.ServerApiLevelInfo
import java.util.*
sealed class OverviewFragmentItem
object OverviewFragmentHeaderUsers: OverviewFragmentItem()
@ -41,4 +43,11 @@ sealed class ShowMoreOverviewFragmentItem: OverviewFragmentItem() {
object ShowAllUsers: ShowMoreOverviewFragmentItem()
data class ShowMoreDevices(val level: DeviceListItemVisibility): ShowMoreOverviewFragmentItem()
}
data class TaskReviewOverviewItem(val task: ChildTask, val childTitle: String, val categoryTitle: String, val hasPremium: Boolean): OverviewFragmentItem()
data class TaskReviewOverviewItem(
val task: ChildTask,
val childTitle: String,
val categoryTitle: String,
val hasPremium: Boolean,
val childTimezone: TimeZone,
val serverApiLevel: ServerApiLevelInfo
): OverviewFragmentItem()

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -26,6 +26,7 @@ import io.timelimit.android.livedata.liveDataFromFunction
import io.timelimit.android.livedata.map
import io.timelimit.android.livedata.switchMap
import io.timelimit.android.logic.DefaultAppLogic
import java.util.*
class OverviewFragmentModel(application: Application): AndroidViewModel(application) {
private val logic = DefaultAppLogic.with(application)
@ -124,9 +125,18 @@ class OverviewFragmentModel(application: Application): AndroidViewModel(applicat
}
private val hasPremiumLive = logic.fullVersion.shouldProvideFullVersionFunctions
private val pendingTaskItemLive = hasPremiumLive.switchMap { hasPremium ->
logic.serverApiLevelLogic.infoLive.switchMap { serverApiLevel ->
pendingTasksToShowLive.map { tasks ->
tasks.firstOrNull()?.let {
TaskReviewOverviewItem(task = it.childTask, childTitle = it.childName, categoryTitle = it.categoryTitle, hasPremium = hasPremium)
TaskReviewOverviewItem(
task = it.childTask,
childTitle = it.childName,
categoryTitle = it.categoryTitle,
hasPremium = hasPremium,
childTimezone = TimeZone.getTimeZone(it.childTimezone),
serverApiLevel = serverApiLevel
)
}
}
}
}