Skip to content

Commit

Permalink
feat: react to network availability (#20)
Browse files Browse the repository at this point in the history
* feat: react to network availability
* Add tests for network availability
* More network tests and encapsulating Android details
* Remove unusued class and fixed compilation issues
  • Loading branch information
gastonfournier authored Jul 16, 2024
1 parent 320fb08 commit 9ddcb19
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 48 deletions.
1 change: 1 addition & 0 deletions unleashandroidsdk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.getunleash.android.data.DataStrategy
import io.getunleash.android.data.UnleashContext
import io.getunleash.android.data.Variant
import io.getunleash.android.events.UnleashEventListener
import io.getunleash.android.http.NetworkStatusHelper
import io.getunleash.android.metrics.MetricsCollector
import io.getunleash.android.metrics.MetricsSender
import io.getunleash.android.metrics.NoOpMetrics
Expand Down Expand Up @@ -65,6 +66,7 @@ class DefaultUnleash(
private val cache: ObservableToggleCache
private var ready = AtomicBoolean(false)
private val fetcher: UnleashFetcher?
private val networkStatusHelper = NetworkStatusHelper(androidContext)

init {
val metricsSender =
Expand Down Expand Up @@ -101,8 +103,10 @@ class DefaultUnleash(
)
)
}
}.toImmutableList()
}.toImmutableList(),
networkAvailable = networkStatusHelper.isAvailable()
)
networkStatusHelper.registerNetworkListener(taskManager)
cache = ObservableCache(cacheImpl)
if (unleashConfig.localStorageConfig.enabled) {
val localBackup = loadFromBackup(cacheImpl, eventListener)
Expand Down Expand Up @@ -231,6 +235,7 @@ class DefaultUnleash(
}

override fun close() {
networkStatusHelper.close()
job.cancel("Unleash received closed signal")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.getunleash.android.http

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 android.util.Log

interface NetworkListener {
fun onAvailable()
fun onLost()
}

class NetworkStatusHelper(val context: Context) {
companion object {
private const val TAG = "NetworkState"
}

private val networkCallbacks = mutableListOf<ConnectivityManager.NetworkCallback>()

fun registerNetworkListener(listener: NetworkListener) {
val connectivityManager = getConnectivityManager() ?: return
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()

// wrap the listener in a NetworkCallback so the listener doesn't have to know about Android specifics
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
listener.onAvailable()
}

override fun onLost(network: Network) {
listener.onLost()
}
}

connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
networkCallbacks += networkCallback
}

fun close () {
val connectivityManager = getConnectivityManager() ?: return
networkCallbacks.forEach {
connectivityManager.unregisterNetworkCallback(it)
}
}

private fun isNetworkAvailable(): Boolean {
val connectivityManager = getConnectivityManager() ?: return true

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNetwork = connectivityManager.activeNetwork ?: return false
val capabilities =
connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else {
@Suppress("DEPRECATION")
val networkInfo = connectivityManager.activeNetworkInfo ?: return false
@Suppress("DEPRECATION")
return networkInfo.isConnected
}
}

private fun getConnectivityManager(): ConnectivityManager? {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
if (connectivityManager !is ConnectivityManager) {
Log.w(TAG, "Failed to get ConnectivityManager assuming network is available")
return null
}
return connectivityManager
}

private fun isAirplaneModeOn(): Boolean {
return android.provider.Settings.System.getInt(
context.contentResolver,
android.provider.Settings.Global.AIRPLANE_MODE_ON, 0
) != 0
}

fun isAvailable(): Boolean {
return !isAirplaneModeOn() && isNetworkAvailable()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import io.getunleash.android.data.DataStrategy
import io.getunleash.android.http.NetworkListener
import io.getunleash.android.unleashScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -18,9 +19,10 @@ data class DataJob(val id: String, val strategy: DataStrategy, val action: suspe

class LifecycleAwareTaskManager(
private val dataJobs: List<DataJob>,
private var networkAvailable: Boolean = true,
private val scope: CoroutineScope = unleashScope,
private val ioContext: CoroutineContext = Dispatchers.IO
) : LifecycleEventObserver {
) : LifecycleEventObserver, NetworkListener {
companion object {
private const val TAG = "TaskManager"
}
Expand All @@ -29,6 +31,10 @@ class LifecycleAwareTaskManager(
private var isDestroying = false

internal fun startForegroundJobs() {
if (!networkAvailable) {
Log.d(TAG, "Network not available, not starting foreground jobs")
return
}
if (!isForeground) {
isForeground = true

Expand All @@ -46,11 +52,11 @@ class LifecycleAwareTaskManager(
}

private fun stopForegroundJobs() {
if (isForeground || isDestroying) {
if (isForeground || isDestroying || !networkAvailable) {
isForeground = false

dataJobs.forEach { dataJob ->
if (dataJob.strategy.pauseOnBackground || isDestroying) {
if (dataJob.strategy.pauseOnBackground || isDestroying || !networkAvailable) {
Log.d(TAG, "Pausing foreground job: ${dataJob.id}")
foregroundWorkers[dataJob.id]?.cancel()
} else {
Expand All @@ -67,7 +73,8 @@ class LifecycleAwareTaskManager(
): Job {
return scope.launch {
withContext(ioContext) {
while (!isDestroying && (isForeground || !strategy.pauseOnBackground)) {
while (!isDestroying && (isForeground || !strategy.pauseOnBackground)
&& networkAvailable) {
if (strategy.delay > 0) {
delay(strategy.delay)
}
Expand Down Expand Up @@ -96,4 +103,16 @@ class LifecycleAwareTaskManager(
else -> {}
}
}

override fun onAvailable() {
Log.d(TAG, "Network available")
networkAvailable = true
startForegroundJobs()
}

override fun onLost() {
Log.d(TAG, "Network connection lost")
networkAvailable = false
stopForegroundJobs()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.getunleash.android.http

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkInfo

import io.getunleash.android.BaseTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.robolectric.annotation.Config

@Suppress("DEPRECATION")
class NetworkStatusHelperTest : BaseTest() {

@Test
fun `when connectivity service is not available assumes network is available`() {
val networkStatusHelper = NetworkStatusHelper(mock(Context::class.java))
assertThat(networkStatusHelper.isAvailable()).isTrue()
}

@Test
fun `when api version is 21 check active network info`() {
val context = mock(Context::class.java)
val connectivityManager = mock(ConnectivityManager::class.java)
val activeNetwork = mock(NetworkInfo::class.java)
`when`(context.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(connectivityManager)
`when`(connectivityManager.activeNetworkInfo).thenReturn(activeNetwork)
`when`(activeNetwork.isConnected).thenReturn(true)
val networkStatusHelper = NetworkStatusHelper(context)
assertThat(networkStatusHelper.isAvailable()).isTrue()
verify(activeNetwork).isConnected
}

@Test
@Config(sdk = [23])
fun `when api version is 23 check active network info`() {
val context = mock(Context::class.java)
val connectivityManager = mock(ConnectivityManager::class.java)
val activeNetwork = mock(Network::class.java)
`when`(context.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(connectivityManager)
`when`(connectivityManager.activeNetwork).thenReturn(activeNetwork)
val networkCapabilities = mock(NetworkCapabilities::class.java)
`when`(connectivityManager.getNetworkCapabilities(activeNetwork)).thenReturn(networkCapabilities)
`when`(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
`when`(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).thenReturn(true)
val networkStatusHelper = NetworkStatusHelper(context)
assertThat(networkStatusHelper.isAvailable()).isTrue()
verify(networkCapabilities, times(2)).hasCapability(anyInt())
}
}
Loading

0 comments on commit 9ddcb19

Please sign in to comment.