Fix: make sure we ignore notifications for open rooms (#867)
* Make sure we ignore notifications for open rooms - Listen to process lifecycle changes in `AppForegroundStateService`. Use initializers to reliable create it. - Merge `AppNavigationState` with `AppForegroundState`. Renamed the previous `AppNavigationState` to `NavigationState`, created a new `AppNavigationState` which contains both the navigation state and the foreground state.
This commit is contained in:
parent
004b86b05d
commit
9247cd765a
26 changed files with 552 additions and 246 deletions
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.services.appnavstate.impl
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class DefaultAppForegroundStateService : AppForegroundStateService {
|
||||
|
||||
private val state = MutableStateFlow(false)
|
||||
override val isInForeground: StateFlow<Boolean> = state
|
||||
|
||||
private val appLifecycle: Lifecycle by lazy { ProcessLifecycleOwner.get().lifecycle }
|
||||
|
||||
override fun start() {
|
||||
appLifecycle.addObserver(lifecycleObserver)
|
||||
}
|
||||
|
||||
private val lifecycleObserver = LifecycleEventObserver { _, _ -> state.value = getCurrentState() }
|
||||
|
||||
private fun getCurrentState(): Boolean = appLifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||
}
|
||||
|
|
@ -24,10 +24,15 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -38,113 +43,131 @@ private val loggerTag = LoggerTag("Navigation")
|
|||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStateService {
|
||||
class DefaultAppNavigationStateService @Inject constructor(
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : AppNavigationStateService {
|
||||
|
||||
private val currentAppNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow(AppNavigationState.Root)
|
||||
override val appNavigationStateFlow: StateFlow<AppNavigationState> = currentAppNavigationState
|
||||
private val state = MutableStateFlow(
|
||||
AppNavigationState(
|
||||
navigationState = NavigationState.Root,
|
||||
isInForeground = true,
|
||||
)
|
||||
)
|
||||
override val appNavigationState: StateFlow<AppNavigationState> = state
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
appForegroundStateService.start()
|
||||
|
||||
appForegroundStateService.isInForeground.collect { isInForeground ->
|
||||
state.getAndUpdate { it.copy(isInForeground = isInForeground) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigateToSession(owner: String, sessionId: SessionId) {
|
||||
val currentValue = currentAppNavigationState.value
|
||||
val currentValue = state.value.navigationState
|
||||
Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue")
|
||||
val newValue: AppNavigationState.Session = when (currentValue) {
|
||||
is AppNavigationState.Session,
|
||||
is AppNavigationState.Space,
|
||||
is AppNavigationState.Room,
|
||||
is AppNavigationState.Thread,
|
||||
is AppNavigationState.Root -> AppNavigationState.Session(owner, sessionId)
|
||||
val newValue: NavigationState.Session = when (currentValue) {
|
||||
is NavigationState.Session,
|
||||
is NavigationState.Space,
|
||||
is NavigationState.Room,
|
||||
is NavigationState.Thread,
|
||||
is NavigationState.Root -> NavigationState.Session(owner, sessionId)
|
||||
}
|
||||
currentAppNavigationState.value = newValue
|
||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||
}
|
||||
|
||||
override fun onNavigateToSpace(owner: String, spaceId: SpaceId) {
|
||||
val currentValue = currentAppNavigationState.value
|
||||
val currentValue = state.value.navigationState
|
||||
Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue")
|
||||
val newValue: AppNavigationState.Space = when (currentValue) {
|
||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is AppNavigationState.Session -> AppNavigationState.Space(owner, spaceId, currentValue)
|
||||
is AppNavigationState.Space -> AppNavigationState.Space(owner, spaceId, currentValue.parentSession)
|
||||
is AppNavigationState.Room -> AppNavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
|
||||
is AppNavigationState.Thread -> AppNavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession)
|
||||
val newValue: NavigationState.Space = when (currentValue) {
|
||||
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is NavigationState.Session -> NavigationState.Space(owner, spaceId, currentValue)
|
||||
is NavigationState.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession)
|
||||
is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
|
||||
is NavigationState.Thread -> NavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession)
|
||||
}
|
||||
currentAppNavigationState.value = newValue
|
||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||
}
|
||||
|
||||
override fun onNavigateToRoom(owner: String, roomId: RoomId) {
|
||||
val currentValue = currentAppNavigationState.value
|
||||
val currentValue = state.value.navigationState
|
||||
Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue")
|
||||
val newValue: AppNavigationState.Room = when (currentValue) {
|
||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is AppNavigationState.Space -> AppNavigationState.Room(owner, roomId, currentValue)
|
||||
is AppNavigationState.Room -> AppNavigationState.Room(owner, roomId, currentValue.parentSpace)
|
||||
is AppNavigationState.Thread -> AppNavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace)
|
||||
val newValue: NavigationState.Room = when (currentValue) {
|
||||
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue)
|
||||
is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace)
|
||||
is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace)
|
||||
}
|
||||
currentAppNavigationState.value = newValue
|
||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||
}
|
||||
|
||||
override fun onNavigateToThread(owner: String, threadId: ThreadId) {
|
||||
val currentValue = currentAppNavigationState.value
|
||||
val currentValue = state.value.navigationState
|
||||
Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue")
|
||||
val newValue: AppNavigationState.Thread = when (currentValue) {
|
||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is AppNavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||
is AppNavigationState.Room -> AppNavigationState.Thread(owner, threadId, currentValue)
|
||||
is AppNavigationState.Thread -> AppNavigationState.Thread(owner, threadId, currentValue.parentRoom)
|
||||
val newValue: NavigationState.Thread = when (currentValue) {
|
||||
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||
is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue)
|
||||
is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom)
|
||||
}
|
||||
currentAppNavigationState.value = newValue
|
||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||
}
|
||||
|
||||
override fun onLeavingThread(owner: String) {
|
||||
val currentValue = currentAppNavigationState.value
|
||||
val currentValue = state.value.navigationState
|
||||
Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue")
|
||||
if (!currentValue.assertOwner(owner)) return
|
||||
val newValue: AppNavigationState.Room = when (currentValue) {
|
||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is AppNavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||
is AppNavigationState.Room -> error("onNavigateToThread() must be called first")
|
||||
is AppNavigationState.Thread -> currentValue.parentRoom
|
||||
val newValue: NavigationState.Room = when (currentValue) {
|
||||
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||
is NavigationState.Room -> error("onNavigateToThread() must be called first")
|
||||
is NavigationState.Thread -> currentValue.parentRoom
|
||||
}
|
||||
currentAppNavigationState.value = newValue
|
||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||
}
|
||||
|
||||
override fun onLeavingRoom(owner: String) {
|
||||
val currentValue = currentAppNavigationState.value
|
||||
val currentValue = state.value.navigationState
|
||||
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
|
||||
if (!currentValue.assertOwner(owner)) return
|
||||
val newValue: AppNavigationState.Space = when (currentValue) {
|
||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is AppNavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||
is AppNavigationState.Room -> currentValue.parentSpace
|
||||
is AppNavigationState.Thread -> currentValue.parentRoom.parentSpace
|
||||
val newValue: NavigationState.Space = when (currentValue) {
|
||||
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||
is NavigationState.Room -> currentValue.parentSpace
|
||||
is NavigationState.Thread -> currentValue.parentRoom.parentSpace
|
||||
}
|
||||
currentAppNavigationState.value = newValue
|
||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||
}
|
||||
|
||||
override fun onLeavingSpace(owner: String) {
|
||||
val currentValue = currentAppNavigationState.value
|
||||
val currentValue = state.value.navigationState
|
||||
Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue")
|
||||
if (!currentValue.assertOwner(owner)) return
|
||||
val newValue: AppNavigationState.Session = when (currentValue) {
|
||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is AppNavigationState.Space -> currentValue.parentSession
|
||||
is AppNavigationState.Room -> currentValue.parentSpace.parentSession
|
||||
is AppNavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession
|
||||
val newValue: NavigationState.Session = when (currentValue) {
|
||||
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||
is NavigationState.Space -> currentValue.parentSession
|
||||
is NavigationState.Room -> currentValue.parentSpace.parentSession
|
||||
is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession
|
||||
}
|
||||
currentAppNavigationState.value = newValue
|
||||
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||
}
|
||||
|
||||
override fun onLeavingSession(owner: String) {
|
||||
val currentValue = currentAppNavigationState.value
|
||||
val currentValue = state.value.navigationState
|
||||
Timber.tag(loggerTag.value).d("Leaving session. Current state: $currentValue")
|
||||
if (!currentValue.assertOwner(owner)) return
|
||||
currentAppNavigationState.value = AppNavigationState.Root
|
||||
state.getAndUpdate { it.copy(navigationState = NavigationState.Root) }
|
||||
}
|
||||
|
||||
private fun AppNavigationState.assertOwner(owner: String): Boolean {
|
||||
private fun NavigationState.assertOwner(owner: String): Boolean {
|
||||
if (this.owner != owner) {
|
||||
Timber.tag(loggerTag.value).d("Can't leave current state as the owner is not the same (current = ${this.owner}, new = $owner)")
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.services.appnavstate.impl.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.startup.AppInitializer
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import io.element.android.services.appnavstate.impl.initializer.AppForegroundStateServiceInitializer
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
object AppNavStateModule {
|
||||
|
||||
@Provides
|
||||
fun provideAppForegroundStateService(
|
||||
@ApplicationContext context: Context
|
||||
): AppForegroundStateService =
|
||||
AppInitializer.getInstance(context).initializeComponent(AppForegroundStateServiceInitializer::class.java)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.services.appnavstate.impl.initializer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ProcessLifecycleInitializer
|
||||
import androidx.startup.Initializer
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import io.element.android.services.appnavstate.impl.DefaultAppForegroundStateService
|
||||
|
||||
class AppForegroundStateServiceInitializer : Initializer<AppForegroundStateService> {
|
||||
override fun create(context: Context): AppForegroundStateService {
|
||||
return DefaultAppForegroundStateService()
|
||||
}
|
||||
|
||||
override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf(
|
||||
ProcessLifecycleInitializer::class.java
|
||||
)
|
||||
}
|
||||
|
|
@ -21,35 +21,36 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.test.A_ROOM_OWNER
|
||||
import io.element.android.services.appnavstate.test.A_SESSION_OWNER
|
||||
import io.element.android.services.appnavstate.test.A_SPACE_OWNER
|
||||
import io.element.android.services.appnavstate.test.A_THREAD_OWNER
|
||||
import io.element.android.tests.testutils.runCancellableScopeTest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultAppNavigationStateServiceTest {
|
||||
class DefaultNavigationStateServiceTest {
|
||||
|
||||
@Test
|
||||
fun testNavigation() = runTest {
|
||||
val service = DefaultAppNavigationStateService()
|
||||
fun testNavigation() = runCancellableScopeTest { scope ->
|
||||
val service = createStateService(scope)
|
||||
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
|
||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
||||
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
||||
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
|
||||
assertThat(service.appNavigationStateFlow.first()).isEqualTo(
|
||||
AppNavigationState.Thread(
|
||||
assertThat(service.appNavigationState.first().navigationState).isEqualTo(
|
||||
NavigationState.Thread(
|
||||
A_THREAD_OWNER, A_THREAD_ID,
|
||||
AppNavigationState.Room(
|
||||
NavigationState.Room(
|
||||
A_ROOM_OWNER,
|
||||
A_ROOM_ID,
|
||||
AppNavigationState.Space(
|
||||
NavigationState.Space(
|
||||
A_SPACE_OWNER,
|
||||
A_SPACE_ID,
|
||||
AppNavigationState.Session(
|
||||
NavigationState.Session(
|
||||
A_SESSION_OWNER,
|
||||
A_SESSION_ID
|
||||
)
|
||||
|
|
@ -60,8 +61,13 @@ class DefaultAppNavigationStateServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testFailure() = runTest {
|
||||
val service = DefaultAppNavigationStateService()
|
||||
fun testFailure() = runCancellableScopeTest { scope ->
|
||||
val service = createStateService(scope)
|
||||
|
||||
assertThrows(IllegalStateException::class.java) { service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) }
|
||||
}
|
||||
|
||||
private fun createStateService(
|
||||
coroutineScope: CoroutineScope
|
||||
) = DefaultAppNavigationStateService(FakeAppForegroundStateService(), coroutineScope)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.services.appnavstate.impl
|
||||
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class FakeAppForegroundStateService(
|
||||
initialValue: Boolean = true,
|
||||
) : AppForegroundStateService {
|
||||
|
||||
private val state = MutableStateFlow(initialValue)
|
||||
override val isInForeground: StateFlow<Boolean> = state
|
||||
|
||||
override fun start() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
fun givenIsInForeground(isInForeground: Boolean) {
|
||||
state.value = isInForeground
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue