diff --git a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt index fd15acb73f3..8066f7473ac 100644 --- a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt @@ -21,6 +21,7 @@ package com.wire.android.di import android.content.Context import android.os.Build import com.wire.android.BuildConfig +import com.wire.android.emm.ManagedConfigurationsManager import com.wire.android.util.isWebsocketEnabledByDefault import com.wire.kalium.logic.featureFlags.BuildFileRestrictionState import com.wire.kalium.logic.featureFlags.KaliumConfigs @@ -34,7 +35,10 @@ import dagger.hilt.components.SingletonComponent class KaliumConfigsModule { @Provides - fun provideKaliumConfigs(context: Context): KaliumConfigs { + fun provideKaliumConfigs( + context: Context, + managedConfigurationsManager: ManagedConfigurationsManager + ): KaliumConfigs { val fileRestriction: BuildFileRestrictionState = if (BuildConfig.FILE_RESTRICTION_ENABLED) { BuildConfig.FILE_RESTRICTION_LIST.split(",").map { it.trim() }.let { BuildFileRestrictionState.AllowSome(it) @@ -57,7 +61,10 @@ class KaliumConfigsModule { wipeOnCookieInvalid = BuildConfig.WIPE_ON_COOKIE_INVALID, wipeOnDeviceRemoval = BuildConfig.WIPE_ON_DEVICE_REMOVAL, wipeOnRootedDevice = BuildConfig.WIPE_ON_ROOTED_DEVICE, - isWebSocketEnabledByDefault = isWebsocketEnabledByDefault(context), + isWebSocketEnabledByDefault = isWebsocketEnabledByDefault( + context, + managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value + ), certPinningConfig = BuildConfig.CERTIFICATE_PINNING_CONFIG, maxRemoteSearchResultCount = BuildConfig.MAX_REMOTE_SEARCH_RESULT_COUNT, limitTeamMembersFetchDuringSlowSync = BuildConfig.LIMIT_TEAM_MEMBERS_FETCH_DURING_SLOW_SYNC, diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsKeys.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsKeys.kt index a29bec76371..8b9984f1829 100644 --- a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsKeys.kt +++ b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsKeys.kt @@ -19,7 +19,8 @@ package com.wire.android.emm enum class ManagedConfigurationsKeys { DEFAULT_SERVER_URLS, - SSO_CODE; + SSO_CODE, + KEEP_WEBSOCKET_CONNECTION; fun asKey() = name.lowercase() } diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsManager.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsManager.kt index dc285e16b44..d67804ae1a7 100644 --- a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsManager.kt +++ b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsManager.kt @@ -24,6 +24,9 @@ import com.wire.android.config.ServerConfigProvider import com.wire.android.util.EMPTY import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.configuration.server.ServerConfig +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.util.concurrent.atomic.AtomicReference @@ -66,6 +69,19 @@ interface ManagedConfigurationsManager { * empty if no config found or cleared, or failure with reason. */ suspend fun refreshSSOCodeConfig(): SSOCodeConfigResult + + /** + * Whether persistent WebSocket connection is enforced by MDM. + * When true, the persistent WebSocket service should always be started + * and users should not be able to change this setting. + */ + val persistentWebSocketEnforcedByMDM: StateFlow + + /** + * Refresh the persistent WebSocket configuration from managed restrictions. + * This should be called when the app starts or when broadcast receiver triggers. + */ + suspend fun refreshPersistentWebSocketConfig() } internal class ManagedConfigurationsManagerImpl( @@ -82,6 +98,7 @@ internal class ManagedConfigurationsManagerImpl( private val _currentServerConfig = AtomicReference(null) private val _currentSSOCodeConfig = AtomicReference(String.EMPTY) + private val _persistentWebSocketEnforcedByMDM = MutableStateFlow(false) override val currentServerConfig: ServerConfig.Links get() = _currentServerConfig.get() ?: serverConfigProvider.getDefaultServerConfig() @@ -89,6 +106,9 @@ internal class ManagedConfigurationsManagerImpl( override val currentSSOCodeConfig: String get() = _currentSSOCodeConfig.get() + override val persistentWebSocketEnforcedByMDM: StateFlow + get() = _persistentWebSocketEnforcedByMDM.asStateFlow() + override suspend fun refreshServerConfig(): ServerConfigResult = withContext(dispatchers.io()) { val managedServerConfig = getServerConfig() val serverConfig: ServerConfig.Links = when (managedServerConfig) { @@ -118,6 +138,22 @@ internal class ManagedConfigurationsManagerImpl( managedSSOCodeConfig } + override suspend fun refreshPersistentWebSocketConfig() { + withContext(dispatchers.io()) { + val restrictions = restrictionsManager.applicationRestrictions + val isEnforced = if (restrictions == null || restrictions.isEmpty) { + false + } else { + restrictions.getBoolean( + ManagedConfigurationsKeys.KEEP_WEBSOCKET_CONNECTION.asKey(), + false + ) + } + _persistentWebSocketEnforcedByMDM.value = isEnforced + logger.i("Persistent WebSocket enforced by MDM refreshed to: $isEnforced") + } + } + private suspend fun getSSOCodeConfig(): SSOCodeConfigResult = withContext(dispatchers.io()) { val restrictions = restrictionsManager.applicationRestrictions diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReceiver.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReceiver.kt index f1ac6288862..9ae901a5eec 100644 --- a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReceiver.kt @@ -21,8 +21,12 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.wire.android.appLogger +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase import com.wire.android.util.EMPTY import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.CoreLogic +import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -33,6 +37,8 @@ import javax.inject.Singleton class ManagedConfigurationsReceiver @Inject constructor( private val managedConfigurationsManager: ManagedConfigurationsManager, private val managedConfigurationsReporter: ManagedConfigurationsReporter, + @KaliumCoreLogic private val coreLogic: Lazy, + private val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase, dispatcher: DispatcherProvider ) : BroadcastReceiver() { @@ -48,6 +54,7 @@ class ManagedConfigurationsReceiver @Inject constructor( logger.i("Received intent to refresh managed configurations") updateServerConfig() updateSSOCodeConfig() + updatePersistentWebSocketConfig() } } @@ -103,6 +110,26 @@ class ManagedConfigurationsReceiver @Inject constructor( } } + private suspend fun updatePersistentWebSocketConfig() { + val wasEnforced = managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value + managedConfigurationsManager.refreshPersistentWebSocketConfig() + val isEnforced = managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value + + // Only bulk update when MDM enforcement turns ON + if (!wasEnforced && isEnforced) { + coreLogic.get().getGlobalScope().setAllPersistentWebSocketEnabled(true) + } + + // Trigger service start/stop based on current state + startPersistentWebsocketIfNecessary() + + managedConfigurationsReporter.reportAppliedState( + key = ManagedConfigurationsKeys.KEEP_WEBSOCKET_CONNECTION.asKey(), + message = if (isEnforced) "Persistent WebSocket enforced" else "Persistent WebSocket not enforced", + data = isEnforced.toString() + ) + } + companion object { private const val TAG = "ManagedConfigurationsReceiver" } diff --git a/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt index 9239a77586a..5d6ebdd8ba3 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt @@ -18,6 +18,7 @@ package com.wire.android.feature import com.wire.android.di.KaliumCoreLogic +import com.wire.android.emm.ManagedConfigurationsManager import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import kotlinx.coroutines.flow.firstOrNull @@ -27,9 +28,15 @@ import javax.inject.Singleton @Singleton class ShouldStartPersistentWebSocketServiceUseCase @Inject constructor( - @KaliumCoreLogic private val coreLogic: CoreLogic + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val managedConfigurationsManager: ManagedConfigurationsManager ) { suspend operator fun invoke(): Result { + // MDM takes priority - if enforced, always start service + if (managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value) { + return Result.Success(true) + } + return coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus().let { result -> when (result) { is ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure -> Result.Failure diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 3676d2dcc79..7e1964df887 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -224,6 +224,7 @@ class WireActivity : AppCompatActivity() { lifecycleScope.launch(Dispatchers.IO) { managedConfigurationsManager.refreshServerConfig() managedConfigurationsManager.refreshSSOCodeConfig() + managedConfigurationsManager.refreshPersistentWebSocketConfig() } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt index 6b74b22988d..b92699177c7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt @@ -49,6 +49,7 @@ fun NetworkSettingsScreen( NetworkSettingsScreenContent( onBackPressed = navigator::navigateBack, isWebSocketEnabled = networkSettingsViewModel.networkSettingsState.isPersistentWebSocketConnectionEnabled, + isEnforcedByMDM = networkSettingsViewModel.networkSettingsState.isEnforcedByMDM, setWebSocketState = { networkSettingsViewModel.setWebSocketState(it) }, ) } @@ -57,6 +58,7 @@ fun NetworkSettingsScreen( fun NetworkSettingsScreenContent( onBackPressed: () -> Unit, isWebSocketEnabled: Boolean, + isEnforcedByMDM: Boolean, setWebSocketState: (Boolean) -> Unit, modifier: Modifier = Modifier ) { @@ -79,20 +81,26 @@ fun NetworkSettingsScreenContent( val isWebSocketEnforcedByDefault = remember { isWebsocketEnabledByDefault(appContext) } - val switchState = remember(isWebSocketEnabled) { - if (isWebSocketEnforcedByDefault) { - SwitchState.TextOnly(true) - } else { - SwitchState.Enabled( + val switchState = remember(isWebSocketEnabled, isEnforcedByMDM) { + when { + isEnforcedByMDM -> SwitchState.TextOnly(true) + isWebSocketEnforcedByDefault -> SwitchState.TextOnly(true) + else -> SwitchState.Enabled( value = isWebSocketEnabled, onCheckedChange = setWebSocketState ) } } + val subtitle = if (isEnforcedByMDM) { + stringResource(R.string.settings_keep_websocket_enforced_by_organization) + } else { + stringResource(R.string.settings_keep_connection_to_websocket_description) + } + GroupConversationOptionsItem( title = stringResource(R.string.settings_keep_connection_to_websocket), - subtitle = stringResource(R.string.settings_keep_connection_to_websocket_description), + subtitle = subtitle, switchState = switchState, arrowType = ArrowType.NONE ) @@ -106,6 +114,7 @@ fun PreviewNetworkSettingsScreen() = WireTheme { NetworkSettingsScreenContent( onBackPressed = {}, isWebSocketEnabled = true, + isEnforcedByMDM = false, setWebSocketState = {}, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsState.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsState.kt index 9a08cbb5516..2c1c8d6d997 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsState.kt @@ -19,5 +19,6 @@ package com.wire.android.ui.home.settings.appsettings.networkSettings data class NetworkSettingsState( - val isPersistentWebSocketConnectionEnabled: Boolean = false + val isPersistentWebSocketConnectionEnabled: Boolean = false, + val isEnforcedByMDM: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt index afead2d62d6..648ddf20c8f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger +import com.wire.android.emm.ManagedConfigurationsManager import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase @@ -37,12 +38,29 @@ class NetworkSettingsViewModel @Inject constructor( private val persistPersistentWebSocketConnectionStatus: PersistPersistentWebSocketConnectionStatusUseCase, private val observePersistentWebSocketConnectionStatus: ObservePersistentWebSocketConnectionStatusUseCase, - private val currentSession: CurrentSessionUseCase + private val currentSession: CurrentSessionUseCase, + private val managedConfigurationsManager: ManagedConfigurationsManager ) : ViewModel() { var networkSettingsState by mutableStateOf(NetworkSettingsState()) init { observePersistentWebSocketConnection() + observeMDMEnforcement() + } + + private fun observeMDMEnforcement() { + viewModelScope.launch { + managedConfigurationsManager.persistentWebSocketEnforcedByMDM.collect { isEnforced -> + networkSettingsState = networkSettingsState.copy( + isEnforcedByMDM = isEnforced, + isPersistentWebSocketConnectionEnabled = if (isEnforced) { + true + } else { + networkSettingsState.isPersistentWebSocketConnectionEnabled + } + ) + } + } } private fun observePersistentWebSocketConnection() = @@ -57,6 +75,7 @@ class NetworkSettingsViewModel is ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure -> { appLogger.e("Failure while fetching persistent web socket status flow from network settings") } + is ObservePersistentWebSocketConnectionStatusUseCase.Result.Success -> { it.persistentWebSocketStatusListFlow.collect { it.map { persistentWebSocketStatus -> @@ -64,7 +83,7 @@ class NetworkSettingsViewModel networkSettingsState = networkSettingsState.copy( isPersistentWebSocketConnectionEnabled = - persistentWebSocketStatus.isPersistentWebSocketEnabled + persistentWebSocketStatus.isPersistentWebSocketEnabled ) } } @@ -73,6 +92,7 @@ class NetworkSettingsViewModel } } } + else -> { // NO SESSION - Nothing to do } @@ -80,6 +100,10 @@ class NetworkSettingsViewModel } fun setWebSocketState(isEnabled: Boolean) { + // Block changes when MDM enforces the setting + if (networkSettingsState.isEnforcedByMDM) { + return + } viewModelScope.launch { persistPersistentWebSocketConnectionStatus(isEnabled) } diff --git a/app/src/main/kotlin/com/wire/android/util/WebsocketHelper.kt b/app/src/main/kotlin/com/wire/android/util/WebsocketHelper.kt index e89f83d3a7f..fc1d27cd01e 100644 --- a/app/src/main/kotlin/com/wire/android/util/WebsocketHelper.kt +++ b/app/src/main/kotlin/com/wire/android/util/WebsocketHelper.kt @@ -22,8 +22,16 @@ import com.wire.android.BuildConfig import com.wire.android.util.extension.isGoogleServicesAvailable /** - * If [BuildConfig.WEBSOCKET_ENABLED_BY_DEFAULT] is true, the websocket should be enabled by default always. - * Otherwise, it should be enabled by default only if Google Play Services are not available. + * Determines if websocket should be enabled by default. + * + * Returns true if: + * - MDM enforces persistent websocket, OR + * - [BuildConfig.WEBSOCKET_ENABLED_BY_DEFAULT] is true, OR + * - Google Play Services are not available */ -fun isWebsocketEnabledByDefault(context: Context) = - BuildConfig.WEBSOCKET_ENABLED_BY_DEFAULT || !context.isGoogleServicesAvailable() +fun isWebsocketEnabledByDefault( + context: Context, + persistentWebSocketEnforcedByMDM: Boolean = false +) = persistentWebSocketEnforcedByMDM || + BuildConfig.WEBSOCKET_ENABLED_BY_DEFAULT || + !context.isGoogleServicesAvailable() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c1856d9e18..3f7ae4749d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1875,6 +1875,9 @@ In group conversations, the group admin can overwrite this setting. SSO code configuration JSON value with the server endpoints configuration JSON value with the default SSO code configuration + Keep WebSocket Connection + Force persistent WebSocket connection for all users + This setting is managed by your organization. Channels are available for team members. diff --git a/app/src/main/res/xml/app_restrictions.xml b/app/src/main/res/xml/app_restrictions.xml index c43e1117b30..e6317d49ed6 100644 --- a/app/src/main/res/xml/app_restrictions.xml +++ b/app/src/main/res/xml/app_restrictions.xml @@ -31,4 +31,12 @@ android:restrictionType="string" android:title="@string/restriction_sso_code_title" /> + + + diff --git a/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsManagerTest.kt b/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsManagerTest.kt index 328b04b553a..ba18717072b 100644 --- a/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsManagerTest.kt @@ -126,6 +126,36 @@ class ManagedConfigurationsManagerTest { assertEquals(ServerConfigProvider().getDefaultServerConfig(), serverConfig) } + @Test + fun `given keep_websocket_connection is true, then persistentWebSocketEnforcedByMDM returns true`() = runTest { + val (_, manager) = Arrangement() + .withBooleanRestrictions(mapOf(ManagedConfigurationsKeys.KEEP_WEBSOCKET_CONNECTION.asKey() to true)) + .arrange() + + manager.refreshPersistentWebSocketConfig() + assertEquals(true, manager.persistentWebSocketEnforcedByMDM.value) + } + + @Test + fun `given keep_websocket_connection is false, then persistentWebSocketEnforcedByMDM returns false`() = runTest { + val (_, manager) = Arrangement() + .withBooleanRestrictions(mapOf(ManagedConfigurationsKeys.KEEP_WEBSOCKET_CONNECTION.asKey() to false)) + .arrange() + + manager.refreshPersistentWebSocketConfig() + assertEquals(false, manager.persistentWebSocketEnforcedByMDM.value) + } + + @Test + fun `given no keep_websocket_connection restriction, then persistentWebSocketEnforcedByMDM returns false`() = runTest { + val (_, manager) = Arrangement() + .withRestrictions(emptyMap()) + .arrange() + + manager.refreshPersistentWebSocketConfig() + assertEquals(false, manager.persistentWebSocketEnforcedByMDM.value) + } + private class Arrangement { private val context: Context = ApplicationProvider.getApplicationContext() @@ -143,6 +173,19 @@ class ManagedConfigurationsManagerTest { ) } + fun withBooleanRestrictions(restrictions: Map) = apply { + val restrictionsManager = + context.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + val shadowRestrictionsManager = Shadows.shadowOf(restrictionsManager) + shadowRestrictionsManager.setApplicationRestrictions( + Bundle().apply { + restrictions.forEach { (key, value) -> + putBoolean(key, value) + } + } + ) + } + fun arrange() = this to ManagedConfigurationsManagerImpl( context = context, serverConfigProvider = ServerConfigProvider(), diff --git a/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsReceiverTest.kt b/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsReceiverTest.kt index 96de544b576..5c2bd38ce85 100644 --- a/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsReceiverTest.kt +++ b/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsReceiverTest.kt @@ -22,10 +22,15 @@ import android.content.Context import android.content.Intent import androidx.test.core.app.ApplicationProvider import com.wire.android.config.TestDispatcherProvider +import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase import com.wire.android.util.EMPTY +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.GlobalKaliumScope import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test @@ -63,6 +68,14 @@ class ManagedConfigurationsReceiverTest { any() ) } + coVerify(exactly = 1) { arrangement.managedConfigurationsManager.refreshPersistentWebSocketConfig() } + coVerify(exactly = 1) { + arrangement.managedConfigurationsReporter.reportAppliedState( + eq(ManagedConfigurationsKeys.KEEP_WEBSOCKET_CONNECTION.asKey()), + any(), + any() + ) + } } @Test @@ -177,14 +190,67 @@ class ManagedConfigurationsReceiverTest { coVerify(exactly = 0) { arrangement.managedConfigurationsManager.refreshSSOCodeConfig() } } + @Test + fun `given websocket enforcement turns ON, when onReceive is called, then bulk enable persistent websocket and trigger service`() = + runTest { + val (arrangement, receiver) = Arrangement() + .withIntent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) + .withPersistentWebSocketTransition(from = false, to = true) + .arrange() + + receiver.onReceive(arrangement.context, arrangement.intent) + advanceUntilIdle() + + coVerify(exactly = 1) { arrangement.globalScope.setAllPersistentWebSocketEnabled(true) } + coVerify(exactly = 1) { arrangement.startPersistentWebsocketIfNecessary.invoke() } + coVerify(exactly = 1) { + arrangement.managedConfigurationsReporter.reportAppliedState( + eq(ManagedConfigurationsKeys.KEEP_WEBSOCKET_CONNECTION.asKey()), + eq("Persistent WebSocket enforced"), + eq("true") + ) + } + } + + @Test + fun `given websocket enforcement turns OFF, when onReceive is called, then do not bulk enable and trigger service`() = + runTest { + val (arrangement, receiver) = Arrangement() + .withIntent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) + .withPersistentWebSocketTransition(from = true, to = false) + .arrange() + + receiver.onReceive(arrangement.context, arrangement.intent) + advanceUntilIdle() + + coVerify(exactly = 0) { arrangement.globalScope.setAllPersistentWebSocketEnabled(true) } + coVerify(exactly = 1) { arrangement.startPersistentWebsocketIfNecessary.invoke() } + coVerify(exactly = 1) { + arrangement.managedConfigurationsReporter.reportAppliedState( + eq(ManagedConfigurationsKeys.KEEP_WEBSOCKET_CONNECTION.asKey()), + eq("Persistent WebSocket not enforced"), + eq("false") + ) + } + } + private class Arrangement { val context: Context = ApplicationProvider.getApplicationContext() val managedConfigurationsManager: ManagedConfigurationsManager = mockk(relaxed = true) val managedConfigurationsReporter: ManagedConfigurationsReporter = mockk(relaxed = true) + val coreLogic: CoreLogic = mockk(relaxed = true) + val globalScope: GlobalKaliumScope = mockk(relaxed = true) + val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase = mockk(relaxed = true) private val dispatchers = TestDispatcherProvider() + private val persistentWebSocketEnforcedFlow = MutableStateFlow(false) lateinit var intent: Intent + init { + every { managedConfigurationsManager.persistentWebSocketEnforcedByMDM } returns persistentWebSocketEnforcedFlow + every { coreLogic.getGlobalScope() } returns globalScope + } + fun withIntent(action: String?) = apply { intent = if (action != null) Intent(action) else Intent() } @@ -197,9 +263,22 @@ class ManagedConfigurationsReceiverTest { coEvery { managedConfigurationsManager.refreshSSOCodeConfig() } returns result } + fun withPersistentWebSocketEnforced(enforced: Boolean) = apply { + persistentWebSocketEnforcedFlow.value = enforced + } + + fun withPersistentWebSocketTransition(from: Boolean, to: Boolean) = apply { + persistentWebSocketEnforcedFlow.value = from + coEvery { managedConfigurationsManager.refreshPersistentWebSocketConfig() } answers { + persistentWebSocketEnforcedFlow.value = to + } + } + fun arrange() = this to ManagedConfigurationsReceiver( managedConfigurationsManager, managedConfigurationsReporter, + lazy { coreLogic }, + startPersistentWebsocketIfNecessary, dispatchers ) } diff --git a/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt index 79b46b87bb2..7015d99784a 100644 --- a/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt @@ -17,15 +17,18 @@ */ package com.wire.android.feature +import com.wire.android.emm.ManagedConfigurationsManager 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.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy @@ -126,17 +129,70 @@ class ShouldStartPersistentWebSocketServiceUseCaseTest { assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Failure::class.java, result) } + @Test + fun givenMDMEnforcesPersistentWebSocket_whenInvoking_shouldReturnSuccessTrue() = + runTest { + // given + val (_, useCase) = Arrangement() + .withMDMEnforcedPersistentWebSocket(true) + .arrange() + // when + val result = useCase.invoke() + // then + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also { + assertEquals(true, it.shouldStartPersistentWebSocketService) + } + } + + @Test + fun givenMDMEnforcesPersistentWebSocket_whenInvoking_shouldNotCheckUserPreferences() = + runTest { + // given - MDM enforces, but user has it disabled + val (_, useCase) = Arrangement() + .withMDMEnforcedPersistentWebSocket(true) + .withObservePersistentWebSocketConnectionStatusSuccess(flowOf(listOf(PersistentWebSocketStatus(userId, false)))) + .arrange() + // when + val result = useCase.invoke() + // then - should still return true because MDM takes priority + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also { + assertEquals(true, it.shouldStartPersistentWebSocketService) + } + } + + @Test + fun givenMDMDoesNotEnforcePersistentWebSocket_whenInvoking_shouldCheckUserPreferences() = + runTest { + // given - MDM does not enforce, user has it disabled + val (_, useCase) = Arrangement() + .withMDMEnforcedPersistentWebSocket(false) + .withObservePersistentWebSocketConnectionStatusSuccess(flowOf(listOf(PersistentWebSocketStatus(userId, false)))) + .arrange() + // when + val result = useCase.invoke() + // then - should return false because user has it disabled + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also { + assertEquals(false, it.shouldStartPersistentWebSocketService) + } + } + inner class Arrangement { @MockK private lateinit var coreLogic: CoreLogic + @MockK + private lateinit var managedConfigurationsManager: ManagedConfigurationsManager + + private val persistentWebSocketEnforcedByMDMFlow = MutableStateFlow(false) + val useCase by lazy { - ShouldStartPersistentWebSocketServiceUseCase(coreLogic) + ShouldStartPersistentWebSocketServiceUseCase(coreLogic, managedConfigurationsManager) } init { MockKAnnotations.init(this, relaxUnitFun = true) + every { managedConfigurationsManager.persistentWebSocketEnforcedByMDM } returns persistentWebSocketEnforcedByMDMFlow } fun arrange() = this to useCase @@ -149,6 +205,9 @@ class ShouldStartPersistentWebSocketServiceUseCaseTest { coEvery { coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus() } returns ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure.StorageFailure } + fun withMDMEnforcedPersistentWebSocket(enforced: Boolean) = apply { + persistentWebSocketEnforcedByMDMFlow.value = enforced + } } companion object { diff --git a/app/src/test/kotlin/com/wire/android/util/WebsocketHelperTest.kt b/app/src/test/kotlin/com/wire/android/util/WebsocketHelperTest.kt new file mode 100644 index 00000000000..2325befd87b --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/util/WebsocketHelperTest.kt @@ -0,0 +1,49 @@ +package com.wire.android.util + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.wire.android.BuildConfig +import io.mockk.coEvery +import io.mockk.mockkStatic +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class WebsocketHelperTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun `when MDM enforces persistent websocket, isWebsocketEnabledByDefault returns true`() { + mockkStatic("com.wire.android.util.extension.GoogleServicesKt") + coEvery { context.isGoogleServicesAvailable() } returns true + + val result = isWebsocketEnabledByDefault(context, persistentWebSocketEnforcedByMDM = true) + assertTrue(result) + } + + @Test + fun `when GMS not available and MDM not enforced, isWebsocketEnabledByDefault returns true`() { + mockkStatic("com.wire.android.util.extension.GoogleServicesKt") + coEvery { context.isGoogleServicesAvailable() } returns false + + val result = isWebsocketEnabledByDefault(context, persistentWebSocketEnforcedByMDM = false) + assertTrue(result) + } + + @Test + fun `when GMS available and MDM not enforced, isWebsocketEnabledByDefault matches BuildConfig flag`() { + mockkStatic("com.wire.android.util.extension.GoogleServicesKt") + coEvery { context.isGoogleServicesAvailable() } returns true + + val result = isWebsocketEnabledByDefault(context, persistentWebSocketEnforcedByMDM = false) + assertEquals(BuildConfig.WEBSOCKET_ENABLED_BY_DEFAULT, result) + } +} + diff --git a/kalium b/kalium index ae71a9a75ae..30091ea067f 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ae71a9a75ae63027c8970905ef5bdeabc59fee8b +Subproject commit 30091ea067f16317f3234371fb51c4c8f998913b