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\"")
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\"")
abstract fun getParentUserByIdLive(userId: String): LiveData<User?>

View file

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

View file

@ -15,6 +15,8 @@
*/
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.livedata.liveDataFromNonNullValue
import io.timelimit.android.livedata.map
@ -26,6 +28,10 @@ class ServerApiLevelLogic(logic: AppLogic) {
ServerApiLevelInfo.Offline
else
ServerApiLevelInfo.Online(serverLevel = database.config().getServerApiLevelSync())
private suspend fun getCoroutine(database: Database): ServerApiLevelInfo = Threads.database.executeAndWait {
getSync(database)
}
}
private val database = logic.database
@ -38,6 +44,8 @@ class ServerApiLevelLogic(logic: AppLogic) {
ServerApiLevelInfo.Online(serverLevel = apiLevel)
}
}
suspend fun getCoroutine() = getCoroutine(database)
}
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.overview.overview.CanNotAddDevicesInLocalModeDialogFragment
import io.timelimit.android.ui.payment.ActivityPurchaseModel
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.util.SyncStatusModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@ -141,6 +142,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
ActivityCommand.ShowAddDeviceFragment -> AddDeviceFragment().show(supportFragmentManager)
ActivityCommand.ShowCanNotAddDevicesInLocalModeDialogFragment -> CanNotAddDevicesInLocalModeDialogFragment().show(supportFragmentManager)
ActivityCommand.ShowAuthenticationScreen -> showAuthenticationScreen()
ActivityCommand.ShowMissingPremiumDialog -> RequiresPurchaseDialogFragment().show(supportFragmentManager)
}
}
}
@ -276,7 +278,11 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
.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?,
title: String,
subtitle: String?,
snackbarHostState: SnackbarHostState?,
content: @Composable (PaddingValues) -> Unit,
executeCommand: (UpdateStateCommand) -> Unit,
showAuthenticationDialog: (() -> Unit)?
@ -106,6 +107,7 @@ fun ScreenScaffold(
}
}
},
snackbarHost = { SnackbarHost(snackbarHostState ?: it) },
content = content
)
}

View file

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

View file

@ -15,6 +15,7 @@
*/
package io.timelimit.android.ui.model
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import io.timelimit.android.R
@ -34,7 +35,8 @@ sealed class Screen(
class OverviewScreen(
state: State,
val content: OverviewHandling.OverviewScreen
val content: OverviewHandling.OverviewScreen,
override val snackbarHostState: SnackbarHostState
): Screen(
state,
listOf(Menu.Icon(
@ -46,7 +48,10 @@ sealed class Screen(
R.string.main_tab_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
import androidx.compose.material.SnackbarHostState
import androidx.lifecycle.asFlow
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.async.Threads
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.HintsToShow
import io.timelimit.android.data.model.UserType
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.whileTrue
import io.timelimit.android.integration.platform.RuntimePermissionStatus
import io.timelimit.android.livedata.map
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.apply.ApplyActionUtil
import io.timelimit.android.ui.model.ActivityCommand
@ -51,7 +54,8 @@ object OverviewHandling {
authentication: AuthenticationModelApi,
stateLive: MutableStateFlow<State>
): 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 overviewState2Live: Flow<State.Overview> = stateLive.transform { if (it is State.Overview) emit(it) }
val overviewScreenLive: Flow<OverviewScreen> = getScreen(logic, actions, overviewStateLive)
@ -59,7 +63,7 @@ object OverviewHandling {
return hasMatchingStateLive.whileTrue {
overviewState2Live.combine(overviewScreenLive) { state, overviewScreen ->
Screen.OverviewScreen(state, overviewScreen)
Screen.OverviewScreen(state, overviewScreen, snackbarHostState)
}
}
}
@ -69,20 +73,31 @@ object OverviewHandling {
scope: CoroutineScope,
activityCommand: SendChannel<ActivityCommand>,
authentication: AuthenticationModelApi,
stateLive: MutableStateFlow<State>
stateLive: MutableStateFlow<State>,
snackbarHostState: SnackbarHostState
): Actions {
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(
hideIntro = {
scope.launch {
launch {
Threads.database.executeAndWait {
logic.database.config().setHintsShownSync(HintsToShow.OVERVIEW_INTRODUCTION)
}
}
},
addDevice = {
scope.launch {
launch {
lock.tryWithLock {
val isLocalMode = Threads.database.executeAndWait {
logic.database.config().getDeviceAuthTokenSync().isEmpty()
@ -94,7 +109,7 @@ object OverviewHandling {
}
},
skipTaskReview = { task ->
scope.launch {
launch {
lock.tryWithLock {
if (authentication.doParentAuthentication() != null) {
stateLive.update { oldState ->
@ -110,8 +125,7 @@ object OverviewHandling {
}
},
reviewReject = { task ->
// TODO: add error handler to scope
scope.launch {
launch {
lock.tryWithLock {
authentication.doParentAuthentication()?.let { parent ->
ApplyActionUtil.applyParentAction(
@ -128,9 +142,30 @@ object OverviewHandling {
}
}
},
reviewAccept = {
scope.launch {
TODO()
reviewAccept = { task ->
launch {
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?> {
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 ->
private fun getTaskToReview(logic: AppLogic, hiddenTaskIdsLive: Flow<Set<String>>): Flow<TaskToReview?> =
logic.database.childTasks().getPendingTasksFlow().combine(hiddenTaskIdsLive) { pendingTasks, hiddenTaskIds ->
pendingTasks
.filterNot { hiddenTaskIds.contains(it.childTask.taskId) }
.firstOrNull()
}.transformLatest {
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)
?.let { TaskToReview(it) }
}
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(
val hiddenTaskIds: Set<String>,
val visibleDevices: DeviceList,
@ -346,12 +357,7 @@ object OverviewHandling {
val showServerMessage: String?,
val showIntro: Boolean
)
data class TaskToReview(
val task: FullChildTask,
val hasPremium: Boolean,
val childTimezone: TimeZone,
val serverApiLevel: ServerApiLevelInfo
)
data class TaskToReview(val task: FullChildTask)
data class UserItem(
val id: 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.unit.dp
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.main.OverviewHandling
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.TimeTextUtil
@ -45,9 +41,6 @@ fun OverviewScreen(
executeCommand: (UpdateStateCommand) -> Unit,
modifier: Modifier = Modifier
) {
// TODO: implement this without dependency on MainActivity
val activity = LocalContext.current as MainActivity
LazyColumn (
contentPadding = PaddingValues(0.dp, 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
@ -186,9 +179,6 @@ fun OverviewScreen(
}
Row {
val auth = activity.getActivityViewModel()
val logic = auth.logic
TextButton(onClick = {
screen.actions.skipTaskReview(screen.taskToReview)
}) {
@ -203,23 +193,7 @@ fun OverviewScreen(
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = {
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()
}) {
OutlinedButton(onClick = { screen.actions.reviewAccept(screen.taskToReview) }) {
Text(stringResource(R.string.generic_yes))
}
}