diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManager.kt new file mode 100644 index 000000000..50c48eacb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManager.kt @@ -0,0 +1,12 @@ +package com.x8bit.bitwarden.data.platform.manager + +/** + * Manager to detect and handle changes to network connectivity. + */ +interface NetworkConnectionManager { + /** + * Returns `true` if the application has a network connection and access to the Internet is + * available. + */ + val isNetworkConnected: Boolean +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManagerImpl.kt new file mode 100644 index 000000000..d2ced16a0 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManagerImpl.kt @@ -0,0 +1,22 @@ +package com.x8bit.bitwarden.data.platform.manager + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities + +/** + * Primary implementation of [NetworkConnectionManager]. + */ +class NetworkConnectionManagerImpl( + context: Context, +) : NetworkConnectionManager { + private val connectivityManager: ConnectivityManager = context + .applicationContext + .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + override val isNetworkConnected: Boolean + get() = connectivityManager + .getNetworkCapabilities(connectivityManager.activeNetwork) + ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + ?: false +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index 69cf18a39..f8aba96d2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.manager.di +import android.app.Application import android.content.Context import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.repository.AuthRepository @@ -12,6 +13,8 @@ import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppForegroundManagerImpl import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl +import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager +import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManagerImpl import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl import com.x8bit.bitwarden.data.platform.manager.SdkClientManager @@ -79,6 +82,14 @@ object PlatformManagerModule { dispatcherManager = dispatcherManager, ) + @Provides + @Singleton + fun provideNetworkConnectionManager( + application: Application, + ): NetworkConnectionManager = NetworkConnectionManagerImpl( + context = application.applicationContext, + ) + @Provides @Singleton fun providePushManager( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt index 0bdd21278..6c1b5c524 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt @@ -68,7 +68,7 @@ fun EnterpriseSignOnScreen( is EnterpriseSignOnState.DialogState.Error -> { BitwardenBasicDialog( visibilityState = BasicDialogState.Shown( - title = R.string.an_error_has_occurred.asText(), + title = dialog.title ?: R.string.an_error_has_occurred.asText(), message = dialog.message, ), onDismissRequest = remember(viewModel) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt index c1ba864a2..b899978b5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText @@ -18,6 +19,7 @@ private const val KEY_STATE = "state" */ @HiltViewModel class EnterpriseSignOnViewModel @Inject constructor( + private val networkConnectionManager: NetworkConnectionManager, savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] @@ -50,11 +52,23 @@ class EnterpriseSignOnViewModel @Inject constructor( // TODO BIT-816: submit request for single sign on sendEvent(EnterpriseSignOnEvent.ShowToast("Not yet implemented.")) + if (!networkConnectionManager.isNetworkConnected) { + mutableStateFlow.update { + it.copy( + dialogState = EnterpriseSignOnState.DialogState.Error( + title = R.string.internet_connection_required_title.asText(), + message = R.string.internet_connection_required_message.asText(), + ), + ) + } + return + } + if (state.orgIdentifierInput.isBlank()) { mutableStateFlow.update { it.copy( dialogState = EnterpriseSignOnState.DialogState.Error( - R.string.validation_field_required.asText( + message = R.string.validation_field_required.asText( R.string.org_identifier.asText(), ), ), @@ -83,10 +97,12 @@ data class EnterpriseSignOnState( */ sealed class DialogState : Parcelable { /** - * Represents an error dialog with the given [message]. + * Represents an error dialog with the given [message] and optional [title]. It no title + * is specified a default will be provided. */ @Parcelize data class Error( + val title: Text? = null, val message: Text, ) : DialogState() diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManagerTest.kt new file mode 100644 index 000000000..1ae003ad6 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/NetworkConnectionManagerTest.kt @@ -0,0 +1,64 @@ +package com.x8bit.bitwarden.data.platform.manager + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue + +class NetworkConnectionManagerTest { + @Test + fun `isNetworkConnected should return false if no active network`() { + val connectivityManager: ConnectivityManager = mockk { + every { activeNetwork } returns null + every { getNetworkCapabilities(any()) } returns null + } + val networkConnectionManager = createNetworkConnectionManager(connectivityManager) + assertFalse(networkConnectionManager.isNetworkConnected) + } + + @Test + fun `isNetworkConnected should return false if active network has no Internet capabilities`() { + val network: Network = mockk() + val networkCapabilities: NetworkCapabilities = mockk { + every { hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } returns false + } + val connectivityManager: ConnectivityManager = mockk { + every { activeNetwork } returns network + every { getNetworkCapabilities(network) } returns networkCapabilities + } + val networkConnectionManager = createNetworkConnectionManager(connectivityManager) + assertFalse(networkConnectionManager.isNetworkConnected) + } + + @Test + fun `isNetworkConnected should return true if active network has Internet capabilities`() { + val network: Network = mockk() + val networkCapabilities: NetworkCapabilities = mockk { + every { hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } returns true + } + val connectivityManager: ConnectivityManager = mockk { + every { activeNetwork } returns network + every { getNetworkCapabilities(network) } returns networkCapabilities + } + val networkConnectionManager = createNetworkConnectionManager(connectivityManager) + assertTrue(networkConnectionManager.isNetworkConnected) + } + + private fun createNetworkConnectionManager( + connectivityManager: ConnectivityManager, + ): NetworkConnectionManager { + val appContext: Context = mockk { + every { getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager + } + val context: Context = mockk { + every { applicationContext } returns appContext + } + + return NetworkConnectionManagerImpl(context) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/FakeNetworkConnectionManager.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/FakeNetworkConnectionManager.kt new file mode 100644 index 000000000..48173ca17 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/FakeNetworkConnectionManager.kt @@ -0,0 +1,7 @@ +package com.x8bit.bitwarden.data.platform.manager.util + +import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager + +class FakeNetworkConnectionManager( + override val isNetworkConnected: Boolean, +) : NetworkConnectionManager diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt index 429104b5b..6bd41e144 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.platform.manager.util.FakeNetworkConnectionManager import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText import kotlinx.coroutines.test.runTest @@ -67,7 +68,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() { assertEquals( DEFAULT_STATE.copy( dialogState = EnterpriseSignOnState.DialogState.Error( - R.string.validation_field_required.asText( + message = R.string.validation_field_required.asText( R.string.org_identifier.asText(), ), ), @@ -81,6 +82,28 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `LogInClick with no Internet should emit ShowToast and show error dialog`() = runTest { + val viewModel = createViewModel(isNetworkConnected = false) + viewModel.eventFlow.test { + viewModel.actionChannel.trySend(EnterpriseSignOnAction.LogInClick) + assertEquals( + DEFAULT_STATE.copy( + dialogState = EnterpriseSignOnState.DialogState.Error( + title = R.string.internet_connection_required_title.asText(), + message = R.string.internet_connection_required_message.asText(), + ), + ), + viewModel.stateFlow.value, + ) + assertEquals( + EnterpriseSignOnEvent.ShowToast("Not yet implemented."), + awaitItem(), + ) + } + } + @Test fun `OrgIdentifierInputChange should update organization identifier`() = runTest { val input = "input" @@ -141,7 +164,9 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() { savedStateHandle: SavedStateHandle = SavedStateHandle( initialState = mapOf("state" to initialState), ), + isNetworkConnected: Boolean = true, ): EnterpriseSignOnViewModel = EnterpriseSignOnViewModel( + networkConnectionManager = FakeNetworkConnectionManager(isNetworkConnected), savedStateHandle = savedStateHandle, )