diff --git a/app/src/main/java/io/timelimit/android/data/dao/ChildTaskDao.kt b/app/src/main/java/io/timelimit/android/data/dao/ChildTaskDao.kt index 5e210c2..180a687 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/ChildTaskDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/ChildTaskDao.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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 - @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> @Query("SELECT * FROM child_task WHERE category_id = :categoryId") diff --git a/app/src/main/java/io/timelimit/android/data/model/derived/FullChildTask.kt b/app/src/main/java/io/timelimit/android/data/model/derived/FullChildTask.kt index daba776..5334048 100644 --- a/app/src/main/java/io/timelimit/android/data/model/derived/FullChildTask.kt +++ b/app/src/main/java/io/timelimit/android/data/model/derived/FullChildTask.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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 ) \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/logic/AppLogic.kt b/app/src/main/java/io/timelimit/android/logic/AppLogic.kt index fda2a25..87d044d 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppLogic.kt @@ -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().apply { value = false } val isConnected = isConnectedInternal.castDown() diff --git a/app/src/main/java/io/timelimit/android/logic/ServerApiLevelLogic.kt b/app/src/main/java/io/timelimit/android/logic/ServerApiLevelLogic.kt new file mode 100644 index 0000000..e009bbe --- /dev/null +++ b/app/src/main/java/io/timelimit/android/logic/ServerApiLevelLogic.kt @@ -0,0 +1,43 @@ +/* + * TimeLimit Copyright 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 . + */ +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 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt b/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt index a17b35a..6902293 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2021 Jonas Lochmann + * TimeLimit Copyright 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() } } diff --git a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt index 2894f75..9c22f0a 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt @@ -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)) diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt index 852a347..fb62be2 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2021 Jonas Lochmann + * TimeLimit Copyright 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 ) ) } diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentAdapter.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentAdapter.kt index 4e25f83..26384fd 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentAdapter.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentAdapter.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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() { @@ -260,7 +262,15 @@ class OverviewFragmentAdapter : RecyclerView.Adapter 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) } diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentItem.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentItem.kt index 46c6604..fb04035 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentItem.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentItem.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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() \ No newline at end of file +data class TaskReviewOverviewItem( + val task: ChildTask, + val childTitle: String, + val categoryTitle: String, + val hasPremium: Boolean, + val childTimezone: TimeZone, + val serverApiLevel: ServerApiLevelInfo +): OverviewFragmentItem() \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentModel.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentModel.kt index e90326c..3610405 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragmentModel.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2021 Jonas Lochmann + * TimeLimit Copyright 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 -> - pendingTasksToShowLive.map { tasks -> - tasks.firstOrNull()?.let { - TaskReviewOverviewItem(task = it.childTask, childTitle = it.childName, categoryTitle = it.categoryTitle, hasPremium = hasPremium) + logic.serverApiLevelLogic.infoLive.switchMap { serverApiLevel -> + pendingTasksToShowLive.map { tasks -> + tasks.firstOrNull()?.let { + TaskReviewOverviewItem( + task = it.childTask, + childTitle = it.childName, + categoryTitle = it.categoryTitle, + hasPremium = hasPremium, + childTimezone = TimeZone.getTimeZone(it.childTimezone), + serverApiLevel = serverApiLevel + ) + } } } }