Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
private val ENTER_TO_SENT = booleanPreferencesKey("enter_to_sent")
private val ANONYMOUS_REGISTRATION_TRACK_ID = stringPreferencesKey("anonymous_registration_track_id")
private val IS_ANONYMOUS_REGISTRATION_ENABLED = booleanPreferencesKey("is_anonymous_registration_enabled")
private val PERSISTENT_WEBSOCKET_ENFORCED_BY_MDM = booleanPreferencesKey("persistent_websocket_enforced_by_mdm")

val APP_THEME_OPTION = stringPreferencesKey("app_theme_option")
val RECORD_AUDIO_EFFECTS_CHECKBOX = booleanPreferencesKey("record_audio_effects_checkbox")
Expand Down Expand Up @@ -205,4 +206,11 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
suspend fun setEnterToSend(enabled: Boolean) {
context.dataStore.edit { it[ENTER_TO_SENT] = enabled }
}

fun isPersistentWebSocketEnforcedByMDM(): Flow<Boolean> =
getBooleanPreference(PERSISTENT_WEBSOCKET_ENFORCED_BY_MDM, false)

suspend fun setPersistentWebSocketEnforcedByMDM(enforced: Boolean) {
context.dataStore.edit { it[PERSISTENT_WEBSOCKET_ENFORCED_BY_MDM] = enforced }
}
}
10 changes: 8 additions & 2 deletions app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package com.wire.android.di
import android.content.Context
import androidx.work.WorkManager
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.emm.ManagedConfigurationsManager
import com.wire.android.util.ImageUtil
import com.wire.android.util.UserAgentProvider
import com.wire.android.util.isWebsocketEnabledByDefault
Expand Down Expand Up @@ -152,8 +153,13 @@ class CoreLogicModule {

@DefaultWebSocketEnabledByDefault
@Provides
fun provideDefaultWebSocketEnabledByDefault(@ApplicationContext context: Context): Boolean =
isWebsocketEnabledByDefault(context)
fun provideDefaultWebSocketEnabledByDefault(
@ApplicationContext context: Context,
managedConfigurationsManager: ManagedConfigurationsManager
): Boolean = isWebsocketEnabledByDefault(
context,
managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value
)
}

@Module
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.wire.android.di
import android.content.Context
import com.wire.android.BuildConfig
import com.wire.android.config.ServerConfigProvider
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.emm.ManagedConfigurationsManager
import com.wire.android.emm.ManagedConfigurationsManagerImpl
import com.wire.android.util.EMPTY
Expand All @@ -46,9 +47,10 @@ class ManagedConfigurationsModule {
fun provideManagedConfigurationsRepository(
@ApplicationContext context: Context,
dispatcherProvider: DispatcherProvider,
serverConfigProvider: ServerConfigProvider
serverConfigProvider: ServerConfigProvider,
globalDataStore: GlobalDataStore
): ManagedConfigurationsManager {
return ManagedConfigurationsManagerImpl(context, dispatcherProvider, serverConfigProvider)
return ManagedConfigurationsManagerImpl(context, dispatcherProvider, serverConfigProvider, globalDataStore)
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ import android.content.Context
import android.content.RestrictionsManager
import com.wire.android.appLogger
import com.wire.android.config.ServerConfigProvider
import com.wire.android.datastore.GlobalDataStore
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.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.util.concurrent.atomic.AtomicReference
Expand Down Expand Up @@ -66,12 +72,26 @@ 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<Boolean>

/**
* Refresh the persistent WebSocket configuration from managed restrictions.
* This should be called when the app starts or when broadcast receiver triggers.
*/
suspend fun refreshPersistentWebSocketConfig(): Boolean
}

internal class ManagedConfigurationsManagerImpl(
private val context: Context,
private val dispatchers: DispatcherProvider,
private val serverConfigProvider: ServerConfigProvider,
private val globalDataStore: GlobalDataStore,
) : ManagedConfigurationsManager {

private val json: Json = Json { ignoreUnknownKeys = true }
Expand All @@ -82,13 +102,19 @@ internal class ManagedConfigurationsManagerImpl(

private val _currentServerConfig = AtomicReference<ServerConfig.Links?>(null)
private val _currentSSOCodeConfig = AtomicReference(String.EMPTY)
private val _persistentWebSocketEnforcedByMDM by lazy {
MutableStateFlow(runBlocking { globalDataStore.isPersistentWebSocketEnforcedByMDM().first() })
}

override val currentServerConfig: ServerConfig.Links
get() = _currentServerConfig.get() ?: serverConfigProvider.getDefaultServerConfig()

override val currentSSOCodeConfig: String
get() = _currentSSOCodeConfig.get()

override val persistentWebSocketEnforcedByMDM: StateFlow<Boolean>
get() = _persistentWebSocketEnforcedByMDM.asStateFlow()

override suspend fun refreshServerConfig(): ServerConfigResult = withContext(dispatchers.io()) {
val managedServerConfig = getServerConfig()
val serverConfig: ServerConfig.Links = when (managedServerConfig) {
Expand Down Expand Up @@ -118,6 +144,24 @@ internal class ManagedConfigurationsManagerImpl(
managedSSOCodeConfig
}

override suspend fun refreshPersistentWebSocketConfig(): Boolean {
return 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
globalDataStore.setPersistentWebSocketEnforcedByMDM(isEnforced)
logger.i("Persistent WebSocket enforced by MDM refreshed to: $isEnforced")
isEnforced
}
}

private suspend fun getSSOCodeConfig(): SSOCodeConfigResult =
withContext(dispatchers.io()) {
val restrictions = restrictionsManager.applicationRestrictions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<CoreLogic>,
private val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase,
dispatcher: DispatcherProvider
) : BroadcastReceiver() {

Expand All @@ -48,6 +54,7 @@ class ManagedConfigurationsReceiver @Inject constructor(
logger.i("Received intent to refresh managed configurations")
updateServerConfig()
updateSSOCodeConfig()
updatePersistentWebSocketConfig()
}
}

Expand Down Expand Up @@ -103,6 +110,25 @@ class ManagedConfigurationsReceiver @Inject constructor(
}
}

private suspend fun updatePersistentWebSocketConfig() {
val wasEnforced = managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value
val isEnforced = managedConfigurationsManager.refreshPersistentWebSocketConfig()

// 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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ class WireActivity : AppCompatActivity() {
managedConfigurationsManager.refreshServerConfig()
managedConfigurationsManager.refreshSSOCodeConfig()
}
viewModel.applyPersistentWebSocketConfigFromMDM()
}
}

Expand Down
30 changes: 22 additions & 8 deletions app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import com.wire.android.util.deeplink.LoginType
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.UIText
import com.wire.android.emm.ManagedConfigurationsManager
import com.wire.android.workmanager.worker.cancelPeriodicPersistentWebsocketCheckWorker
import com.wire.android.workmanager.worker.enqueuePeriodicPersistentWebsocketCheckWorker
import com.wire.kalium.logic.CoreLogic
Expand Down Expand Up @@ -96,6 +97,7 @@
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
Expand Down Expand Up @@ -132,6 +134,7 @@
private val isProfileQRCodeEnabledFactory: IsProfileQRCodeEnabledUseCaseProvider.Factory,
private val observeSelfUserFactory: ObserveSelfUserUseCaseProvider.Factory,
private val monitorSyncWorkUseCase: MonitorSyncWorkUseCase,
private val managedConfigurationsManager: ManagedConfigurationsManager,
) : ActionsViewModel<WireActivityViewAction>() {

var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState())
Expand Down Expand Up @@ -512,6 +515,16 @@
globalAppState = globalAppState.copy(maxAccountDialog = false)
}

fun applyPersistentWebSocketConfigFromMDM() {
viewModelScope.launch(dispatchers.io()) {
val wasEnforced = managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value
val isEnforced = managedConfigurationsManager.refreshPersistentWebSocketConfig()

Check warning on line 521 in app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt#L519-L521

Added lines #L519 - L521 were not covered by tests
if (!wasEnforced && isEnforced) {
coreLogic.get().getGlobalScope().setAllPersistentWebSocketEnabled(true)

Check warning on line 523 in app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt#L523

Added line #L523 was not covered by tests
}
}
}

fun observePersistentConnectionStatus() {
viewModelScope.launch {
coreLogic.get().getGlobalScope().observePersistentWebSocketConnectionStatus()
Expand All @@ -522,15 +535,16 @@
}

is ObservePersistentWebSocketConnectionStatusUseCase.Result.Success -> {
result.persistentWebSocketStatusListFlow.collect { statuses ->

if (statuses.any { it.isPersistentWebSocketEnabled }) {
if (!servicesManager.get()
.isPersistentWebSocketServiceRunning()
) {
combine(
result.persistentWebSocketStatusListFlow,
managedConfigurationsManager.persistentWebSocketEnforcedByMDM
) { statuses, mdmEnforced ->
mdmEnforced || statuses.any { it.isPersistentWebSocketEnabled }
}.collect { shouldBeRunning ->
if (shouldBeRunning) {
if (!servicesManager.get().isPersistentWebSocketServiceRunning()) {
servicesManager.get().startPersistentWebSocketService()
workManager.get()
.enqueuePeriodicPersistentWebsocketCheckWorker()
workManager.get().enqueuePeriodicPersistentWebsocketCheckWorker()
}
} else {
servicesManager.get().stopPersistentWebSocketService()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ fun NetworkSettingsScreen(
NetworkSettingsScreenContent(
onBackPressed = navigator::navigateBack,
isWebSocketEnabled = networkSettingsViewModel.networkSettingsState.isPersistentWebSocketConnectionEnabled,
isEnforcedByMDM = networkSettingsViewModel.networkSettingsState.isEnforcedByMDM,
setWebSocketState = { networkSettingsViewModel.setWebSocketState(it) },
)
}
Expand All @@ -57,6 +58,7 @@ fun NetworkSettingsScreen(
fun NetworkSettingsScreenContent(
onBackPressed: () -> Unit,
isWebSocketEnabled: Boolean,
isEnforcedByMDM: Boolean,
setWebSocketState: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Expand All @@ -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
)
Expand All @@ -106,6 +114,7 @@ fun PreviewNetworkSettingsScreen() = WireTheme {
NetworkSettingsScreenContent(
onBackPressed = {},
isWebSocketEnabled = true,
isEnforcedByMDM = false,
setWebSocketState = {},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 23 in app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsState.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsState.kt#L22-L23

Added lines #L22 - L23 were not covered by tests
)
Loading