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:
Jorge Martin Espinosa 2023-07-17 17:02:06 +02:00 committed by GitHub
parent a852465554
commit e61af2eb7d
26 changed files with 552 additions and 246 deletions

View file

@ -24,5 +24,8 @@ android {
dependencies {
implementation(libs.coroutines.core)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.startup)
implementation(projects.libraries.matrix.api)
}

View file

@ -0,0 +1,34 @@
/*
* 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.api
import kotlinx.coroutines.flow.StateFlow
/**
* A service that tracks the foreground state of the app.
*/
interface AppForegroundStateService {
/**
* Any updates to the foreground state of the app will be emitted here.
*/
val isInForeground: StateFlow<Boolean>
/**
* Start observing the foreground state.
*/
fun start()
}

View file

@ -16,43 +16,10 @@
package io.element.android.services.appnavstate.api
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
/**
* Can represent the current global app navigation state.
* @param owner mostly a Node identifier associated with the state.
* We are using the owner parameter to check when calling onLeaving methods is still using the same owner than his companion onNavigate.
* Why this is needed : for now we rely on lifecycle methods of the node, which are async.
* If you navigate quickly between nodes, onCreate of the new node is called before onDestroy of the previous node.
* So we assume if we don't get the same owner, we can skip the onLeaving action as we already replaced it.
* A wrapper for the current navigation state of the app, along with its foreground/background state.
*/
sealed class AppNavigationState(open val owner: String) {
object Root : AppNavigationState("ROOT")
data class Session(
override val owner: String,
val sessionId: SessionId,
) : AppNavigationState(owner)
data class Space(
override val owner: String,
// Can be fake value, if no space is selected
val spaceId: SpaceId,
val parentSession: Session,
) : AppNavigationState(owner)
data class Room(
override val owner: String,
val roomId: RoomId,
val parentSpace: Space,
) : AppNavigationState(owner)
data class Thread(
override val owner: String,
val threadId: ThreadId,
val parentRoom: Room,
) : AppNavigationState(owner)
}
data class AppNavigationState(
val navigationState: NavigationState,
val isInForeground: Boolean,
)

View file

@ -1,62 +0,0 @@
/*
* 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.api
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
fun AppNavigationState.currentSessionId(): SessionId? {
return when (this) {
AppNavigationState.Root -> null
is AppNavigationState.Session -> sessionId
is AppNavigationState.Space -> parentSession.sessionId
is AppNavigationState.Room -> parentSpace.parentSession.sessionId
is AppNavigationState.Thread -> parentRoom.parentSpace.parentSession.sessionId
}
}
fun AppNavigationState.currentSpaceId(): SpaceId? {
return when (this) {
AppNavigationState.Root -> null
is AppNavigationState.Session -> null
is AppNavigationState.Space -> spaceId
is AppNavigationState.Room -> parentSpace.spaceId
is AppNavigationState.Thread -> parentRoom.parentSpace.spaceId
}
}
fun AppNavigationState.currentRoomId(): RoomId? {
return when (this) {
AppNavigationState.Root -> null
is AppNavigationState.Session -> null
is AppNavigationState.Space -> null
is AppNavigationState.Room -> roomId
is AppNavigationState.Thread -> parentRoom.roomId
}
}
fun AppNavigationState.currentThreadId(): ThreadId? {
return when (this) {
AppNavigationState.Root -> null
is AppNavigationState.Session -> null
is AppNavigationState.Space -> null
is AppNavigationState.Room -> null
is AppNavigationState.Thread -> threadId
}
}

View file

@ -22,8 +22,11 @@ import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId
import kotlinx.coroutines.flow.StateFlow
/**
* A service that tracks the navigation and foreground states of the app.
*/
interface AppNavigationStateService {
val appNavigationStateFlow: StateFlow<AppNavigationState>
val appNavigationState: StateFlow<AppNavigationState>
fun onNavigateToSession(owner: String, sessionId: SessionId)
fun onLeavingSession(owner: String)
@ -37,3 +40,4 @@ interface AppNavigationStateService {
fun onNavigateToThread(owner: String, threadId: ThreadId)
fun onLeavingThread(owner: String)
}

View file

@ -0,0 +1,58 @@
/*
* 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.api
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
/**
* Can represent the current global app navigation state.
* @param owner mostly a Node identifier associated with the state.
* We are using the owner parameter to check when calling onLeaving methods is still using the same owner than his companion onNavigate.
* Why this is needed : for now we rely on lifecycle methods of the node, which are async.
* If you navigate quickly between nodes, onCreate of the new node is called before onDestroy of the previous node.
* So we assume if we don't get the same owner, we can skip the onLeaving action as we already replaced it.
*/
sealed class NavigationState(open val owner: String) {
object Root : NavigationState("ROOT")
data class Session(
override val owner: String,
val sessionId: SessionId,
) : NavigationState(owner)
data class Space(
override val owner: String,
// Can be fake value, if no space is selected
val spaceId: SpaceId,
val parentSession: Session,
) : NavigationState(owner)
data class Room(
override val owner: String,
val roomId: RoomId,
val parentSpace: Space,
) : NavigationState(owner)
data class Thread(
override val owner: String,
val threadId: ThreadId,
val parentRoom: Room,
) : NavigationState(owner)
}

View file

@ -0,0 +1,62 @@
/*
* 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.api
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
fun NavigationState.currentSessionId(): SessionId? {
return when (this) {
NavigationState.Root -> null
is NavigationState.Session -> sessionId
is NavigationState.Space -> parentSession.sessionId
is NavigationState.Room -> parentSpace.parentSession.sessionId
is NavigationState.Thread -> parentRoom.parentSpace.parentSession.sessionId
}
}
fun NavigationState.currentSpaceId(): SpaceId? {
return when (this) {
NavigationState.Root -> null
is NavigationState.Session -> null
is NavigationState.Space -> spaceId
is NavigationState.Room -> parentSpace.spaceId
is NavigationState.Thread -> parentRoom.parentSpace.spaceId
}
}
fun NavigationState.currentRoomId(): RoomId? {
return when (this) {
NavigationState.Root -> null
is NavigationState.Session -> null
is NavigationState.Space -> null
is NavigationState.Room -> roomId
is NavigationState.Thread -> parentRoom.roomId
}
}
fun NavigationState.currentThreadId(): ThreadId? {
return when (this) {
NavigationState.Root -> null
is NavigationState.Session -> null
is NavigationState.Space -> null
is NavigationState.Room -> null
is NavigationState.Thread -> threadId
}
}

View file

@ -38,6 +38,7 @@ dependencies {
implementation(libs.coroutines.core)
implementation(libs.androidx.corektx)
implementation(libs.androidx.lifecycle.process)
api(projects.services.appnavstate.api)
@ -45,5 +46,6 @@ dependencies {
testImplementation(libs.coroutines.test)
testImplementation(libs.test.truth)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.appnavstate.test)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)
}

View file

@ -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
)
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -26,4 +26,5 @@ dependencies {
api(projects.libraries.matrix.api)
api(projects.services.appnavstate.api)
implementation(libs.coroutines.core)
implementation(libs.androidx.lifecycle.runtime)
}

View file

@ -21,33 +21,33 @@ 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.NavigationState
const val A_SESSION_OWNER = "aSessionOwner"
const val A_SPACE_OWNER = "aSpaceOwner"
const val A_ROOM_OWNER = "aRoomOwner"
const val A_THREAD_OWNER = "aThreadOwner"
fun anAppNavigationState(
fun aNavigationState(
sessionId: SessionId? = null,
spaceId: SpaceId? = MAIN_SPACE,
roomId: RoomId? = null,
threadId: ThreadId? = null,
): AppNavigationState {
): NavigationState {
if (sessionId == null) {
return AppNavigationState.Root
return NavigationState.Root
}
val session = AppNavigationState.Session(A_SESSION_OWNER, sessionId)
val session = NavigationState.Session(A_SESSION_OWNER, sessionId)
if (spaceId == null) {
return session
}
val space = AppNavigationState.Space(A_SPACE_OWNER, spaceId, session)
val space = NavigationState.Space(A_SPACE_OWNER, spaceId, session)
if (roomId == null) {
return space
}
val room = AppNavigationState.Room(A_ROOM_OWNER, roomId, space)
val room = NavigationState.Room(A_ROOM_OWNER, roomId, space)
if (threadId == null) {
return room
}
return AppNavigationState.Thread(A_THREAD_OWNER, threadId, room)
return NavigationState.Thread(A_THREAD_OWNER, threadId, room)
}

View file

@ -20,16 +20,22 @@ 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.NavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.AppNavigationState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class NoopAppNavigationStateService : AppNavigationStateService {
class FakeAppNavigationStateService(
private val fakeAppNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow(
AppNavigationState(
navigationState = NavigationState.Root,
isInForeground = true,
)
),
) : AppNavigationStateService {
private val currentAppNavigationState: MutableStateFlow<AppNavigationState> =
MutableStateFlow(AppNavigationState.Root)
override val appNavigationStateFlow: StateFlow<AppNavigationState> = currentAppNavigationState
override val appNavigationState: StateFlow<AppNavigationState> = fakeAppNavigationState
override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit
override fun onLeavingSession(owner: String) = Unit