Refactor task review yes handling

This commit is contained in:
Jonas Lochmann 2023-02-06 01:00:00 +01:00
parent cb4beed825
commit fb864723ef
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
9 changed files with 85 additions and 76 deletions

View file

@ -34,6 +34,9 @@ abstract class UserDao {
@Query("SELECT * from user WHERE id = :userId AND type = \"child\"") @Query("SELECT * from user WHERE id = :userId AND type = \"child\"")
abstract fun getChildUserByIdLive(userId: String): LiveData<User?> abstract fun getChildUserByIdLive(userId: String): LiveData<User?>
@Query("SELECT * from user WHERE id = :userId AND type = \"child\"")
abstract suspend fun getChildUserByIdCoroutine(userId: String): User?
@Query("SELECT * from user WHERE id = :userId AND type = \"parent\"") @Query("SELECT * from user WHERE id = :userId AND type = \"parent\"")
abstract fun getParentUserByIdLive(userId: String): LiveData<User?> abstract fun getParentUserByIdLive(userId: String): LiveData<User?>

View file

@ -15,13 +15,17 @@
*/ */
package io.timelimit.android.logic package io.timelimit.android.logic
import androidx.lifecycle.asFlow
import io.timelimit.android.livedata.ignoreUnchanged import io.timelimit.android.livedata.ignoreUnchanged
import io.timelimit.android.livedata.map import io.timelimit.android.livedata.map
import io.timelimit.android.livedata.or import io.timelimit.android.livedata.or
import kotlinx.coroutines.flow.first
class FullVersionLogic(logic: AppLogic) { class FullVersionLogic(logic: AppLogic) {
private val hasFullVersion = logic.database.config().getFullVersionUntilAsync().map { it != 0L }.ignoreUnchanged() private val hasFullVersion = logic.database.config().getFullVersionUntilAsync().map { it != 0L }.ignoreUnchanged()
val isLocalMode = logic.database.config().getDeviceAuthTokenAsync().map { it == "" } val isLocalMode = logic.database.config().getDeviceAuthTokenAsync().map { it == "" }
val shouldProvideFullVersionFunctions = hasFullVersion.or(isLocalMode) val shouldProvideFullVersionFunctions = hasFullVersion.or(isLocalMode)
suspend fun shouldProvideFullVersionFunctions() = shouldProvideFullVersionFunctions.asFlow().first()
} }

View file

@ -15,6 +15,8 @@
*/ */
package io.timelimit.android.logic package io.timelimit.android.logic
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.Database import io.timelimit.android.data.Database
import io.timelimit.android.livedata.liveDataFromNonNullValue import io.timelimit.android.livedata.liveDataFromNonNullValue
import io.timelimit.android.livedata.map import io.timelimit.android.livedata.map
@ -26,6 +28,10 @@ class ServerApiLevelLogic(logic: AppLogic) {
ServerApiLevelInfo.Offline ServerApiLevelInfo.Offline
else else
ServerApiLevelInfo.Online(serverLevel = database.config().getServerApiLevelSync()) ServerApiLevelInfo.Online(serverLevel = database.config().getServerApiLevelSync())
private suspend fun getCoroutine(database: Database): ServerApiLevelInfo = Threads.database.executeAndWait {
getSync(database)
}
} }
private val database = logic.database private val database = logic.database
@ -38,6 +44,8 @@ class ServerApiLevelLogic(logic: AppLogic) {
ServerApiLevelInfo.Online(serverLevel = apiLevel) ServerApiLevelInfo.Online(serverLevel = apiLevel)
} }
} }
suspend fun getCoroutine() = getCoroutine(database)
} }
sealed class ServerApiLevelInfo { sealed class ServerApiLevelInfo {

View file

@ -63,6 +63,7 @@ import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
import io.timelimit.android.ui.model.* import io.timelimit.android.ui.model.*
import io.timelimit.android.ui.overview.overview.CanNotAddDevicesInLocalModeDialogFragment import io.timelimit.android.ui.overview.overview.CanNotAddDevicesInLocalModeDialogFragment
import io.timelimit.android.ui.payment.ActivityPurchaseModel import io.timelimit.android.ui.payment.ActivityPurchaseModel
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.util.SyncStatusModel import io.timelimit.android.ui.util.SyncStatusModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -141,6 +142,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
ActivityCommand.ShowAddDeviceFragment -> AddDeviceFragment().show(supportFragmentManager) ActivityCommand.ShowAddDeviceFragment -> AddDeviceFragment().show(supportFragmentManager)
ActivityCommand.ShowCanNotAddDevicesInLocalModeDialogFragment -> CanNotAddDevicesInLocalModeDialogFragment().show(supportFragmentManager) ActivityCommand.ShowCanNotAddDevicesInLocalModeDialogFragment -> CanNotAddDevicesInLocalModeDialogFragment().show(supportFragmentManager)
ActivityCommand.ShowAuthenticationScreen -> showAuthenticationScreen() ActivityCommand.ShowAuthenticationScreen -> showAuthenticationScreen()
ActivityCommand.ShowMissingPremiumDialog -> RequiresPurchaseDialogFragment().show(supportFragmentManager)
} }
} }
} }
@ -276,7 +278,11 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
.padding(paddingValues) .padding(paddingValues)
) )
}, },
showAuthenticationDialog = showAuthenticationDialog showAuthenticationDialog = showAuthenticationDialog,
snackbarHostState = when (screen) {
is ScreenWithSnackbar -> screen.snackbarHostState
else -> null
}
) )
} }
} }

View file

@ -34,6 +34,7 @@ fun ScreenScaffold(
screen: Screen?, screen: Screen?,
title: String, title: String,
subtitle: String?, subtitle: String?,
snackbarHostState: SnackbarHostState?,
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
executeCommand: (UpdateStateCommand) -> Unit, executeCommand: (UpdateStateCommand) -> Unit,
showAuthenticationDialog: (() -> Unit)? showAuthenticationDialog: (() -> Unit)?
@ -106,6 +107,7 @@ fun ScreenScaffold(
} }
} }
}, },
snackbarHost = { SnackbarHost(snackbarHostState ?: it) },
content = content content = content
) )
} }

View file

@ -19,4 +19,5 @@ sealed class ActivityCommand {
object ShowCanNotAddDevicesInLocalModeDialogFragment: ActivityCommand() object ShowCanNotAddDevicesInLocalModeDialogFragment: ActivityCommand()
object ShowAddDeviceFragment: ActivityCommand() object ShowAddDeviceFragment: ActivityCommand()
object ShowAuthenticationScreen: ActivityCommand() object ShowAuthenticationScreen: ActivityCommand()
object ShowMissingPremiumDialog: ActivityCommand()
} }

View file

@ -15,6 +15,7 @@
*/ */
package io.timelimit.android.ui.model package io.timelimit.android.ui.model
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import io.timelimit.android.R import io.timelimit.android.R
@ -34,7 +35,8 @@ sealed class Screen(
class OverviewScreen( class OverviewScreen(
state: State, state: State,
val content: OverviewHandling.OverviewScreen val content: OverviewHandling.OverviewScreen,
override val snackbarHostState: SnackbarHostState
): Screen( ): Screen(
state, state,
listOf(Menu.Icon( listOf(Menu.Icon(
@ -46,7 +48,10 @@ sealed class Screen(
R.string.main_tab_uninstall, R.string.main_tab_uninstall,
UpdateStateCommand.Overview.Uninstall UpdateStateCommand.Overview.Uninstall
)) ))
), ScreenWithAuthenticationFab ), ScreenWithAuthenticationFab, ScreenWithSnackbar
} }
interface ScreenWithAuthenticationFab interface ScreenWithAuthenticationFab
interface ScreenWithSnackbar {
val snackbarHostState: SnackbarHostState
}

View file

@ -15,20 +15,23 @@
*/ */
package io.timelimit.android.ui.model.main package io.timelimit.android.ui.model.main
import androidx.compose.material.SnackbarHostState
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.extensions.getTimezone
import io.timelimit.android.data.model.Device import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.HintsToShow import io.timelimit.android.data.model.HintsToShow
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.data.model.derived.FullChildTask import io.timelimit.android.data.model.derived.FullChildTask
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.extensions.tryWithLock import io.timelimit.android.extensions.tryWithLock
import io.timelimit.android.extensions.whileTrue import io.timelimit.android.extensions.whileTrue
import io.timelimit.android.integration.platform.RuntimePermissionStatus import io.timelimit.android.integration.platform.RuntimePermissionStatus
import io.timelimit.android.livedata.map import io.timelimit.android.livedata.map
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.ServerApiLevelInfo
import io.timelimit.android.sync.actions.ReviewChildTaskAction import io.timelimit.android.sync.actions.ReviewChildTaskAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.ui.model.ActivityCommand import io.timelimit.android.ui.model.ActivityCommand
@ -51,7 +54,8 @@ object OverviewHandling {
authentication: AuthenticationModelApi, authentication: AuthenticationModelApi,
stateLive: MutableStateFlow<State> stateLive: MutableStateFlow<State>
): Flow<Screen> { ): Flow<Screen> {
val actions: Actions = getActions(logic, scope, activityCommand, authentication, stateLive) val snackbarHostState = SnackbarHostState()
val actions: Actions = getActions(logic, scope, activityCommand, authentication, stateLive, snackbarHostState)
val overviewStateLive: Flow<OverviewState> = stateLive.transform { if (it is State.Overview) emit(it.state) } val overviewStateLive: Flow<OverviewState> = stateLive.transform { if (it is State.Overview) emit(it.state) }
val overviewState2Live: Flow<State.Overview> = stateLive.transform { if (it is State.Overview) emit(it) } val overviewState2Live: Flow<State.Overview> = stateLive.transform { if (it is State.Overview) emit(it) }
val overviewScreenLive: Flow<OverviewScreen> = getScreen(logic, actions, overviewStateLive) val overviewScreenLive: Flow<OverviewScreen> = getScreen(logic, actions, overviewStateLive)
@ -59,7 +63,7 @@ object OverviewHandling {
return hasMatchingStateLive.whileTrue { return hasMatchingStateLive.whileTrue {
overviewState2Live.combine(overviewScreenLive) { state, overviewScreen -> overviewState2Live.combine(overviewScreenLive) { state, overviewScreen ->
Screen.OverviewScreen(state, overviewScreen) Screen.OverviewScreen(state, overviewScreen, snackbarHostState)
} }
} }
} }
@ -69,20 +73,31 @@ object OverviewHandling {
scope: CoroutineScope, scope: CoroutineScope,
activityCommand: SendChannel<ActivityCommand>, activityCommand: SendChannel<ActivityCommand>,
authentication: AuthenticationModelApi, authentication: AuthenticationModelApi,
stateLive: MutableStateFlow<State> stateLive: MutableStateFlow<State>,
snackbarHostState: SnackbarHostState
): Actions { ): Actions {
val lock = Mutex() val lock = Mutex()
fun launch(action: suspend () -> Unit) {
scope.launch {
try {
action()
} catch (ex: Exception) {
snackbarHostState.showSnackbar(logic.context.getString(R.string.error_general))
}
}
}
return Actions( return Actions(
hideIntro = { hideIntro = {
scope.launch { launch {
Threads.database.executeAndWait { Threads.database.executeAndWait {
logic.database.config().setHintsShownSync(HintsToShow.OVERVIEW_INTRODUCTION) logic.database.config().setHintsShownSync(HintsToShow.OVERVIEW_INTRODUCTION)
} }
} }
}, },
addDevice = { addDevice = {
scope.launch { launch {
lock.tryWithLock { lock.tryWithLock {
val isLocalMode = Threads.database.executeAndWait { val isLocalMode = Threads.database.executeAndWait {
logic.database.config().getDeviceAuthTokenSync().isEmpty() logic.database.config().getDeviceAuthTokenSync().isEmpty()
@ -94,7 +109,7 @@ object OverviewHandling {
} }
}, },
skipTaskReview = { task -> skipTaskReview = { task ->
scope.launch { launch {
lock.tryWithLock { lock.tryWithLock {
if (authentication.doParentAuthentication() != null) { if (authentication.doParentAuthentication() != null) {
stateLive.update { oldState -> stateLive.update { oldState ->
@ -110,8 +125,7 @@ object OverviewHandling {
} }
}, },
reviewReject = { task -> reviewReject = { task ->
// TODO: add error handler to scope launch {
scope.launch {
lock.tryWithLock { lock.tryWithLock {
authentication.doParentAuthentication()?.let { parent -> authentication.doParentAuthentication()?.let { parent ->
ApplyActionUtil.applyParentAction( ApplyActionUtil.applyParentAction(
@ -128,9 +142,30 @@ object OverviewHandling {
} }
} }
}, },
reviewAccept = { reviewAccept = { task ->
scope.launch { launch {
TODO() val parent = authentication.authenticatedParentOnly.first()
val hasPremium = logic.fullVersion.shouldProvideFullVersionFunctions()
if (parent == null) authentication.doParentAuthentication()
else if (!hasPremium) activityCommand.send(ActivityCommand.ShowMissingPremiumDialog)
else {
val serverApiLevel = logic.serverApiLevelLogic.getCoroutine()
val child = logic.database.user().getChildUserByIdCoroutine(task.task.childId)
val time = logic.timeApi.getCurrentTimeInMillis()
val day = DateInTimezone.newInstance(time, child.getTimezone()).dayOfEpoch
ApplyActionUtil.applyParentAction(
ReviewChildTaskAction(
taskId = task.task.childTask.taskId,
ok = true,
time = time,
day = if (serverApiLevel.hasLevelOrIsOffline(2)) day else null
),
parent.authentication,
logic
)
}
} }
} }
) )
@ -274,38 +309,14 @@ object OverviewHandling {
} }
} }
@OptIn(ExperimentalCoroutinesApi::class) private fun getTaskToReview(logic: AppLogic, hiddenTaskIdsLive: Flow<Set<String>>): Flow<TaskToReview?> =
private fun getTaskToReview(logic: AppLogic, hiddenTaskIdsLive: Flow<Set<String>>): Flow<TaskToReview?> { logic.database.childTasks().getPendingTasksFlow().combine(hiddenTaskIdsLive) { pendingTasks, hiddenTaskIds ->
val pendingTasksLive = logic.database.childTasks().getPendingTasksFlow()
val serverApiLevelLive = logic.serverApiLevelLogic.infoLive.asFlow()
val hasPremiumLive = logic.fullVersion.shouldProvideFullVersionFunctions.asFlow()
val taskWithChildLive = pendingTasksLive.combine(hiddenTaskIdsLive) { pendingTasks, hiddenTaskIds ->
pendingTasks pendingTasks
.filterNot { hiddenTaskIds.contains(it.childTask.taskId) } .filterNot { hiddenTaskIds.contains(it.childTask.taskId) }
.firstOrNull() .firstOrNull()
}.transformLatest { ?.let { TaskToReview(it) }
if (it != null) {
emitAll(logic.database.user().getChildUserByIdLive(it.childId).asFlow().map { childInfo ->
if (childInfo == null) null
else Pair(it, childInfo.timeZone)
})
} else emit(null)
} }
return combine(
serverApiLevelLive, hasPremiumLive, taskWithChildLive
) { serverApiLevel, hasPremium, taskWithChild ->
if (taskWithChild == null) null
else TaskToReview(
task = taskWithChild.first,
childTimezone = TimeZone.getTimeZone(taskWithChild.second),
serverApiLevel = serverApiLevel,
hasPremium = hasPremium
)
}
}
data class OverviewState( data class OverviewState(
val hiddenTaskIds: Set<String>, val hiddenTaskIds: Set<String>,
val visibleDevices: DeviceList, val visibleDevices: DeviceList,
@ -346,12 +357,7 @@ object OverviewHandling {
val showServerMessage: String?, val showServerMessage: String?,
val showIntro: Boolean val showIntro: Boolean
) )
data class TaskToReview( data class TaskToReview(val task: FullChildTask)
val task: FullChildTask,
val hasPremium: Boolean,
val childTimezone: TimeZone,
val serverApiLevel: ServerApiLevelInfo
)
data class UserItem( data class UserItem(
val id: String, val id: String,
val name: String, val name: String,

View file

@ -29,12 +29,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.sync.actions.ReviewChildTaskAction
import io.timelimit.android.ui.MainActivity
import io.timelimit.android.ui.model.UpdateStateCommand import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.main.OverviewHandling
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.util.DateUtil import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.TimeTextUtil import io.timelimit.android.util.TimeTextUtil
@ -45,9 +41,6 @@ fun OverviewScreen(
executeCommand: (UpdateStateCommand) -> Unit, executeCommand: (UpdateStateCommand) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// TODO: implement this without dependency on MainActivity
val activity = LocalContext.current as MainActivity
LazyColumn ( LazyColumn (
contentPadding = PaddingValues(0.dp, 8.dp), contentPadding = PaddingValues(0.dp, 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@ -186,9 +179,6 @@ fun OverviewScreen(
} }
Row { Row {
val auth = activity.getActivityViewModel()
val logic = auth.logic
TextButton(onClick = { TextButton(onClick = {
screen.actions.skipTaskReview(screen.taskToReview) screen.actions.skipTaskReview(screen.taskToReview)
}) { }) {
@ -203,23 +193,7 @@ fun OverviewScreen(
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = { OutlinedButton(onClick = { screen.actions.reviewAccept(screen.taskToReview) }) {
if (activity.getActivityViewModel().isParentAuthenticated()) {
if (screen.taskToReview.hasPremium) {
val time = logic.timeApi.getCurrentTimeInMillis()
val day = DateInTimezone.newInstance(time, screen.taskToReview.childTimezone).dayOfEpoch
auth.tryDispatchParentAction(
ReviewChildTaskAction(
taskId = screen.taskToReview.task.childTask.taskId,
ok = true,
time = time,
day = if (screen.taskToReview.serverApiLevel.hasLevelOrIsOffline(2)) day else null
)
)
} else RequiresPurchaseDialogFragment().show(activity.supportFragmentManager)
} else activity.showAuthenticationScreen()
}) {
Text(stringResource(R.string.generic_yes)) Text(stringResource(R.string.generic_yes))
} }
} }