diff --git a/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt b/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt index 52d7836..cbd3980 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt @@ -24,12 +24,11 @@ import io.timelimit.android.data.model.UserType import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling +import io.timelimit.android.ui.model.flow.Case +import io.timelimit.android.ui.model.flow.splitConflated import io.timelimit.android.ui.model.launch.LaunchHandling import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.managechild.ManageChildHandling -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.* @@ -106,39 +105,28 @@ class MainModel(application: Application): AndroidViewModel(application) { val state = MutableStateFlow(State.LaunchState as State) var fragmentIds = mutableSetOf() - val screen: Flow = flow { - while (true) { - val scope = CoroutineScope(viewModelScope.coroutineContext + Job()) + val screen: Flow = state.splitConflated( + Case.simple<_, _, State.LaunchState> { LaunchHandling.processLaunchState(state, logic) }, + Case.simple<_, _, State.Overview> { OverviewHandling.processState(logic, scope, activityCommandInternal, authenticationModelApi, state) }, + Case.simple<_, _, State.ManageChild.Main> { state -> ManageChildHandling.processState(logic, state, updateMethod(::updateState)) }, + Case.simple<_, _, State.ManageChild.Apps> { state -> ManageChildHandling.processState(logic, state, updateMethod(::updateState)) }, + Case.simple<_, _, State.DiagnoseScreen.DeviceOwner> { DeviceOwnerHandling.processState(logic, scope, authenticationModelApi, state) }, + Case.simple<_, _, FragmentState> { state -> + state.transform { + val containerId = it.containerId ?: run { + (viewIdPool - fragmentIds).firstOrNull()?.also { id -> + it.containerId = id + } + } - when (val initialState = state.value) { - is State.LaunchState -> LaunchHandling.processLaunchState(state, logic) - is State.Overview -> emitAll(OverviewHandling.processState(logic, scope, activityCommandInternal, authenticationModelApi, state)) - is State.ManageChild.Main -> emitAll(ManageChildHandling.processState(logic, state)) - is State.ManageChild.Apps -> emitAll(ManageChildHandling.processState(logic, state)) - is State.DiagnoseScreen.DeviceOwner -> emitAll(DeviceOwnerHandling.processState(logic, scope, authenticationModelApi, state)) - is FragmentState -> emitAll(state.transformWhile { - if (it is FragmentState && it::class.java === initialState::class.java) { - val containerId = it.containerId ?: run { - (viewIdPool - fragmentIds).firstOrNull()?.also { id -> - it.containerId = id - } - } + if (containerId != null) { + fragmentIds.add(containerId) - if (containerId != null) { - fragmentIds.add(containerId) - - emit(Screen.FragmentScreen(it, it.toolbarIcons, it.toolbarOptions, it, containerId)) - } - - true - } else false - }) - else -> throw IllegalStateException() + emit(Screen.FragmentScreen(it as State, it.toolbarIcons, it.toolbarOptions, it, containerId)) + } } - - scope.cancel() } - } + ).shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1) fun execute(command: UpdateStateCommand) { command.applyTo(state) @@ -147,4 +135,6 @@ class MainModel(application: Application): AndroidViewModel(application) { fun reportAuthenticationScreenClosed() { authenticationScreenClosed.tryEmit(Unit) } + + private fun updateState(method: (State) -> State): Unit = state.update(method) } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/model/flow/SplitFlow.kt b/app/src/main/java/io/timelimit/android/ui/model/flow/SplitFlow.kt new file mode 100644 index 0000000..02173d9 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/model/flow/SplitFlow.kt @@ -0,0 +1,104 @@ +/* + * TimeLimit Copyright 2019 - 2023 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.ui.model.flow + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +class CaseScope( + val scope: CoroutineScope, + val className: Class +) { + inline fun updateMethod( + crossinline parent: ((SuperStateType) -> SuperStateType) -> Unit + ): ((LocalStateType) -> SuperStateType) -> Unit = { request -> + parent { oldState -> + if (scope.isActive && className.isInstance(oldState)) request(oldState as LocalStateType) + else oldState + } + } +} + +class Case( + val className: Class, + val key: (T) -> Any?, + val producer: CaseScope.(Flow, Any?) -> Flow +) { + companion object { + inline fun simple( + crossinline producer: CaseScope.(Flow) -> Flow + ) = Case( + className = C::class.java, + key = {} + ) { flow, _ -> producer(this as CaseScope, flow as Flow) } + + inline fun withKey( + crossinline withKey: (C) -> K, + crossinline producer: CaseScope.(K, Flow) -> Flow + ) = Case( + className = C::class.java, + key = { withKey(it as C) } + ) { flow, key -> producer(this as CaseScope, key as K, flow as Flow) } + } + + internal fun doesMatch(value: Any?): Boolean = className.isInstance(value) +} + +fun Flow.splitConflated(vararg cases: Case): Flow { + val input = this + + return channelFlow { + val inputChannel = Channel() + + launch { input.collect { inputChannel.send(it) }; inputChannel.close() } + + var value = inputChannel.receive() + + while (true) { + val case = cases.first { it.doesMatch(value) } + val key = case.key(value) + val relayChannel = Channel(Channel.CONFLATED) + val job = launch { + val scope = CaseScope(this, case.className as Class) + val inputFlow = flow { relayChannel.consumeEach { emit(it) } } + + case.producer(scope, inputFlow, key).collect { send(it) } + } + + try { + relayChannel.send(value) + + while (true) { + value = inputChannel.receive() + + val newCase = cases.first { it.doesMatch(value) } + + if (case !== newCase) break + if (case.key(value) != key) break + + relayChannel.send(value) + } + } finally { + relayChannel.cancel() + job.cancel() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/model/launch/LaunchHandling.kt b/app/src/main/java/io/timelimit/android/ui/model/launch/LaunchHandling.kt index 800a960..4f662f5 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/launch/LaunchHandling.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/launch/LaunchHandling.kt @@ -1,3 +1,18 @@ +/* + * TimeLimit Copyright 2019 - 2023 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.ui.model.launch import io.timelimit.android.async.Threads @@ -5,14 +20,17 @@ import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.data.model.UserType import io.timelimit.android.livedata.waitUntilValueMatches import io.timelimit.android.logic.AppLogic +import io.timelimit.android.ui.model.Screen import io.timelimit.android.ui.model.State +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow object LaunchHandling { - suspend fun processLaunchState( + fun processLaunchState( state: MutableStateFlow, logic: AppLogic - ) { + ): Flow = flow { val oldValue = state.value if (oldValue is State.LaunchState) { diff --git a/app/src/main/java/io/timelimit/android/ui/model/managechild/ManageChildHandling.kt b/app/src/main/java/io/timelimit/android/ui/model/managechild/ManageChildHandling.kt index 5974ad5..8cf5be2 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/managechild/ManageChildHandling.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/managechild/ManageChildHandling.kt @@ -17,38 +17,35 @@ package io.timelimit.android.ui.model.managechild import io.timelimit.android.R import io.timelimit.android.data.model.UserType -import io.timelimit.android.extensions.whileTrue import io.timelimit.android.logic.AppLogic import io.timelimit.android.ui.model.BackStackItem import io.timelimit.android.ui.model.Screen import io.timelimit.android.ui.model.State import io.timelimit.android.ui.model.Title +import io.timelimit.android.ui.model.flow.Case +import io.timelimit.android.ui.model.flow.splitConflated import kotlinx.coroutines.flow.* object ManageChildHandling { fun processState( logic: AppLogic, - stateLive: MutableStateFlow - ) = flow { - while (true) when (stateLive.value) { - is State.ManageChild.Main -> emitAll(processMainState(logic, stateLive)) - is State.ManageChild.Apps -> emitAll(processAppsState(logic, stateLive)) - else -> break - } - } + state: Flow, + updateState: ((State.ManageChild) -> State) -> Unit + ) = state.splitConflated( + Case.simple<_, _, State.ManageChild.Main> { processMainState(logic, it, updateMethod(updateState)) }, + Case.simple<_, _, State.ManageChild.Apps> { processAppsState(logic, it, updateMethod(updateState)) } + ) private fun processMainState( logic: AppLogic, - stateLive: MutableStateFlow + stateLive: Flow, + updateState: ((State.ManageChild.Main) -> State) -> Unit ): Flow { - val hasMatchingStateLive = stateLive.map { it is State.ManageChild.Main } - val matchingState = stateLive.filterIsInstance() - - val screenLive = matchingState.transformLatest { state -> + return stateLive.transformLatest { state -> val userLive = logic.database.user().getUserByIdFlow(state.childId) emitAll(userLive.transform {user -> - if (user?.type != UserType.Child) stateLive.compareAndSet(state, state.previousOverview) + if (user?.type != UserType.Child) updateState { state.previousOverview } else emit(Screen.ManageChildScreen( state, state.toolbarIcons, @@ -59,27 +56,23 @@ object ManageChildHandling { listOf( BackStackItem( Title.StringResource(R.string.main_tab_overview) - ) { stateLive.compareAndSet(state, state.previousOverview) } + ) { updateState { state.previousOverview } } ) )) }) } - - return hasMatchingStateLive.whileTrue { screenLive } } private fun processAppsState( logic: AppLogic, - stateLive: MutableStateFlow + stateLive: Flow, + updateState: ((State.ManageChild.Apps) -> State) -> Unit ): Flow { - val hasMatchingStateLive = stateLive.map { it is State.ManageChild.Apps } - val matchingState = stateLive.filterIsInstance() - - val screenLive = matchingState.transformLatest { state -> + return stateLive.transformLatest { state -> val userLive = logic.database.user().getUserByIdFlow(state.childId) emitAll(userLive.transform {user -> - if (user?.type != UserType.Child) stateLive.compareAndSet(state, state.previousChild.previousOverview) + if (user?.type != UserType.Child) updateState { state.previousChild.previousOverview } else emit(Screen.ManageChildAppsScreen( state, state.toolbarIcons, @@ -89,15 +82,13 @@ object ManageChildHandling { listOf( BackStackItem( Title.StringResource(R.string.main_tab_overview) - ) { stateLive.compareAndSet(state, state.previousChild.previousOverview) }, + ) { updateState { state.previousChild.previousOverview } }, BackStackItem( Title.Plain(user.name) - ) { stateLive.compareAndSet(state, state.previousChild) } + ) { updateState { state.previousChild } } ) )) }) } - - return hasMatchingStateLive.whileTrue { screenLive } } } \ No newline at end of file