diff --git a/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt new file mode 100644 index 00000000000..586ac8bc9ec --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt @@ -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 + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt b/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt index 98f788c39f2..6b3e61470e0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt @@ -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 @@ -43,8 +41,7 @@ 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()) @@ -52,32 +49,36 @@ class StartServiceReceiver : BroadcastReceiver() { 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" + } } diff --git a/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt new file mode 100644 index 00000000000..e4ac6157a8d --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt @@ -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>() // 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>) = 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") + } +}