From 0c20ebe9b309570105648b853f0aad0b649ae5b6 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Fri, 7 May 2021 17:03:23 +0200 Subject: [PATCH] Improve network availability checks (DEV) (#3100) * Improve network availability checks. * Remove static access ConnectivityHelper.kt * Add `isInternetAvailable` to NetworkStateProvider.kt * Improve NetworkStateProvider.kt unit tests * Add unit tests for Android 5 specific behavior * Revert default ENV change. Co-authored-by: Juraj Kusnier <jurajkusnier@users.noreply.github.com> --- .../storage/TracingRepository.kt | 8 +- .../InteroperabilityRepository.kt | 51 ++-- .../InteroperabilityConfigurationFragment.kt | 45 ---- ...erabilityConfigurationFragmentViewModel.kt | 9 +- .../rki/coronawarnapp/ui/main/MainActivity.kt | 7 - .../coronawarnapp/util/ConnectivityHelper.kt | 122 --------- .../util/device/PowerManagement.kt | 4 +- .../util/network/NetworkStateProvider.kt | 55 ++-- ...ilityConfigurationFragmentViewModelTest.kt | 13 - .../util/ConnectivityHelperTest.kt | 78 ------ .../util/device/PowerManagementTest.kt | 47 ++++ .../util/network/NetworkStateProviderTest.kt | 235 ++++++++++-------- 12 files changed, 246 insertions(+), 428 deletions(-) delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt delete mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ConnectivityHelperTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/device/PowerManagementTest.kt diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt index bd5aaa78b..7a80372f1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt @@ -1,7 +1,6 @@ package de.rki.coronawarnapp.storage import android.annotation.SuppressLint -import android.content.Context import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient @@ -17,11 +16,10 @@ import de.rki.coronawarnapp.task.TaskInfo import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.tracing.TracingProgress -import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.device.BackgroundModeStatus -import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.network.NetworkStateProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -41,7 +39,6 @@ import javax.inject.Singleton */ @Singleton class TracingRepository @Inject constructor( - @AppContext private val context: Context, @AppScope private val scope: CoroutineScope, private val taskController: TaskController, enfClient: ENFClient, @@ -50,6 +47,7 @@ class TracingRepository @Inject constructor( private val backgroundModeStatus: BackgroundModeStatus, private val exposureWindowRiskWorkScheduler: ExposureWindowRiskWorkScheduler, private val presenceTracingRiskWorkScheduler: PresenceTracingRiskWorkScheduler, + private val networkStateProvider: NetworkStateProvider, ) { @SuppressLint("BinaryOperationInTimber") @@ -108,7 +106,7 @@ class TracingRepository @Inject constructor( // TODO temp place, this needs to go somewhere better suspend fun refreshRiskLevel() { // check if the network is enabled to make the server fetch - val isNetworkEnabled = ConnectivityHelper.isNetworkEnabled(context) + val isNetworkEnabled = networkStateProvider.networkState.first().isInternetAvailable // only fetch the diagnosis keys if background jobs are enabled, so that in manual // model the keys are only fetched on button press of the user diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt index b17572eb8..3021938c3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt @@ -3,6 +3,9 @@ package de.rki.coronawarnapp.storage.interoperability import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.ui.Country +import de.rki.coronawarnapp.util.network.NetworkStateProvider +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -13,31 +16,41 @@ import javax.inject.Singleton @Singleton class InteroperabilityRepository @Inject constructor( private val appConfigProvider: AppConfigProvider, - private val settings: CWASettings + private val settings: CWASettings, + networkStateProvider: NetworkStateProvider, ) { - val countryList = appConfigProvider.currentConfig - .map { configData -> - try { - configData - .supportedCountries - .mapNotNull { rawCode -> - val countryCode = rawCode.toLowerCase(Locale.ROOT) - - val mappedCountry = Country.values().singleOrNull { it.code == countryCode } - if (mappedCountry == null) Timber.e("Unknown countrycode: %s", rawCode) - mappedCountry - } - } catch (e: Exception) { - Timber.e(e, "Failed to map country list.") - emptyList() + private val hasInternetFlow = networkStateProvider.networkState + .map { it.isInternetAvailable } + .distinctUntilChanged() + .onEach { hasInternet -> + // Refresh appConfig on false -> true changes + if (hasInternet) { + Timber.v("Trying app config refresh for interop country list.") + appConfigProvider.getAppConfig() } } - .onEach { Timber.d("Country list: %s", it.joinToString(",")) } - suspend fun refreshCountries() { - appConfigProvider.getAppConfig() + val countryList = combine( + appConfigProvider.currentConfig, + hasInternetFlow, + ) { configData, _ -> + try { + configData + .supportedCountries + .mapNotNull { rawCode -> + val countryCode = rawCode.toLowerCase(Locale.ROOT) + + val mappedCountry = Country.values().singleOrNull { it.code == countryCode } + if (mappedCountry == null) Timber.e("Unknown countrycode: %s", rawCode) + mappedCountry + } + } catch (e: Exception) { + Timber.e(e, "Failed to map country list.") + emptyList() + } } + .onEach { Timber.d("Country list: %s", it.joinToString(",")) } fun saveInteroperabilityUsed() { settings.wasInteroperabilityShownAtLeastOnce = true diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragment.kt index 28cd0f394..347111db1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragment.kt @@ -6,10 +6,8 @@ import android.os.Bundle import android.provider.Settings import android.view.View import androidx.fragment.app.Fragment -import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInteroperabilityConfigurationBinding -import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.popBackStack @@ -26,17 +24,6 @@ class InteroperabilityConfigurationFragment : private val binding: FragmentInteroperabilityConfigurationBinding by viewBindingLazy() - private var isNetworkCallbackRegistered = false - private val networkCallback = object : ConnectivityHelper.NetworkCallback() { - override fun onNetworkAvailable() { - vm.refreshCountries() - } - - override fun onNetworkUnavailable() { - // NOOP - } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -44,10 +31,6 @@ class InteroperabilityConfigurationFragment : binding.countryData = it } - if (ConnectivityHelper.isNetworkEnabled(CoronaWarnApplication.getAppContext())) { - registerNetworkCallback() - } - vm.saveInteroperabilityUsed() binding.interoperabilityConfigurationHeader.headerButtonBack.buttonIcon.setOnClickListener { @@ -69,32 +52,4 @@ class InteroperabilityConfigurationFragment : startActivity(intent) } } - - private fun registerNetworkCallback() { - context?.let { - ConnectivityHelper.registerNetworkStatusCallback(it, networkCallback) - isNetworkCallbackRegistered = true - } - } - - private fun unregisterNetworkCallback() { - if (isNetworkCallbackRegistered) { - context?.let { - ConnectivityHelper.unregisterNetworkStatusCallback(it, networkCallback) - isNetworkCallbackRegistered = false - } - } - } - - override fun onDestroy() { - super.onDestroy() - unregisterNetworkCallback() - } - - override fun onResume() { - super.onResume() - if (ConnectivityHelper.isNetworkEnabled(CoronaWarnApplication.getAppContext())) { - registerNetworkCallback() - } - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModel.kt index 4a26cc863..630fdeee5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModel.kt @@ -11,11 +11,12 @@ import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory class InteroperabilityConfigurationFragmentViewModel @AssistedInject constructor( private val interoperabilityRepository: InteroperabilityRepository, - dispatcherProvider: DispatcherProvider + dispatcherProvider: DispatcherProvider, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val countryList = interoperabilityRepository.countryList .asLiveData(context = dispatcherProvider.Default) + val navigateBack = SingleLiveEvent<Boolean>() fun onBackPressed() { @@ -26,12 +27,6 @@ class InteroperabilityConfigurationFragmentViewModel @AssistedInject constructor interoperabilityRepository.saveInteroperabilityUsed() } - fun refreshCountries() { - launch { - interoperabilityRepository.refreshCountries() - } - } - @AssistedFactory interface Factory : SimpleCWAViewModelFactory<InteroperabilityConfigurationFragmentViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index 159a0b9d5..a90442432 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt @@ -27,7 +27,6 @@ import de.rki.coronawarnapp.ui.setupWithNavController2 import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragment import de.rki.coronawarnapp.util.AppShortcuts import de.rki.coronawarnapp.util.CWADebug -import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.device.PowerManagement @@ -40,12 +39,6 @@ import org.joda.time.LocalDate import timber.log.Timber import javax.inject.Inject -/** - * This activity holds all the fragments (except onboarding) and also registers a listener for - * connectivity and bluetooth to update the ui. - * - * @see ConnectivityHelper - */ class MainActivity : AppCompatActivity(), HasAndroidInjector { companion object { fun start(context: Context, launchIntent: Intent) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt deleted file mode 100644 index f169b9c7b..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt +++ /dev/null @@ -1,122 +0,0 @@ -package de.rki.coronawarnapp.util - -import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkRequest -import android.os.Build -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.reporting.report - -/** - * Helper for connectivity statuses. - */ -object ConnectivityHelper { - private val TAG: String? = ConnectivityHelper::class.simpleName - - /** - * Unregister network state change callback. - * - * @param context the context - * @param callback the network state callback - * - * @see [ConnectivityManager] - */ - fun unregisterNetworkStatusCallback(context: Context, callback: NetworkCallback) { - try { - val manager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - manager.unregisterNetworkCallback(callback) - } catch (e: Exception) { - e.report( - ExceptionCategory.CONNECTIVITY, - TAG, - null - ) - } - } - - /** - * Register network state change callback. - * - * @param context the context - * @param callback the network state callback - * - * @see [ConnectivityManager] - * @see [NetworkCapabilities] - * @see [NetworkRequest] - */ - fun registerNetworkStatusCallback(context: Context, callback: NetworkCallback) { - try { - // If there are no Wi-Fi or mobile data presented when callback is registered - // none of NetworkCallback methods are called - callback.onNetworkUnavailable() - - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) - .build() - val manager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - manager.registerNetworkCallback(request, callback) - } catch (e: Exception) { - e.report( - ExceptionCategory.CONNECTIVITY, - TAG, - null - ) - } - } - - /** - * Get network enabled status. - * - * @return current network status - * - */ - fun isNetworkEnabled(context: Context): Boolean { - val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { - val activeNetwork = manager.activeNetwork - val caps: NetworkCapabilities? = manager.getNetworkCapabilities(activeNetwork) - caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false - } - else -> { - val activeNetworkInfo = manager.activeNetworkInfo - activeNetworkInfo != null && activeNetworkInfo.isConnected - } - } - } - - /** - * Abstract network state change callback. - * - * @see [ConnectivityManager.NetworkCallback] - */ - abstract class NetworkCallback : ConnectivityManager.NetworkCallback() { - - /** - * Called when network is available. - */ - abstract fun onNetworkAvailable() - - /** - * Called when network is unavailable or lost. - */ - abstract fun onNetworkUnavailable() - - override fun onAvailable(network: Network) { - onNetworkAvailable() - } - - override fun onUnavailable() { - onNetworkUnavailable() - } - - override fun onLost(network: Network) { - onNetworkUnavailable() - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/PowerManagement.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/PowerManagement.kt index 294f10ec0..99be9a9b2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/PowerManagement.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/PowerManagement.kt @@ -7,7 +7,9 @@ import android.os.PowerManager import android.provider.Settings import androidx.annotation.RequiresApi import androidx.core.content.getSystemService +import de.rki.coronawarnapp.util.BuildVersionWrap import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.hasAPILevel import javax.inject.Inject import javax.inject.Singleton @@ -19,7 +21,7 @@ class PowerManagement @Inject constructor( private val powerManager by lazy { context.getSystemService<PowerManager>()!! } val isIgnoringBatteryOptimizations - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + get() = if (BuildVersionWrap.hasAPILevel(Build.VERSION_CODES.M)) { powerManager.isIgnoringBatteryOptimizations(context.packageName) } else { true diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/network/NetworkStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/network/NetworkStateProvider.kt index e74c06d65..9222b0d5c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/network/NetworkStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/network/NetworkStateProvider.kt @@ -8,6 +8,7 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED import android.os.Build import androidx.annotation.RequiresApi import androidx.core.net.ConnectivityManagerCompat @@ -70,11 +71,7 @@ class NetworkStateProvider @Inject constructor( registeredCallback = callback } catch (e: SecurityException) { Timber.e(e, "registerNetworkCallback() threw an undocumented SecurityException, Just Samsung Thingsâ„¢ï¸") - State( - activeNetwork = null, - capabilities = null, - linkProperties = null, - ).run { send(this) } + send(FallbackState) } val fakeConnectionSubscriber = launch { @@ -99,22 +96,13 @@ class NetworkStateProvider @Inject constructor( private val currentState: State @SuppressLint("NewApi") get() = when { - BuildVersionWrap.hasAPILevel(Build.VERSION_CODES.M) -> api23NetworkState() - else -> { - // Most state information is not available - State( - activeNetwork = null, - capabilities = null, - linkProperties = null, - assumeMeteredConnection = testSettings.fakeMeteredConnection.value || - ConnectivityManagerCompat.isActiveNetworkMetered(manager) - ) - } + BuildVersionWrap.hasAPILevel(Build.VERSION_CODES.M) -> modernNetworkState() + else -> legacyNetworkState() } @RequiresApi(Build.VERSION_CODES.M) - private fun api23NetworkState() = manager.activeNetwork.let { network -> - State( + private fun modernNetworkState(): State = manager.activeNetwork.let { network -> + ModernState( activeNetwork = network, capabilities = network?.let { try { @@ -136,13 +124,33 @@ class NetworkStateProvider @Inject constructor( ) } - data class State( + @Suppress("DEPRECATION") + private fun legacyNetworkState(): State = StateLegacyAPI21( + isInternetAvailable = manager.activeNetworkInfo?.isConnected ?: false, + isMeteredConnection = testSettings.fakeMeteredConnection.value || + ConnectivityManagerCompat.isActiveNetworkMetered(manager) + ) + + interface State { + val isMeteredConnection: Boolean + val isInternetAvailable: Boolean + } + + data class StateLegacyAPI21( + override val isMeteredConnection: Boolean, + override val isInternetAvailable: Boolean + ) : State + + data class ModernState( val activeNetwork: Network?, val capabilities: NetworkCapabilities?, val linkProperties: LinkProperties?, private val assumeMeteredConnection: Boolean = false - ) { - val isMeteredConnection: Boolean + ) : State { + override val isInternetAvailable: Boolean + get() = capabilities?.hasCapability(NET_CAPABILITY_VALIDATED) ?: false + + override val isMeteredConnection: Boolean get() { val unMetered = if (BuildVersionWrap.hasAPILevel(Build.VERSION_CODES.N)) { capabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) ?: false @@ -153,6 +161,11 @@ class NetworkStateProvider @Inject constructor( } } + object FallbackState : State { + override val isMeteredConnection: Boolean = true + override val isInternetAvailable: Boolean = true + } + companion object { private const val TAG = "NetworkStateProvider" } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModelTest.kt index 52a0c791b..01f57aa20 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModelTest.kt @@ -4,12 +4,8 @@ import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.ui.Country import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.just import io.mockk.verify import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.BeforeEach @@ -28,7 +24,6 @@ class InteroperabilityConfigurationFragmentViewModelTest { fun setupFreshViewModel() { MockKAnnotations.init(this) - coEvery { interoperabilityRepository.refreshCountries() } just Runs every { interoperabilityRepository.countryList } returns flowOf(Country.values().toList()) } @@ -44,14 +39,6 @@ class InteroperabilityConfigurationFragmentViewModelTest { verify { interoperabilityRepository.countryList } } - @Test - fun `forced countrylist refresh via app config`() { - val vm = createViewModel() - coVerify(exactly = 0) { interoperabilityRepository.refreshCountries() } - vm.refreshCountries() - coVerify(exactly = 1) { interoperabilityRepository.refreshCountries() } - } - @Test fun testBackPressButton() { val vm = createViewModel() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ConnectivityHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ConnectivityHelperTest.kt deleted file mode 100644 index 9ae6f964d..000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ConnectivityHelperTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package de.rki.coronawarnapp.util - -import android.bluetooth.BluetoothAdapter -import android.content.Context -import android.net.ConnectivityManager -import android.net.NetworkRequest -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.mockkConstructor -import io.mockk.mockkObject -import io.mockk.mockkStatic -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import testhelpers.BaseTest - -/** - * ConnectivityHelper test. - */ -class ConnectivityHelperTest : BaseTest() { - - @MockK private lateinit var context: Context - - @Before - fun setUp() { - MockKAnnotations.init(this) - mockkStatic(BluetoothAdapter::class) - } - - /** - * Test network callback behavior. - */ - @Test - fun testNetworkCallback() { - var registered: Boolean? = null - var available: Boolean? = null - val callback = object : ConnectivityHelper.NetworkCallback() { - override fun onNetworkAvailable() { - available = true - } - - override fun onNetworkUnavailable() { - available = false - } - } - mockkConstructor(NetworkRequest.Builder::class) - mockkObject(NetworkRequest.Builder()) - val request = mockk<NetworkRequest>() - val manager = mockk<ConnectivityManager>() - every { - anyConstructed<NetworkRequest.Builder>().addCapability(any()).addCapability(any()) - .build() - } returns request - every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns manager - every { manager.registerNetworkCallback(any(), callback) } answers { registered = true } - every { manager.unregisterNetworkCallback(callback) } answers { registered = false } - - // register - ConnectivityHelper.registerNetworkStatusCallback(context, callback) - - assertEquals(registered, true) - assertEquals(available, false) - - // network found - callback.onAvailable(mockk()) - assertEquals(available, true) - - // loss of network - callback.onLost(mockk()) - assertEquals(available, false) - - // unregister - ConnectivityHelper.unregisterNetworkStatusCallback(context, callback) - assertEquals(registered, false) - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/device/PowerManagementTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/device/PowerManagementTest.kt new file mode 100644 index 000000000..21e8c778a --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/device/PowerManagementTest.kt @@ -0,0 +1,47 @@ +package de.rki.coronawarnapp.util.device + +import android.content.Context +import de.rki.coronawarnapp.util.BuildVersionWrap +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.MockKException +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockkObject +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.extensions.InstantExecutorExtension + +@ExtendWith(InstantExecutorExtension::class) +class PowerManagementTest : BaseTest() { + + @MockK lateinit var context: Context + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockkObject(BuildVersionWrap) + every { BuildVersionWrap.SDK_INT } returns 23 + } + + fun createInstance() = PowerManagement( + context = context + ) + + @Test + fun `isIgnoringBatteryOptimizations always returns true below API23`() { + val instance = createInstance() + + shouldThrow<MockKException> { + instance.isIgnoringBatteryOptimizations + } + + every { BuildVersionWrap.SDK_INT } returns 22 + + instance.isIgnoringBatteryOptimizations shouldBe true + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/network/NetworkStateProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/network/NetworkStateProviderTest.kt index 104e1d6e9..5b7d37f41 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/network/NetworkStateProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/network/NetworkStateProviderTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package de.rki.coronawarnapp.util.network import android.content.Context @@ -5,6 +7,7 @@ import android.net.ConnectivityManager import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities +import android.net.NetworkInfo import android.net.NetworkRequest import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.util.BuildVersionWrap @@ -33,10 +36,11 @@ import testhelpers.preferences.mockFlowPreference class NetworkStateProviderTest : BaseTest() { @MockK lateinit var context: Context - @MockK lateinit var conMan: ConnectivityManager + @MockK lateinit var connectivityManager: ConnectivityManager @MockK lateinit var testSettings: TestSettings @MockK lateinit var network: Network + @MockK lateinit var networkInfo: NetworkInfo @MockK lateinit var networkRequest: NetworkRequest @MockK lateinit var networkRequestBuilder: NetworkRequest.Builder @MockK lateinit var networkRequestBuilderProvider: NetworkRequestBuilderProvider @@ -53,29 +57,44 @@ class NetworkStateProviderTest : BaseTest() { mockkObject(BuildVersionWrap) every { BuildVersionWrap.SDK_INT } returns 24 - every { - conMan.registerNetworkCallback( - any<NetworkRequest>(), - any<ConnectivityManager.NetworkCallback>() - ) - } answers { - lastRequest = arg(0) - lastCallback = arg(1) - mockk() + every { testSettings.fakeMeteredConnection } returns mockFlowPreference(false) + every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager + + every { networkRequestBuilderProvider.get() } returns networkRequestBuilder + networkRequestBuilder.apply { + every { addCapability(any()) } returns networkRequestBuilder + every { build() } returns networkRequest } - every { conMan.unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>()) } just Runs - every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns conMan + connectivityManager.apply { + every { activeNetwork } returns network + every { activeNetworkInfo } answers { networkInfo } + every { unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>()) } just Runs - every { networkRequestBuilderProvider.get() } returns networkRequestBuilder - every { networkRequestBuilder.addCapability(any()) } returns networkRequestBuilder - every { networkRequestBuilder.build() } returns networkRequest + every { getNetworkCapabilities(network) } answers { capabilities } + every { getLinkProperties(network) } answers { linkProperties } - every { conMan.activeNetwork } returns network - every { conMan.getNetworkCapabilities(network) } returns capabilities - every { conMan.getLinkProperties(network) } returns linkProperties + every { + registerNetworkCallback(any<NetworkRequest>(), any<ConnectivityManager.NetworkCallback>()) + } answers { + lastRequest = arg(0) + lastCallback = arg(1) + mockk() + } + } - every { testSettings.fakeMeteredConnection } returns mockFlowPreference(false) + networkInfo.apply { + every { type } returns ConnectivityManager.TYPE_WIFI + every { isConnected } returns true + } + + capabilities.apply { + // The happy path is an unmetered internet connection being available + every { hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } returns true + every { hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } returns true + every { hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) } returns true + every { hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns true + } } private fun createInstance(scope: CoroutineScope) = NetworkStateProvider( @@ -90,41 +109,39 @@ class NetworkStateProviderTest : BaseTest() { shouldNotThrowAny { createInstance(TestCoroutineScope()) } - verify { conMan wasNot Called } + verify { connectivityManager wasNot Called } } @Test fun `initial state is emitted correctly without callback`() = runBlockingTest2(ignoreActive = true) { val instance = createInstance(this) - instance.networkState.first() shouldBe NetworkStateProvider.State( - activeNetwork = network, - capabilities = capabilities, - linkProperties = linkProperties - ) + instance.networkState.first().apply { + isMeteredConnection shouldBe false + isInternetAvailable shouldBe true + } advanceUntilIdle() verifySequence { - conMan.activeNetwork - conMan.getNetworkCapabilities(network) - conMan.getLinkProperties(network) - conMan.registerNetworkCallback(networkRequest, any<ConnectivityManager.NetworkCallback>()) - conMan.unregisterNetworkCallback(lastCallback!!) + connectivityManager.activeNetwork + connectivityManager.getNetworkCapabilities(network) + connectivityManager.getLinkProperties(network) + connectivityManager.registerNetworkCallback(networkRequest, any<ConnectivityManager.NetworkCallback>()) + connectivityManager.unregisterNetworkCallback(lastCallback!!) } } @Test fun `we can handle null networks`() = runBlockingTest2(ignoreActive = true) { - every { conMan.activeNetwork } returns null + every { connectivityManager.activeNetwork } returns null val instance = createInstance(this) - instance.networkState.first() shouldBe NetworkStateProvider.State( - activeNetwork = null, - capabilities = null, - linkProperties = null - ) - verify { conMan.activeNetwork } + instance.networkState.first().apply { + isInternetAvailable shouldBe false + isMeteredConnection shouldBe true + } + verify { connectivityManager.activeNetwork } } @Test @@ -135,10 +152,10 @@ class NetworkStateProviderTest : BaseTest() { lastCallback!!.onAvailable(mockk()) - every { conMan.activeNetwork } returns null + every { connectivityManager.activeNetwork } returns null lastCallback!!.onUnavailable() - every { conMan.activeNetwork } returns network + every { connectivityManager.activeNetwork } returns network lastCallback!!.onAvailable(mockk()) advanceUntilIdle() @@ -150,99 +167,72 @@ class NetworkStateProviderTest : BaseTest() { verifySequence { // Start value - conMan.activeNetwork - conMan.getNetworkCapabilities(network) - conMan.getLinkProperties(network) - conMan.registerNetworkCallback(networkRequest, any<ConnectivityManager.NetworkCallback>()) + connectivityManager.activeNetwork + connectivityManager.getNetworkCapabilities(network) + connectivityManager.getLinkProperties(network) + connectivityManager.registerNetworkCallback(networkRequest, any<ConnectivityManager.NetworkCallback>()) // onAvailable - conMan.activeNetwork - conMan.getNetworkCapabilities(network) - conMan.getLinkProperties(network) + connectivityManager.activeNetwork + connectivityManager.getNetworkCapabilities(network) + connectivityManager.getLinkProperties(network) // onUnavailable - conMan.activeNetwork + connectivityManager.activeNetwork // onAvailable - conMan.activeNetwork - conMan.getNetworkCapabilities(network) - conMan.getLinkProperties(network) + connectivityManager.activeNetwork + connectivityManager.getNetworkCapabilities(network) + connectivityManager.getLinkProperties(network) - conMan.unregisterNetworkCallback(lastCallback!!) + connectivityManager.unregisterNetworkCallback(lastCallback!!) } } @Test - fun `metered connection state checks capabilities`() { - val capabilities = mockk<NetworkCapabilities>() - - every { capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) } returns true - NetworkStateProvider.State( - activeNetwork = null, - capabilities = capabilities, - linkProperties = null - ).isMeteredConnection shouldBe false - - NetworkStateProvider.State( - activeNetwork = null, - capabilities = null, - linkProperties = null - ).isMeteredConnection shouldBe true - - every { capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) } returns false - NetworkStateProvider.State( - activeNetwork = null, - capabilities = capabilities, - linkProperties = null - ).isMeteredConnection shouldBe true + fun `metered connection state checks capabilities`() = runBlockingTest2(ignoreActive = true) { + createInstance(this).apply { + networkState.first().isMeteredConnection shouldBe false + + every { capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) } returns false + networkState.first().isMeteredConnection shouldBe true + + every { connectivityManager.getNetworkCapabilities(any()) } returns null + networkState.first().isMeteredConnection shouldBe true + } } @Test fun `metered connection state can be overridden via test settings`() = runBlockingTest2(ignoreActive = true) { - every { testSettings.fakeMeteredConnection } returns mockFlowPreference(true) val instance = createInstance(this) - instance.networkState.first() + instance.networkState.first().isMeteredConnection shouldBe false + + every { testSettings.fakeMeteredConnection } returns mockFlowPreference(true) - NetworkStateProvider.State( - activeNetwork = null, - capabilities = null, - linkProperties = null - ).isMeteredConnection shouldBe true + instance.networkState.first().isMeteredConnection shouldBe true } @Test fun `Android 6 not metered on wifi`() = runBlockingTest2(ignoreActive = true) { every { BuildVersionWrap.SDK_INT } returns 23 - - every { capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns true - - NetworkStateProvider.State( - activeNetwork = null, - capabilities = capabilities, - linkProperties = null - ).isMeteredConnection shouldBe false + val instance = createInstance(this) every { capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns false + instance.networkState.first().isMeteredConnection shouldBe true - NetworkStateProvider.State( - activeNetwork = null, - capabilities = capabilities, - linkProperties = null - ).isMeteredConnection shouldBe true - - NetworkStateProvider.State( - activeNetwork = null, - capabilities = null, - linkProperties = null - ).isMeteredConnection shouldBe true + every { capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns true + instance.networkState.first().isMeteredConnection shouldBe false + + every { connectivityManager.getNetworkCapabilities(any()) } returns null + instance.networkState.first().isMeteredConnection shouldBe true } @Test fun `if we fail to register the callback, we do not attempt to unregister it`() = runBlockingTest2(ignoreActive = true) { every { - conMan.registerNetworkCallback( + connectivityManager.registerNetworkCallback( any(), any<ConnectivityManager.NetworkCallback>() ) @@ -250,20 +240,45 @@ class NetworkStateProviderTest : BaseTest() { val instance = createInstance(this) - instance.networkState.first() shouldBe NetworkStateProvider.State( - activeNetwork = null, - capabilities = null, - linkProperties = null - ) + instance.networkState.first().apply { + isInternetAvailable shouldBe true + isMeteredConnection shouldBe true + } advanceUntilIdle() verifySequence { - conMan.activeNetwork - conMan.getNetworkCapabilities(network) - conMan.getLinkProperties(network) - conMan.registerNetworkCallback(networkRequest, any<ConnectivityManager.NetworkCallback>()) + connectivityManager.activeNetwork + connectivityManager.getNetworkCapabilities(network) + connectivityManager.getLinkProperties(network) + connectivityManager.registerNetworkCallback(networkRequest, any<ConnectivityManager.NetworkCallback>()) } - verify(exactly = 0) { conMan.unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>()) } + verify(exactly = 0) { connectivityManager.unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>()) } } + + @Test + fun `current state is correctly determined below API 23`() = runBlockingTest2(ignoreActive = true) { + every { BuildVersionWrap.SDK_INT } returns 22 + + createInstance(this).apply { + networkState.first().apply { + isInternetAvailable shouldBe true + isMeteredConnection shouldBe false + } + + every { networkInfo.type } returns ConnectivityManager.TYPE_MOBILE + networkState.first().apply { + isInternetAvailable shouldBe true + isMeteredConnection shouldBe true + } + + every { networkInfo.isConnected } returns false + networkState.first().apply { + isInternetAvailable shouldBe false + isMeteredConnection shouldBe true + } + } + + verify { connectivityManager.activeNetworkInfo } + } } -- GitLab