Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: crash about persistent websocket being started from background [WPB-6551] #2745

Merged
merged 6 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.feature

import com.wire.android.di.KaliumCoreLogic
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withTimeoutOrNull
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ShouldStartPersistentWebSocketServiceUseCase @Inject constructor(
@KaliumCoreLogic private val coreLogic: CoreLogic
) {
suspend operator fun invoke(): Result {
return coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus().let { result ->
when (result) {
is ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure -> Result.Failure

is ObservePersistentWebSocketConnectionStatusUseCase.Result.Success -> {
val statusList = withTimeoutOrNull(TIMEOUT) {
val res = result.persistentWebSocketStatusListFlow.firstOrNull()
res
}
if (statusList != null && statusList.map { it.isPersistentWebSocketEnabled }.contains(true)) Result.Success(true)
else Result.Success(false)
}
}
}
}

sealed class Result {
data object Failure : Result()
data class Success(val shouldStartPersistentWebSocketService: Boolean) : Result()
}

companion object {
const val TIMEOUT = 10_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@
import android.content.Intent
import android.os.Build
import com.wire.android.appLogger
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.feature.ShouldStartPersistentWebSocketServiceUseCase
import com.wire.android.services.PersistentWebSocketService
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
Expand All @@ -43,41 +41,44 @@
lateinit var dispatcherProvider: DispatcherProvider

@Inject
@KaliumCoreLogic
lateinit var coreLogic: CoreLogic
lateinit var shouldStartPersistentWebSocketServiceUseCase: ShouldStartPersistentWebSocketServiceUseCase

Check warning on line 44 in app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt#L44

Added line #L44 was not covered by tests

private val scope by lazy {
CoroutineScope(SupervisorJob() + dispatcherProvider.io())
}

override fun onReceive(context: Context?, intent: Intent?) {
val persistentWebSocketServiceIntent = PersistentWebSocketService.newIntent(context)
appLogger.e("persistent web socket receiver")
appLogger.i("$TAG: onReceive called with action ${intent?.action}")
scope.launch {
coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus().let { result ->
when (result) {
is ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure -> {
appLogger.e("Failure while fetching persistent web socket status flow from StartServiceReceiver")
shouldStartPersistentWebSocketServiceUseCase().let {
when (it) {

Check warning on line 55 in app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt#L54-L55

Added lines #L54 - L55 were not covered by tests
is ShouldStartPersistentWebSocketServiceUseCase.Result.Failure -> {
appLogger.e("$TAG: Failure while fetching persistent web socket status flow")

Check warning on line 57 in app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt#L57

Added line #L57 was not covered by tests
}

is ObservePersistentWebSocketConnectionStatusUseCase.Result.Success -> {
result.persistentWebSocketStatusListFlow.collect { status ->
if (status.map { it.isPersistentWebSocketEnabled }.contains(true)) {
appLogger.e("Starting PersistentWebsocket Service from StartServiceReceiver")
if (!PersistentWebSocketService.isServiceStarted) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context?.startForegroundService(persistentWebSocketServiceIntent)
} else {
context?.startService(persistentWebSocketServiceIntent)
}
}
is ShouldStartPersistentWebSocketServiceUseCase.Result.Success -> {
if (it.shouldStartPersistentWebSocketService) {
if (PersistentWebSocketService.isServiceStarted) {
appLogger.i("$TAG: PersistentWebsocketService already started, not starting again")

Check warning on line 62 in app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt#L62

Added line #L62 was not covered by tests
} else {
context?.stopService(persistentWebSocketServiceIntent)
appLogger.i("$TAG: Starting PersistentWebsocketService")

Check warning on line 64 in app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt#L64

Added line #L64 was not covered by tests
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context?.startForegroundService(persistentWebSocketServiceIntent)
} else {
context?.startService(persistentWebSocketServiceIntent)
}
}
} else {
appLogger.i("$TAG: Stopping PersistentWebsocketService, no user with persistent web socket enabled found")

Check warning on line 72 in app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt#L72

Added line #L72 was not covered by tests
context?.stopService(persistentWebSocketServiceIntent)
}
}
}
}
}
}

companion object {

Check warning on line 81 in app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt#L81

Added line #L81 was not covered by tests
const val TAG = "StartServiceReceiver"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.feature

import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.auth.PersistentWebSocketStatus
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Test

class ShouldStartPersistentWebSocketServiceUseCaseTest {

@Test
fun givenObservePersistentWebSocketStatusReturnsSuccessAndThereAreUsersWithPersistentFlagOn_whenInvoking_shouldReturnSuccessTrue() =
runTest {
// given
val (_, useCase) = Arrangement()
.withObservePersistentWebSocketConnectionStatusSuccess(flowOf(listOf(PersistentWebSocketStatus(userId, true))))
.arrange()
// when
val result = useCase.invoke()
// then
assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also {
assertEquals(true, it.shouldStartPersistentWebSocketService)
}
}

@Test
fun givenObservePersistentWebSocketStatusReturnsSuccessAndThereAreNoUsersWithPersistentFlagOn_whenInvoking_shouldReturnSuccessFalse() =
runTest {
// given
val (_, useCase) = Arrangement()
.withObservePersistentWebSocketConnectionStatusSuccess(flowOf(listOf(PersistentWebSocketStatus(userId, false))))
.arrange()
// when
val result = useCase.invoke()
// then
assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also {
assertEquals(false, it.shouldStartPersistentWebSocketService)
}
}

@Test
fun givenObservePersistentWebSocketStatusReturnsSuccessAndThereAreNoUsers_whenInvoking_shouldReturnSuccessFalse() =
runTest {
// given
val (_, useCase) = Arrangement()
.withObservePersistentWebSocketConnectionStatusSuccess(flowOf(emptyList()))
.arrange()
// when
val result = useCase.invoke()
// then
assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also {
assertEquals(false, it.shouldStartPersistentWebSocketService)
}
}

@Test
fun givenObservePersistentWebSocketStatusReturnsSuccessAndTheFlowIsEmpty_whenInvoking_shouldReturnSuccessFalse() =
runTest {
// given
val (_, useCase) = Arrangement()
.withObservePersistentWebSocketConnectionStatusSuccess(emptyFlow())
.arrange()
// when
val result = useCase.invoke()
// then
assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also {
assertEquals(false, it.shouldStartPersistentWebSocketService)
}
}

@Test
fun givenObservePersistentWebSocketStatusReturnsSuccessAndFlowTimesOut_whenInvoking_shouldReturnSuccessFalse() =
runTest {
// given
val sharedFlow = MutableSharedFlow<List<PersistentWebSocketStatus>>() // shared flow doesn't close so we can test the timeout
val (_, useCase) = Arrangement()
.withObservePersistentWebSocketConnectionStatusSuccess(sharedFlow)
.arrange()
// when
val result = useCase.invoke()
advanceTimeBy(ShouldStartPersistentWebSocketServiceUseCase.TIMEOUT + 1000L)
// then
assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also {
assertEquals(false, it.shouldStartPersistentWebSocketService)
}
}

@Test
fun givenObservePersistentWebSocketStatusReturnsFailure_whenInvoking_shouldReturnFailure() =
runTest {
// given
val (_, useCase) = Arrangement()
.withObservePersistentWebSocketConnectionStatusFailure()
.arrange()
// when
val result = useCase.invoke()
// then
assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Failure::class.java, result)
}

inner class Arrangement {

@MockK
private lateinit var coreLogic: CoreLogic

val useCase by lazy {
ShouldStartPersistentWebSocketServiceUseCase(coreLogic)
}

init {
MockKAnnotations.init(this, relaxUnitFun = true)
}

fun arrange() = this to useCase

fun withObservePersistentWebSocketConnectionStatusSuccess(flow: Flow<List<PersistentWebSocketStatus>>) = apply {
coEvery { coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus() } returns
ObservePersistentWebSocketConnectionStatusUseCase.Result.Success(flow)
}
fun withObservePersistentWebSocketConnectionStatusFailure() = apply {
coEvery { coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus() } returns
ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure.StorageFailure
}
}

companion object {
private val userId = UserId("userId", "domain")
}
}