Skip to content

Commit

Permalink
fix: crash about persistent websocket being started from background […
Browse files Browse the repository at this point in the history
…WPB-6551] (#2745)
  • Loading branch information
saleniuk authored Feb 28, 2024
1 parent 88c2846 commit 85593a6
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 23 deletions.
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.Context
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 @@ class StartServiceReceiver : BroadcastReceiver() {
lateinit var dispatcherProvider: DispatcherProvider

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

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) {
is ShouldStartPersistentWebSocketServiceUseCase.Result.Failure -> {
appLogger.e("$TAG: Failure while fetching persistent web socket status flow")
}

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")
} else {
context?.stopService(persistentWebSocketServiceIntent)
appLogger.i("$TAG: Starting PersistentWebsocketService")
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")
context?.stopService(persistentWebSocketServiceIntent)
}
}
}
}
}
}

companion object {
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")
}
}

0 comments on commit 85593a6

Please sign in to comment.