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
a852465554
commit
e61af2eb7d
26 changed files with 552 additions and 246 deletions
|
|
@ -209,6 +209,7 @@ dependencies {
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.androidx.lifecycle.runtime)
|
implementation(libs.androidx.lifecycle.runtime)
|
||||||
|
implementation(libs.androidx.lifecycle.process)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.startup)
|
implementation(libs.androidx.startup)
|
||||||
implementation(libs.androidx.preference)
|
implementation(libs.androidx.preference)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,18 @@
|
||||||
android:theme="@style/Theme.ElementX"
|
android:theme="@style/Theme.ElementX"
|
||||||
tools:targetApi="33">
|
tools:targetApi="33">
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name='androidx.lifecycle.ProcessLifecycleInitializer'
|
||||||
|
android:value='androidx.startup' />
|
||||||
|
|
||||||
|
</provider>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.libraries.architecture.childNode
|
import io.element.android.libraries.architecture.childNode
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.services.appnavstate.test.NoopAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ class RoomFlowNodeTest {
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
messagesEntryPoint = messagesEntryPoint,
|
messagesEntryPoint = messagesEntryPoint,
|
||||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||||
appNavigationStateService = NoopAppNavigationStateService(),
|
appNavigationStateService = FakeAppNavigationStateService(),
|
||||||
roomMembershipObserver = RoomMembershipObserver()
|
roomMembershipObserver = RoomMembershipObserver()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ dependencies {
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.androidx.lifecycle.process)
|
|
||||||
implementation(libs.androidx.security.crypto)
|
implementation(libs.androidx.security.crypto)
|
||||||
implementation(libs.network.retrofit)
|
implementation(libs.network.retrofit)
|
||||||
implementation(libs.serialization.json)
|
implementation(libs.serialization.json)
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
import io.element.android.libraries.push.api.store.PushDataStore
|
import io.element.android.libraries.push.api.store.PushDataStore
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.services.appnavstate.api.NavigationState
|
||||||
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -65,7 +63,6 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
|
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
|
||||||
*/
|
*/
|
||||||
private val notificationState by lazy { createInitialNotificationState() }
|
private val notificationState by lazy { createInitialNotificationState() }
|
||||||
private var currentAppNavigationState: AppNavigationState? = null
|
|
||||||
private val firstThrottler = FirstThrottler(200)
|
private val firstThrottler = FirstThrottler(200)
|
||||||
|
|
||||||
// TODO EAx add a setting per user for this
|
// TODO EAx add a setting per user for this
|
||||||
|
|
@ -74,26 +71,25 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
init {
|
init {
|
||||||
// Observe application state
|
// Observe application state
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
appNavigationStateService.appNavigationStateFlow
|
appNavigationStateService.appNavigationState
|
||||||
.collect { onAppNavigationStateChange(it) }
|
.collect { onAppNavigationStateChange(it.navigationState) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAppNavigationStateChange(appNavigationState: AppNavigationState) {
|
private fun onAppNavigationStateChange(navigationState: NavigationState) {
|
||||||
currentAppNavigationState = appNavigationState
|
when (navigationState) {
|
||||||
when (appNavigationState) {
|
NavigationState.Root -> {}
|
||||||
AppNavigationState.Root -> {}
|
is NavigationState.Session -> {}
|
||||||
is AppNavigationState.Session -> {}
|
is NavigationState.Space -> {}
|
||||||
is AppNavigationState.Space -> {}
|
is NavigationState.Room -> {
|
||||||
is AppNavigationState.Room -> {
|
|
||||||
// Cleanup notification for current room
|
// Cleanup notification for current room
|
||||||
clearMessagesForRoom(appNavigationState.parentSpace.parentSession.sessionId, appNavigationState.roomId)
|
clearMessagesForRoom(navigationState.parentSpace.parentSession.sessionId, navigationState.roomId)
|
||||||
}
|
}
|
||||||
is AppNavigationState.Thread -> {
|
is NavigationState.Thread -> {
|
||||||
onEnteringThread(
|
onEnteringThread(
|
||||||
appNavigationState.parentRoom.parentSpace.parentSession.sessionId,
|
navigationState.parentRoom.parentSpace.parentSession.sessionId,
|
||||||
appNavigationState.parentRoom.roomId,
|
navigationState.parentRoom.roomId,
|
||||||
appNavigationState.threadId
|
navigationState.threadId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +221,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
private suspend fun refreshNotificationDrawerBg() {
|
private suspend fun refreshNotificationDrawerBg() {
|
||||||
Timber.v("refreshNotificationDrawerBg()")
|
Timber.v("refreshNotificationDrawerBg()")
|
||||||
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
|
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
|
||||||
notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also {
|
notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also {
|
||||||
queuedEvents.clearAndAdd(it.onlyKeptEvents())
|
queuedEvents.clearAndAdd(it.onlyKeptEvents())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -275,8 +271,4 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents)
|
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldIgnoreMessageEventInRoom(resolvedEvent: NotifiableMessageEvent): Boolean {
|
|
||||||
return resolvedEvent.shouldIgnoreEventInRoom(currentAppNavigationState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -31,18 +31,19 @@ private typealias ProcessedEvents = List<ProcessedEvent<NotifiableEvent>>
|
||||||
|
|
||||||
class NotifiableEventProcessor @Inject constructor(
|
class NotifiableEventProcessor @Inject constructor(
|
||||||
private val outdatedDetector: OutdatedEventDetector,
|
private val outdatedDetector: OutdatedEventDetector,
|
||||||
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun process(
|
fun process(
|
||||||
queuedEvents: List<NotifiableEvent>,
|
queuedEvents: List<NotifiableEvent>,
|
||||||
appNavigationState: AppNavigationState?,
|
|
||||||
renderedEvents: ProcessedEvents,
|
renderedEvents: ProcessedEvents,
|
||||||
): ProcessedEvents {
|
): ProcessedEvents {
|
||||||
|
val appState = appNavigationStateService.appNavigationState.value
|
||||||
val processedEvents = queuedEvents.map {
|
val processedEvents = queuedEvents.map {
|
||||||
val type = when (it) {
|
val type = when (it) {
|
||||||
is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP
|
is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP
|
||||||
is NotifiableMessageEvent -> when {
|
is NotifiableMessageEvent -> when {
|
||||||
it.shouldIgnoreEventInRoom(appNavigationState) -> {
|
it.shouldIgnoreEventInRoom(appState) -> {
|
||||||
ProcessedEvent.Type.REMOVE
|
ProcessedEvent.Type.REMOVE
|
||||||
.also { Timber.d("notification message removed due to currently viewing the same room or thread") }
|
.also { Timber.d("notification message removed due to currently viewing the same room or thread") }
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +56,7 @@ class NotifiableEventProcessor @Inject constructor(
|
||||||
else -> ProcessedEvent.Type.KEEP
|
else -> ProcessedEvent.Type.KEEP
|
||||||
}
|
}
|
||||||
is FallbackNotifiableEvent -> when {
|
is FallbackNotifiableEvent -> when {
|
||||||
it.shouldIgnoreEventInRoom(appNavigationState) -> {
|
it.shouldIgnoreEventInRoom(appState) -> {
|
||||||
ProcessedEvent.Type.REMOVE
|
ProcessedEvent.Type.REMOVE
|
||||||
.also { Timber.d("notification fallback removed due to currently viewing the same room or thread") }
|
.also { Timber.d("notification fallback removed due to currently viewing the same room or thread") }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
package io.element.android.libraries.push.impl.notifications.model
|
package io.element.android.libraries.push.impl.notifications.model
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.SessionId
|
||||||
|
|
@ -69,18 +67,13 @@ data class NotifiableMessageEvent(
|
||||||
/**
|
/**
|
||||||
* Used to check if a notification should be ignored based on the current app and navigation state.
|
* Used to check if a notification should be ignored based on the current app and navigation state.
|
||||||
*/
|
*/
|
||||||
fun NotifiableEvent.shouldIgnoreEventInRoom(
|
fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationState): Boolean {
|
||||||
appNavigationState: AppNavigationState?
|
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
|
||||||
): Boolean {
|
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
|
||||||
val currentSessionId = appNavigationState?.currentSessionId() ?: return false
|
|
||||||
return when (val currentRoomId = appNavigationState.currentRoomId()) {
|
|
||||||
null -> false
|
null -> false
|
||||||
else -> isAppInForeground
|
else -> appNavigationState.isInForeground
|
||||||
&& sessionId == currentSessionId
|
&& sessionId == currentSessionId
|
||||||
&& roomId == currentRoomId
|
&& roomId == currentRoomId
|
||||||
&& (this as? NotifiableMessageEvent)?.threadId == appNavigationState.currentThreadId()
|
&& (this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val isAppInForeground: Boolean
|
|
||||||
get() = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,20 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
import io.element.android.services.appnavstate.test.anAppNavigationState
|
import io.element.android.services.appnavstate.api.NavigationState
|
||||||
|
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||||
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
|
import io.element.android.services.appnavstate.test.aNavigationState
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
private val NOT_VIEWING_A_ROOM = anAppNavigationState()
|
private val NOT_VIEWING_A_ROOM = aNavigationState()
|
||||||
private val VIEWING_A_ROOM = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID)
|
private val VIEWING_A_ROOM = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID)
|
||||||
private val VIEWING_A_THREAD = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID)
|
private val VIEWING_A_THREAD = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID)
|
||||||
|
|
||||||
class NotifiableEventProcessorTest {
|
class NotifiableEventProcessorTest {
|
||||||
|
|
||||||
private val outdatedDetector = FakeOutdatedEventDetector()
|
private val outdatedDetector = FakeOutdatedEventDetector()
|
||||||
private val eventProcessor = NotifiableEventProcessor(outdatedDetector.instance)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given simple events when processing then keep simple events`() {
|
fun `given simple events when processing then keep simple events`() {
|
||||||
|
|
@ -48,8 +51,9 @@ class NotifiableEventProcessorTest {
|
||||||
aSimpleNotifiableEvent(eventId = AN_EVENT_ID),
|
aSimpleNotifiableEvent(eventId = AN_EVENT_ID),
|
||||||
aSimpleNotifiableEvent(eventId = AN_EVENT_ID_2)
|
aSimpleNotifiableEvent(eventId = AN_EVENT_ID_2)
|
||||||
)
|
)
|
||||||
|
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -62,8 +66,9 @@ class NotifiableEventProcessorTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given redacted simple event when processing then remove redaction event`() {
|
fun `given redacted simple event when processing then remove redaction event`() {
|
||||||
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = EventType.REDACTION))
|
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = EventType.REDACTION))
|
||||||
|
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -78,8 +83,9 @@ class NotifiableEventProcessorTest {
|
||||||
anInviteNotifiableEvent(roomId = A_ROOM_ID),
|
anInviteNotifiableEvent(roomId = A_ROOM_ID),
|
||||||
anInviteNotifiableEvent(roomId = A_ROOM_ID_2)
|
anInviteNotifiableEvent(roomId = A_ROOM_ID_2)
|
||||||
)
|
)
|
||||||
|
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -94,7 +100,9 @@ class NotifiableEventProcessorTest {
|
||||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
||||||
outdatedDetector.givenEventIsOutOfDate(events[0])
|
outdatedDetector.givenEventIsOutOfDate(events[0])
|
||||||
|
|
||||||
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||||
|
|
||||||
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -107,8 +115,9 @@ class NotifiableEventProcessorTest {
|
||||||
fun `given in date message event when processing then keep message event`() {
|
fun `given in date message event when processing then keep message event`() {
|
||||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
||||||
outdatedDetector.givenEventIsInDate(events[0])
|
outdatedDetector.givenEventIsInDate(events[0])
|
||||||
|
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -121,8 +130,9 @@ class NotifiableEventProcessorTest {
|
||||||
fun `given viewing the same room main timeline when processing main timeline message event then removes message`() {
|
fun `given viewing the same room main timeline when processing main timeline message event then removes message`() {
|
||||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = null))
|
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = null))
|
||||||
events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
|
events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
|
||||||
|
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList())
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -135,8 +145,9 @@ class NotifiableEventProcessorTest {
|
||||||
fun `given viewing the same thread timeline when processing thread message event then removes message`() {
|
fun `given viewing the same thread timeline when processing thread message event then removes message`() {
|
||||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
|
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
|
||||||
events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
|
events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
|
||||||
|
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList())
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -149,8 +160,9 @@ class NotifiableEventProcessorTest {
|
||||||
fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() {
|
fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() {
|
||||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
|
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
|
||||||
outdatedDetector.givenEventIsInDate(events[0])
|
outdatedDetector.givenEventIsInDate(events[0])
|
||||||
|
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList())
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -163,8 +175,9 @@ class NotifiableEventProcessorTest {
|
||||||
fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() {
|
fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() {
|
||||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
||||||
outdatedDetector.givenEventIsInDate(events[0])
|
outdatedDetector.givenEventIsInDate(events[0])
|
||||||
|
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList())
|
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -180,8 +193,9 @@ class NotifiableEventProcessorTest {
|
||||||
ProcessedEvent(ProcessedEvent.Type.KEEP, events[0]),
|
ProcessedEvent(ProcessedEvent.Type.KEEP, events[0]),
|
||||||
ProcessedEvent(ProcessedEvent.Type.KEEP, anInviteNotifiableEvent(eventId = AN_EVENT_ID_2))
|
ProcessedEvent(ProcessedEvent.Type.KEEP, anInviteNotifiableEvent(eventId = AN_EVENT_ID_2))
|
||||||
)
|
)
|
||||||
|
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||||
|
|
||||||
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = renderedEvents)
|
val result = eventProcessor.process(events, renderedEvents = renderedEvents)
|
||||||
|
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOfProcessedEvents(
|
listOfProcessedEvents(
|
||||||
|
|
@ -194,4 +208,14 @@ class NotifiableEventProcessorTest {
|
||||||
private fun listOfProcessedEvents(vararg event: Pair<ProcessedEvent.Type, NotifiableEvent>) = event.map {
|
private fun listOfProcessedEvents(vararg event: Pair<ProcessedEvent.Type, NotifiableEvent>) = event.map {
|
||||||
ProcessedEvent(it.first, it.second)
|
ProcessedEvent(it.first, it.second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createProcessor(
|
||||||
|
isInForeground: Boolean = false,
|
||||||
|
navigationState: NavigationState
|
||||||
|
): NotifiableEventProcessor {
|
||||||
|
return NotifiableEventProcessor(
|
||||||
|
outdatedDetector.instance,
|
||||||
|
FakeAppNavigationStateService(MutableStateFlow(AppNavigationState(navigationState, isInForeground))),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,8 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime)
|
||||||
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
implementation(libs.androidx.startup)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -16,43 +16,10 @@
|
||||||
|
|
||||||
package io.element.android.services.appnavstate.api
|
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.
|
* A wrapper for the current navigation state of the app, along with its foreground/background 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 AppNavigationState(open val owner: String) {
|
data class AppNavigationState(
|
||||||
object Root : AppNavigationState("ROOT")
|
val navigationState: NavigationState,
|
||||||
|
val isInForeground: Boolean,
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,8 +22,11 @@ import io.element.android.libraries.matrix.api.core.SpaceId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that tracks the navigation and foreground states of the app.
|
||||||
|
*/
|
||||||
interface AppNavigationStateService {
|
interface AppNavigationStateService {
|
||||||
val appNavigationStateFlow: StateFlow<AppNavigationState>
|
val appNavigationState: StateFlow<AppNavigationState>
|
||||||
|
|
||||||
fun onNavigateToSession(owner: String, sessionId: SessionId)
|
fun onNavigateToSession(owner: String, sessionId: SessionId)
|
||||||
fun onLeavingSession(owner: String)
|
fun onLeavingSession(owner: String)
|
||||||
|
|
@ -37,3 +40,4 @@ interface AppNavigationStateService {
|
||||||
fun onNavigateToThread(owner: String, threadId: ThreadId)
|
fun onNavigateToThread(owner: String, threadId: ThreadId)
|
||||||
fun onLeavingThread(owner: String)
|
fun onLeavingThread(owner: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ dependencies {
|
||||||
|
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
|
||||||
api(projects.services.appnavstate.api)
|
api(projects.services.appnavstate.api)
|
||||||
|
|
||||||
|
|
@ -45,5 +46,6 @@ dependencies {
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
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.AppNavigationStateService
|
||||||
|
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -38,113 +43,131 @@ private val loggerTag = LoggerTag("Navigation")
|
||||||
*/
|
*/
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
@SingleIn(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)
|
private val state = MutableStateFlow(
|
||||||
override val appNavigationStateFlow: StateFlow<AppNavigationState> = currentAppNavigationState
|
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) {
|
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")
|
Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue")
|
||||||
val newValue: AppNavigationState.Session = when (currentValue) {
|
val newValue: NavigationState.Session = when (currentValue) {
|
||||||
is AppNavigationState.Session,
|
is NavigationState.Session,
|
||||||
is AppNavigationState.Space,
|
is NavigationState.Space,
|
||||||
is AppNavigationState.Room,
|
is NavigationState.Room,
|
||||||
is AppNavigationState.Thread,
|
is NavigationState.Thread,
|
||||||
is AppNavigationState.Root -> AppNavigationState.Session(owner, sessionId)
|
is NavigationState.Root -> NavigationState.Session(owner, sessionId)
|
||||||
}
|
}
|
||||||
currentAppNavigationState.value = newValue
|
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigateToSpace(owner: String, spaceId: SpaceId) {
|
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")
|
Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue")
|
||||||
val newValue: AppNavigationState.Space = when (currentValue) {
|
val newValue: NavigationState.Space = when (currentValue) {
|
||||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||||
is AppNavigationState.Session -> AppNavigationState.Space(owner, spaceId, currentValue)
|
is NavigationState.Session -> NavigationState.Space(owner, spaceId, currentValue)
|
||||||
is AppNavigationState.Space -> AppNavigationState.Space(owner, spaceId, currentValue.parentSession)
|
is NavigationState.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession)
|
||||||
is AppNavigationState.Room -> AppNavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
|
is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
|
||||||
is AppNavigationState.Thread -> AppNavigationState.Space(owner, spaceId, currentValue.parentRoom.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) {
|
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")
|
Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue")
|
||||||
val newValue: AppNavigationState.Room = when (currentValue) {
|
val newValue: NavigationState.Room = when (currentValue) {
|
||||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||||
is AppNavigationState.Space -> AppNavigationState.Room(owner, roomId, currentValue)
|
is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue)
|
||||||
is AppNavigationState.Room -> AppNavigationState.Room(owner, roomId, currentValue.parentSpace)
|
is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace)
|
||||||
is AppNavigationState.Thread -> AppNavigationState.Room(owner, roomId, currentValue.parentRoom.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) {
|
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")
|
Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue")
|
||||||
val newValue: AppNavigationState.Thread = when (currentValue) {
|
val newValue: NavigationState.Thread = when (currentValue) {
|
||||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||||
is AppNavigationState.Space -> error("onNavigateToRoom() must be called first")
|
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||||
is AppNavigationState.Room -> AppNavigationState.Thread(owner, threadId, currentValue)
|
is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue)
|
||||||
is AppNavigationState.Thread -> AppNavigationState.Thread(owner, threadId, currentValue.parentRoom)
|
is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom)
|
||||||
}
|
}
|
||||||
currentAppNavigationState.value = newValue
|
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLeavingThread(owner: String) {
|
override fun onLeavingThread(owner: String) {
|
||||||
val currentValue = currentAppNavigationState.value
|
val currentValue = state.value.navigationState
|
||||||
Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue")
|
Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue")
|
||||||
if (!currentValue.assertOwner(owner)) return
|
if (!currentValue.assertOwner(owner)) return
|
||||||
val newValue: AppNavigationState.Room = when (currentValue) {
|
val newValue: NavigationState.Room = when (currentValue) {
|
||||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||||
is AppNavigationState.Space -> error("onNavigateToRoom() must be called first")
|
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||||
is AppNavigationState.Room -> error("onNavigateToThread() must be called first")
|
is NavigationState.Room -> error("onNavigateToThread() must be called first")
|
||||||
is AppNavigationState.Thread -> currentValue.parentRoom
|
is NavigationState.Thread -> currentValue.parentRoom
|
||||||
}
|
}
|
||||||
currentAppNavigationState.value = newValue
|
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLeavingRoom(owner: String) {
|
override fun onLeavingRoom(owner: String) {
|
||||||
val currentValue = currentAppNavigationState.value
|
val currentValue = state.value.navigationState
|
||||||
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
|
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
|
||||||
if (!currentValue.assertOwner(owner)) return
|
if (!currentValue.assertOwner(owner)) return
|
||||||
val newValue: AppNavigationState.Space = when (currentValue) {
|
val newValue: NavigationState.Space = when (currentValue) {
|
||||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||||
is AppNavigationState.Space -> error("onNavigateToRoom() must be called first")
|
is NavigationState.Space -> error("onNavigateToRoom() must be called first")
|
||||||
is AppNavigationState.Room -> currentValue.parentSpace
|
is NavigationState.Room -> currentValue.parentSpace
|
||||||
is AppNavigationState.Thread -> currentValue.parentRoom.parentSpace
|
is NavigationState.Thread -> currentValue.parentRoom.parentSpace
|
||||||
}
|
}
|
||||||
currentAppNavigationState.value = newValue
|
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLeavingSpace(owner: String) {
|
override fun onLeavingSpace(owner: String) {
|
||||||
val currentValue = currentAppNavigationState.value
|
val currentValue = state.value.navigationState
|
||||||
Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue")
|
Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue")
|
||||||
if (!currentValue.assertOwner(owner)) return
|
if (!currentValue.assertOwner(owner)) return
|
||||||
val newValue: AppNavigationState.Session = when (currentValue) {
|
val newValue: NavigationState.Session = when (currentValue) {
|
||||||
AppNavigationState.Root -> error("onNavigateToSession() must be called first")
|
NavigationState.Root -> error("onNavigateToSession() must be called first")
|
||||||
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
|
is NavigationState.Session -> error("onNavigateToSpace() must be called first")
|
||||||
is AppNavigationState.Space -> currentValue.parentSession
|
is NavigationState.Space -> currentValue.parentSession
|
||||||
is AppNavigationState.Room -> currentValue.parentSpace.parentSession
|
is NavigationState.Room -> currentValue.parentSpace.parentSession
|
||||||
is AppNavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession
|
is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession
|
||||||
}
|
}
|
||||||
currentAppNavigationState.value = newValue
|
state.getAndUpdate { it.copy(navigationState = newValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLeavingSession(owner: String) {
|
override fun onLeavingSession(owner: String) {
|
||||||
val currentValue = currentAppNavigationState.value
|
val currentValue = state.value.navigationState
|
||||||
Timber.tag(loggerTag.value).d("Leaving session. Current state: $currentValue")
|
Timber.tag(loggerTag.value).d("Leaving session. Current state: $currentValue")
|
||||||
if (!currentValue.assertOwner(owner)) return
|
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) {
|
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)")
|
Timber.tag(loggerTag.value).d("Can't leave current state as the owner is not the same (current = ${this.owner}, new = $owner)")
|
||||||
return false
|
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_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SPACE_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.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_ROOM_OWNER
|
||||||
import io.element.android.services.appnavstate.test.A_SESSION_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_SPACE_OWNER
|
||||||
import io.element.android.services.appnavstate.test.A_THREAD_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.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Assert.assertThrows
|
import org.junit.Assert.assertThrows
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DefaultAppNavigationStateServiceTest {
|
class DefaultNavigationStateServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNavigation() = runTest {
|
fun testNavigation() = runCancellableScopeTest { scope ->
|
||||||
val service = DefaultAppNavigationStateService()
|
val service = createStateService(scope)
|
||||||
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
|
service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
|
||||||
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
|
||||||
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
|
||||||
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
|
service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
|
||||||
assertThat(service.appNavigationStateFlow.first()).isEqualTo(
|
assertThat(service.appNavigationState.first().navigationState).isEqualTo(
|
||||||
AppNavigationState.Thread(
|
NavigationState.Thread(
|
||||||
A_THREAD_OWNER, A_THREAD_ID,
|
A_THREAD_OWNER, A_THREAD_ID,
|
||||||
AppNavigationState.Room(
|
NavigationState.Room(
|
||||||
A_ROOM_OWNER,
|
A_ROOM_OWNER,
|
||||||
A_ROOM_ID,
|
A_ROOM_ID,
|
||||||
AppNavigationState.Space(
|
NavigationState.Space(
|
||||||
A_SPACE_OWNER,
|
A_SPACE_OWNER,
|
||||||
A_SPACE_ID,
|
A_SPACE_ID,
|
||||||
AppNavigationState.Session(
|
NavigationState.Session(
|
||||||
A_SESSION_OWNER,
|
A_SESSION_OWNER,
|
||||||
A_SESSION_ID
|
A_SESSION_ID
|
||||||
)
|
)
|
||||||
|
|
@ -60,8 +61,13 @@ class DefaultAppNavigationStateServiceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testFailure() = runTest {
|
fun testFailure() = runCancellableScopeTest { scope ->
|
||||||
val service = DefaultAppNavigationStateService()
|
val service = createStateService(scope)
|
||||||
|
|
||||||
assertThrows(IllegalStateException::class.java) { service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) }
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,4 +26,5 @@ dependencies {
|
||||||
api(projects.libraries.matrix.api)
|
api(projects.libraries.matrix.api)
|
||||||
api(projects.services.appnavstate.api)
|
api(projects.services.appnavstate.api)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
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_SESSION_OWNER = "aSessionOwner"
|
||||||
const val A_SPACE_OWNER = "aSpaceOwner"
|
const val A_SPACE_OWNER = "aSpaceOwner"
|
||||||
const val A_ROOM_OWNER = "aRoomOwner"
|
const val A_ROOM_OWNER = "aRoomOwner"
|
||||||
const val A_THREAD_OWNER = "aThreadOwner"
|
const val A_THREAD_OWNER = "aThreadOwner"
|
||||||
|
|
||||||
fun anAppNavigationState(
|
fun aNavigationState(
|
||||||
sessionId: SessionId? = null,
|
sessionId: SessionId? = null,
|
||||||
spaceId: SpaceId? = MAIN_SPACE,
|
spaceId: SpaceId? = MAIN_SPACE,
|
||||||
roomId: RoomId? = null,
|
roomId: RoomId? = null,
|
||||||
threadId: ThreadId? = null,
|
threadId: ThreadId? = null,
|
||||||
): AppNavigationState {
|
): NavigationState {
|
||||||
if (sessionId == null) {
|
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) {
|
if (spaceId == null) {
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
val space = AppNavigationState.Space(A_SPACE_OWNER, spaceId, session)
|
val space = NavigationState.Space(A_SPACE_OWNER, spaceId, session)
|
||||||
if (roomId == null) {
|
if (roomId == null) {
|
||||||
return space
|
return space
|
||||||
}
|
}
|
||||||
val room = AppNavigationState.Room(A_ROOM_OWNER, roomId, space)
|
val room = NavigationState.Room(A_ROOM_OWNER, roomId, space)
|
||||||
if (threadId == null) {
|
if (threadId == null) {
|
||||||
return room
|
return room
|
||||||
}
|
}
|
||||||
return AppNavigationState.Thread(A_THREAD_OWNER, threadId, room)
|
return NavigationState.Thread(A_THREAD_OWNER, threadId, room)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
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.AppNavigationStateService
|
||||||
|
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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> =
|
override val appNavigationState: StateFlow<AppNavigationState> = fakeAppNavigationState
|
||||||
MutableStateFlow(AppNavigationState.Root)
|
|
||||||
override val appNavigationStateFlow: StateFlow<AppNavigationState> = currentAppNavigationState
|
|
||||||
|
|
||||||
override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit
|
override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit
|
||||||
override fun onLeavingSession(owner: String) = Unit
|
override fun onLeavingSession(owner: String) = Unit
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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.tests.testutils
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a test with a [CoroutineScope] that will be cancelled automatically and avoiding failing the test.
|
||||||
|
*/
|
||||||
|
fun runCancellableScopeTest(block: suspend (CoroutineScope) -> Unit) = runTest {
|
||||||
|
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
block(scope)
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue