Skip to content

Commit

Permalink
fix: periodic checks for ws service to start if necessary (WPB-6343) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
yamilmedina committed Mar 14, 2024
1 parent d6e94cc commit ce5094b
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 33 deletions.
1 change: 1 addition & 0 deletions app/src/main/kotlin/com/wire/android/WireApplication.kt
Expand Up @@ -71,6 +71,7 @@ class WireApplication : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(wireWorkerFactory.get())
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
}

Expand Down
@@ -0,0 +1,76 @@
/*
* 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/.
*/
@file:Suppress("StringTemplate")

package com.wire.android.feature

import android.content.Context
import android.content.Intent
import android.os.Build
import com.wire.android.appLogger
import com.wire.android.services.PersistentWebSocketService
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class StartPersistentWebsocketIfNecessaryUseCase @Inject constructor(
@ApplicationContext private val appContext: Context,
private val shouldStartPersistentWebSocketService: ShouldStartPersistentWebSocketServiceUseCase
) {
suspend operator fun invoke() {
val persistentWebSocketServiceIntent = PersistentWebSocketService.newIntent(appContext)
shouldStartPersistentWebSocketService().let {
when (it) {
is ShouldStartPersistentWebSocketServiceUseCase.Result.Failure -> {
appLogger.e("${TAG}: Failure while fetching persistent web socket status flow")
}

is ShouldStartPersistentWebSocketServiceUseCase.Result.Success -> {
if (it.shouldStartPersistentWebSocketService) {
startForegroundService(persistentWebSocketServiceIntent)
} else {
appLogger.i("${TAG}: Stopping PersistentWebsocketService, no user with persistent web socket enabled found")
appContext.stopService(persistentWebSocketServiceIntent)
}
}
}
}
}

private fun startForegroundService(persistentWebSocketServiceIntent: Intent) {
when {
PersistentWebSocketService.isServiceStarted -> {
appLogger.i("${TAG}: PersistentWebsocketService already started, not starting again")
}

else -> {
appLogger.i("${TAG}: Starting PersistentWebsocketService")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
appContext.startForegroundService(persistentWebSocketServiceIntent)
} else {
appContext.startService(persistentWebSocketServiceIntent)
}
}
}
}

companion object {
const val TAG = "StartPersistentWebsocketIfNecessaryUseCase"
}
}
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.WorkManager
import com.wire.android.BuildConfig
import com.wire.android.appLogger
import com.wire.android.datastore.GlobalDataStore
Expand All @@ -48,6 +49,8 @@ import com.wire.android.util.deeplink.DeepLinkProcessor
import com.wire.android.util.deeplink.DeepLinkResult
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.UIText
import com.wire.android.workmanager.worker.cancelPeriodicPersistentWebsocketCheckWorker
import com.wire.android.workmanager.worker.enqueuePeriodicPersistentWebsocketCheckWorker
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.kalium.logic.data.auth.AccountInfo
Expand Down Expand Up @@ -112,7 +115,8 @@ class WireActivityViewModel @Inject constructor(
private val currentScreenManager: CurrentScreenManager,
private val observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory,
private val globalDataStore: GlobalDataStore,
private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory
private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory,
private val workManager: WorkManager,
) : ViewModel() {

var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState())
Expand Down Expand Up @@ -462,9 +466,11 @@ class WireActivityViewModel @Inject constructor(
if (statuses.any { it.isPersistentWebSocketEnabled }) {
if (!servicesManager.isPersistentWebSocketServiceRunning()) {
servicesManager.startPersistentWebSocketService()
workManager.enqueuePeriodicPersistentWebsocketCheckWorker()
}
} else {
servicesManager.stopPersistentWebSocketService()
workManager.cancelPeriodicPersistentWebsocketCheckWorker()
}
}
}
Expand Down
Expand Up @@ -21,10 +21,8 @@ package com.wire.android.ui.debug
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import com.wire.android.appLogger
import com.wire.android.feature.ShouldStartPersistentWebSocketServiceUseCase
import com.wire.android.services.PersistentWebSocketService
import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase
import com.wire.android.util.dispatchers.DispatcherProvider
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
Expand All @@ -41,41 +39,15 @@ class StartServiceReceiver : BroadcastReceiver() {
lateinit var dispatcherProvider: DispatcherProvider

@Inject
lateinit var shouldStartPersistentWebSocketServiceUseCase: ShouldStartPersistentWebSocketServiceUseCase
lateinit var startPersistentWebSocketService: StartPersistentWebsocketIfNecessaryUseCase

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

override fun onReceive(context: Context?, intent: Intent?) {
val persistentWebSocketServiceIntent = PersistentWebSocketService.newIntent(context)
appLogger.i("$TAG: onReceive called with action ${intent?.action}")
scope.launch {
shouldStartPersistentWebSocketServiceUseCase().let {
when (it) {
is ShouldStartPersistentWebSocketServiceUseCase.Result.Failure -> {
appLogger.e("$TAG: Failure while fetching persistent web socket status flow")
}
is ShouldStartPersistentWebSocketServiceUseCase.Result.Success -> {
if (it.shouldStartPersistentWebSocketService) {
if (PersistentWebSocketService.isServiceStarted) {
appLogger.i("$TAG: PersistentWebsocketService already started, not starting again")
} else {
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)
}
}
}
}
}
scope.launch { startPersistentWebSocketService() }
}

companion object {
Expand Down
Expand Up @@ -23,11 +23,13 @@ import androidx.work.ListenableWorker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase
import com.wire.android.migration.MigrationManager
import com.wire.android.notification.NotificationChannelsManager
import com.wire.android.notification.WireNotificationManager
import com.wire.android.workmanager.worker.MigrationWorker
import com.wire.android.workmanager.worker.NotificationFetchWorker
import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker
import com.wire.android.workmanager.worker.SingleUserMigrationWorker
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.sync.WrapperWorker
Expand All @@ -38,6 +40,7 @@ class WireWorkerFactory @Inject constructor(
private val wireNotificationManager: WireNotificationManager,
private val notificationChannelsManager: NotificationChannelsManager,
private val migrationManager: MigrationManager,
private val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase,
@KaliumCoreLogic
private val coreLogic: CoreLogic
) : WorkerFactory() {
Expand All @@ -47,12 +50,19 @@ class WireWorkerFactory @Inject constructor(
WrapperWorker::class.java.canonicalName ->
WrapperWorkerFactory(coreLogic, WireForegroundNotificationDetailsProvider)
.createWorker(appContext, workerClassName, workerParameters)

NotificationFetchWorker::class.java.canonicalName ->
NotificationFetchWorker(appContext, workerParameters, wireNotificationManager, notificationChannelsManager)

MigrationWorker::class.java.canonicalName ->
MigrationWorker(appContext, workerParameters, migrationManager, notificationChannelsManager)

SingleUserMigrationWorker::class.java.canonicalName ->
SingleUserMigrationWorker(appContext, workerParameters, migrationManager, notificationChannelsManager)

PersistentWebsocketCheckWorker::class.java.canonicalName ->
PersistentWebsocketCheckWorker(appContext, workerParameters, startPersistentWebsocketIfNecessary)

else -> null
}
}
Expand Down
@@ -0,0 +1,74 @@
/*
* 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/.
*/
@file:Suppress("StringTemplate")

package com.wire.android.workmanager.worker

import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.wire.android.appLogger
import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase
import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.NAME
import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.TAG
import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.WORK_INTERVAL
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.coroutineScope
import kotlin.time.Duration.Companion.hours
import kotlin.time.toJavaDuration

@HiltWorker
class PersistentWebsocketCheckWorker
@AssistedInject constructor(
@Assisted private val appContext: Context,
@Assisted private val workerParams: WorkerParameters,
private val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase
) : CoroutineWorker(appContext, workerParams) {

override suspend fun doWork(): Result = coroutineScope {
appLogger.i("${TAG}: Starting periodic work check for persistent websocket connection")
startPersistentWebsocketIfNecessary()
Result.success()
}

companion object {
const val NAME = "wss_check_worker"
const val TAG = "PersistentWebsocketCheckWorker"
val WORK_INTERVAL = 24.hours.toJavaDuration()
}
}

fun WorkManager.enqueuePeriodicPersistentWebsocketCheckWorker() {
appLogger.i("${TAG}: Enqueueing periodic work for $TAG")
enqueueUniquePeriodicWork(
NAME, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<PersistentWebsocketCheckWorker>(WORK_INTERVAL)
.addTag(TAG) // adds the tag so we can cancel later all related work.
.build()
)
}

fun WorkManager.cancelPeriodicPersistentWebsocketCheckWorker() {
appLogger.i("${TAG}: Cancelling all periodic scheduled work for the tag $TAG")
cancelAllWorkByTag(TAG)
}
@@ -0,0 +1,86 @@
/*
* 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 android.content.ComponentName
import android.content.Context
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test

class StartPersistentWebsocketIfNecessaryUseCaseTest {

@Test
fun givenShouldStartPersistentWebsocketTrue_whenInvoking_thenStartService() =
runTest {
// given
val (arrangement, sut) = Arrangement()
.withShouldStartPersistentWebsocketServiceResult(true)
.arrange()

// when
sut.invoke()

// then
verify(exactly = 1) { arrangement.applicationContext.startService(any()) }
}

@Test
fun givenShouldStartPersistentWebsocketFalse_whenInvoking_thenDONTStartService() =
runTest {
// given
val (arrangement, sut) = Arrangement()
.withShouldStartPersistentWebsocketServiceResult(false)
.arrange()

// when
sut.invoke()

// then
verify(exactly = 0) { arrangement.applicationContext.startService(any()) }
}

inner class Arrangement {

@MockK
lateinit var shouldStartPersistentWebSocketServiceUseCase: ShouldStartPersistentWebSocketServiceUseCase

@MockK
lateinit var applicationContext: Context

init {
MockKAnnotations.init(this, relaxUnitFun = true)
every { applicationContext.startService(any()) } returns ComponentName.createRelative("dummy", "class")
every { applicationContext.stopService(any()) } returns true
}

fun arrange() = this to StartPersistentWebsocketIfNecessaryUseCase(
applicationContext,
shouldStartPersistentWebSocketServiceUseCase
)

fun withShouldStartPersistentWebsocketServiceResult(shouldStart: Boolean) = apply {
coEvery { shouldStartPersistentWebSocketServiceUseCase.invoke() } returns
ShouldStartPersistentWebSocketServiceUseCase.Result.Success(shouldStart)
}
}
}

0 comments on commit ce5094b

Please sign in to comment.