├── sample
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ └── colors.xml
│ │ ├── mipmap
│ │ │ └── ic_launcher.png
│ │ └── layout
│ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── ru
│ │ └── beryukhov
│ │ └── sample
│ │ └── MainActivity.kt
├── proguard-rules.pro
└── build.gradle.kts
├── reactiveNetwork
├── .gitignore
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── robolectric.properties
│ │ └── kotlin
│ │ │ └── ru
│ │ │ └── beryukhov
│ │ │ └── reactivenetwork
│ │ │ ├── internet
│ │ │ └── observing
│ │ │ │ ├── error
│ │ │ │ └── DefaultErrorHandlerTest.kt
│ │ │ │ ├── InternetObservingSettingsTest.kt
│ │ │ │ └── strategy
│ │ │ │ ├── SocketInternetObservingStrategyTest.kt
│ │ │ │ └── WalledGardenInternetObservingStrategyTest.kt
│ │ │ ├── network
│ │ │ └── observing
│ │ │ │ ├── strategy
│ │ │ │ ├── LollipopNetworkObservingStrategyTest.kt
│ │ │ │ ├── PreLollipopNetworkObservingStrategyTest.kt
│ │ │ │ └── MarshmallowNetworkObservingStrategyTest.kt
│ │ │ │ └── NetworkObservingStrategyTest.kt
│ │ │ ├── PreconditionsTest.kt
│ │ │ ├── ReactiveNetworkTest.kt
│ │ │ └── ConnectivityTest.kt
│ └── main
│ │ ├── kotlin
│ │ └── ru
│ │ │ └── beryukhov
│ │ │ └── reactivenetwork
│ │ │ ├── internet
│ │ │ └── observing
│ │ │ │ ├── error
│ │ │ │ ├── ErrorHandler.kt
│ │ │ │ └── DefaultErrorHandler.kt
│ │ │ │ ├── InternetObservingStrategy.kt
│ │ │ │ ├── strategy
│ │ │ │ ├── SocketInternetObservingStrategy.kt
│ │ │ │ └── WalledGardenInternetObservingStrategy.kt
│ │ │ │ └── InternetObservingSettings.kt
│ │ │ ├── Predicate.kt
│ │ │ ├── network
│ │ │ └── observing
│ │ │ │ ├── NetworkObservingStrategy.kt
│ │ │ │ └── strategy
│ │ │ │ ├── PreLollipopNetworkObservingStrategy.kt
│ │ │ │ ├── LollipopNetworkObservingStrategy.kt
│ │ │ │ └── MarshmallowNetworkObservingStrategy.kt
│ │ │ ├── TickerFlow.kt
│ │ │ ├── Preconditions.kt
│ │ │ ├── ConnectivityPredicate.kt
│ │ │ ├── Connectivity.kt
│ │ │ └── ReactiveNetwork.kt
│ │ ├── res
│ │ └── xml
│ │ │ └── network_security_config.xml
│ │ └── AndroidManifest.xml
└── build.gradle.kts
├── Makefile
├── .gitignore
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── settings.gradle.kts
├── gradle.properties
├── .editorconfig
├── .github
└── workflows
│ └── android.yml
├── utils.gradle
├── scripts
├── publish-root.gradle
└── publish-module.gradle
├── README.md
├── gradlew.bat
├── gradlew
├── LICENSE
└── detektConfig.yml
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/reactiveNetwork/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | path := ./
2 |
3 | detekt:
4 | $(path)gradlew detektAll
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /build-logic/build
8 |
9 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Sample
3 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/resources/robolectric.properties:
--------------------------------------------------------------------------------
1 | # suppress inspection "UnusedProperty" for whole file
2 | sdk=23
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phansier/FlowReactiveNetwork/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phansier/FlowReactiveNetwork/HEAD/sample/src/main/res/mipmap/ic_launcher.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
2 |
3 | rootProject.name = "FlowReactiveNetwork"
4 | include(":sample")
5 | include(":reactiveNetwork")
6 |
7 | includeBuild("build-logic")
8 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC
2 | android.useAndroidX=true
3 | kotlin.code.style=official
4 | org.gradle.configuration-cache=true
5 |
6 | bintrayuser=replaceme
7 | bintraykey=replaceme
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
6 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/internet/observing/error/ErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing.error
2 |
3 | public interface ErrorHandler {
4 | public fun handleError(exception: Exception?, message: String?)
5 | }
6 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | clients3.google.com
5 |
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | # See detektConfig.yml, values should be the same
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 4
9 | indent_style = space
10 | insert_final_newline = true
11 | max_line_length = 120
12 |
13 | ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/internet/observing/error/DefaultErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing.error
2 |
3 | import android.util.Log
4 | import ru.beryukhov.reactivenetwork.ReactiveNetwork
5 |
6 | public class DefaultErrorHandler :
7 | ErrorHandler {
8 | override fun handleError(
9 | exception: Exception?,
10 | message: String?
11 | ) {
12 | Log.e(ReactiveNetwork.LOG_TAG, message, exception)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/Predicate.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | /**
4 | * A functional interface (callback) that returns true or false for the given input value.
5 | * @param the first value
6 | */
7 | public interface Predicate {
8 | /**
9 | * Test the given input value and return a boolean.
10 | * @param t the value
11 | * @return the boolean result
12 | * @throws Exception on error
13 | */
14 | @Throws(Exception::class)
15 | public fun test(t: T): Boolean
16 | }
17 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: set up JDK 17
17 | uses: actions/setup-java@v3
18 | with:
19 | distribution: 'adopt'
20 | java-version: '17'
21 | cache: 'gradle'
22 | - name: Detekt
23 | run: make detekt
24 | - name: Run tests
25 | run: ./gradlew test
26 | - name: Build a library with Gradle
27 | run: ./gradlew assembleRelease
28 |
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/utils.gradle:
--------------------------------------------------------------------------------
1 | def isAndroidProject() {
2 | def plugins = project.getPlugins()
3 | return plugins.hasPlugin('com.android.application') || plugins.hasPlugin('com.android.library')
4 | }
5 |
6 | def getStringProperty(String propertyName) {
7 | return project.hasProperty(propertyName) ? project.getProperty(propertyName) : ""
8 | }
9 |
10 | def getBooleanProperty(String propertyName) {
11 | return project.hasProperty(propertyName) ? project.getProperty(propertyName) : false
12 | }
13 |
14 | def getArrayProperty(String propertyName) {
15 | return project.hasProperty(propertyName) ? project.getProperty(propertyName) : []
16 | }
17 |
18 | ext {
19 | isAndroidProject = this.&isAndroidProject
20 | getStringProperty = this.&getStringProperty
21 | getBooleanProperty = this.&getBooleanProperty
22 | getArrayProperty = this.&getArrayProperty
23 | }
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/internet/observing/error/DefaultErrorHandlerTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing.error
2 |
3 | import io.mockk.spyk
4 | import io.mockk.verify
5 | import org.junit.Test
6 | import org.junit.runner.RunWith
7 | import org.robolectric.RobolectricTestRunner
8 |
9 | @RunWith(RobolectricTestRunner::class)
10 | open class DefaultErrorHandlerTest {
11 |
12 | private val handler = spyk(DefaultErrorHandler())
13 |
14 | @Test
15 | fun shouldHandleErrorDuringClosingSocket() {
16 | // given
17 | val errorMsg = "Could not close the socket"
18 | val exception = Exception(errorMsg)
19 | // when
20 | handler.handleError(exception, errorMsg)
21 | // then
22 | verify(exactly = 1) { handler.handleError(exception, errorMsg) }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/network/observing/NetworkObservingStrategy.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.network.observing
2 |
3 | import android.content.Context
4 | import kotlinx.coroutines.flow.Flow
5 | import ru.beryukhov.reactivenetwork.Connectivity
6 |
7 | /**
8 | * Network observing strategy allows to implement different strategies for monitoring network
9 | * connectivity change. Network monitoring API may differ depending of specific Android version.
10 | */
11 | public interface NetworkObservingStrategy {
12 | /**
13 | * Observes network connectivity
14 | *
15 | * @param context of the Activity or an Application
16 | * @return Observable representing stream of the network connectivity
17 | */
18 | public fun observeNetworkConnectivity(context: Context): Flow
19 |
20 | /**
21 | * Handles errors, which occurred during observing network connectivity
22 | *
23 | * @param message to be processed
24 | * @param exception which was thrown
25 | */
26 | public fun onError(message: String, exception: Exception)
27 | }
28 |
--------------------------------------------------------------------------------
/sample/src/main/java/ru/beryukhov/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.sample
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.activity.ComponentActivity
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.flow.launchIn
9 | import kotlinx.coroutines.flow.onEach
10 | import ru.beryukhov.reactivenetwork.ReactiveNetwork
11 |
12 | class MainActivity : ComponentActivity() {
13 |
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_main)
17 | }
18 |
19 | override fun onResume() {
20 | super.onResume()
21 | ReactiveNetwork().observeInternetConnectivity().onEach {
22 | Log.i("MainActivity", "InternetConnectivity changed on $it")
23 | }.launchIn(CoroutineScope(Dispatchers.Default))
24 |
25 | ReactiveNetwork().observeNetworkConnectivity(applicationContext).onEach {
26 | Log.i("MainActivity", "NetworkConnectivity changed on $it")
27 | }.launchIn(CoroutineScope(Dispatchers.Default))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/TickerFlow.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | import kotlinx.coroutines.Job
4 | import kotlinx.coroutines.channels.awaitClose
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.callbackFlow
7 | import java.util.Timer
8 | import kotlin.concurrent.schedule
9 |
10 | /**
11 | * Creates a flow that produces the first item after the given initial delay and subsequent items with the
12 | * given delay between them.
13 | *
14 | * The resulting flow is a callback flow, which basically listens @see [Timer.schedule]
15 | *
16 | * This Flow stops producing elements immediately after [Job.cancel] invocation.
17 | *
18 | * @param period period between each element in milliseconds.
19 | * @param initialDelay delay after which the first element will be produced (it is equal to [period] by default) in milliseconds.
20 | */
21 | internal fun tickerFlow(
22 | period: Long,
23 | initialDelay: Long = period
24 | ): Flow = callbackFlow {
25 | require(period > 0)
26 | require(initialDelay > -1)
27 |
28 | val timer = Timer()
29 | timer.schedule(initialDelay, period) {
30 | trySend(Unit)
31 | }
32 |
33 | awaitClose { timer.cancel() }
34 | }
35 |
--------------------------------------------------------------------------------
/sample/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | namespace = "ru.beryukhov.sample"
8 | compileSdk = libs.versions.compileSdk.get().toInt()
9 |
10 | defaultConfig {
11 | applicationId = "ru.beryukhov.sample"
12 | minSdk = libs.versions.minSdk.get().toInt()
13 | targetSdk = libs.versions.targetSdk.get().toInt()
14 | versionCode = 1
15 | versionName = "1.0"
16 |
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | isMinifyEnabled = false
23 | proguardFiles(
24 | getDefaultProguardFile("proguard-android-optimize.txt"),
25 | "proguard-rules.pro"
26 | )
27 | }
28 | }
29 | compileOptions {
30 | targetCompatibility = JavaVersion.VERSION_17
31 | sourceCompatibility = JavaVersion.VERSION_17
32 | }
33 | }
34 |
35 | dependencies {
36 | // implementation(flowreactivenetwork)
37 | implementation(projects.reactiveNetwork)
38 |
39 | implementation(libs.coroutines.core)
40 | implementation(libs.coreKtx)
41 | implementation(libs.material)
42 | }
43 |
--------------------------------------------------------------------------------
/scripts/publish-root.gradle:
--------------------------------------------------------------------------------
1 | ext["signing.keyId"] = ''
2 | ext["signing.password"] = ''
3 | ext["signing.key"] = ''
4 | ext["ossrhUsername"] = ''
5 | ext["ossrhPassword"] = ''
6 | ext["sonatypeStagingProfileId"] = ''
7 |
8 | File secretPropsFile = project.rootProject.file('local.properties')
9 | if (secretPropsFile.exists()) {
10 | // Read local.properties file first if it exists
11 | Properties p = new Properties()
12 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
13 | p.each { name, value -> ext[name] = value }
14 | } else {
15 | // Use system environment variables
16 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
17 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
18 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
19 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
20 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
21 | ext["signing.key"] = System.getenv('SIGNING_KEY')
22 | }
23 |
24 | // Set up Sonatype repository
25 | nexusPublishing {
26 | repositories {
27 | sonatype {
28 | stagingProfileId = sonatypeStagingProfileId
29 | username = ossrhUsername
30 | password = ossrhPassword
31 | nexusUrl.set(uri("https://oss.sonatype.org/service/local/"))
32 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
33 |
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/reactiveNetwork/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | `maven-publish`
5 | }
6 |
7 | android {
8 | namespace = "ru.beryukhov.reactivenetwork"
9 |
10 | compileSdk = libs.versions.compileSdk.get().toInt()
11 | testOptions.unitTests.isIncludeAndroidResources = true
12 |
13 | defaultConfig {
14 | minSdk = libs.versions.minSdk.get().toInt()
15 | }
16 | compileOptions {
17 | targetCompatibility = JavaVersion.VERSION_11
18 | }
19 | kotlinOptions {
20 | jvmTarget = "11"
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | }
32 |
33 | kotlin {
34 | explicitApi()
35 | }
36 | }
37 |
38 | dependencies {
39 |
40 | implementation(libs.coroutines.core)
41 | implementation(libs.coroutines.android)
42 |
43 | implementation(libs.androidx.annotation)
44 |
45 | testImplementation(libs.kotlin.test)
46 | testImplementation(libs.coroutines.test)
47 | testImplementation(libs.truth)
48 | testImplementation(libs.robolectric)
49 | testImplementation(libs.mockk)
50 | testImplementation(libs.turbine)
51 | testImplementation(libs.androidx.test)
52 | }
53 |
54 | // apply {from("${rootProject.projectDir}/scripts/publish-root.gradle")}
55 | // apply {from("${rootProject.projectDir}/scripts/publish-module.gradle")}
56 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/network/observing/strategy/LollipopNetworkObservingStrategyTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.network.observing.strategy
2 |
3 | import android.content.Context
4 | import android.net.NetworkInfo
5 | import androidx.test.core.app.ApplicationProvider
6 | import app.cash.turbine.test
7 | import com.google.common.truth.Truth.assertThat
8 | import io.mockk.spyk
9 | import io.mockk.verify
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.test.runTest
12 | import org.junit.Test
13 | import org.junit.runner.RunWith
14 | import org.robolectric.RobolectricTestRunner
15 | import ru.beryukhov.reactivenetwork.network.observing.NetworkObservingStrategy
16 |
17 | @RunWith(RobolectricTestRunner::class)
18 | class LollipopNetworkObservingStrategyTest {
19 |
20 | @Test
21 | fun shouldObserveConnectivity() = runTest {
22 | // given
23 | val strategy: NetworkObservingStrategy = LollipopNetworkObservingStrategy()
24 | val context = ApplicationProvider.getApplicationContext()
25 |
26 | strategy.observeNetworkConnectivity(context).map { it.state }.test {
27 | assertThat(awaitItem()).isEqualTo(NetworkInfo.State.CONNECTED)
28 | }
29 | }
30 |
31 | @Test
32 | fun shouldCallOnError() {
33 | // given
34 | val message = "error message"
35 | val exception = Exception()
36 | val strategy = spyk(LollipopNetworkObservingStrategy())
37 | // when
38 | strategy.onError(message, exception)
39 | // then
40 | verify(exactly = 1) { strategy.onError(message, exception) }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/network/observing/NetworkObservingStrategyTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.network.observing
2 |
3 | import android.content.Context
4 | import android.net.NetworkInfo
5 | import androidx.test.core.app.ApplicationProvider
6 | import app.cash.turbine.test
7 | import com.google.common.truth.Truth.assertThat
8 | import kotlinx.coroutines.flow.map
9 | import kotlinx.coroutines.test.runTest
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.robolectric.RobolectricTestRunner
13 | import ru.beryukhov.reactivenetwork.network.observing.strategy.LollipopNetworkObservingStrategy
14 | import ru.beryukhov.reactivenetwork.network.observing.strategy.PreLollipopNetworkObservingStrategy
15 |
16 | @RunWith(RobolectricTestRunner::class)
17 | class NetworkObservingStrategyTest {
18 | @Test
19 | fun lollipopObserveNetworkConnectivityShouldBeConnectedWhenNetworkIsAvailable() {
20 | // given
21 | val strategy: NetworkObservingStrategy = LollipopNetworkObservingStrategy()
22 | // when
23 | assertThatIsConnected(strategy)
24 | }
25 |
26 | @Test
27 | fun preLollipopObserveNetworkConnectivityShouldBeConnectedWhenNetworkIsAvailable() {
28 | // given
29 | val strategy: NetworkObservingStrategy = PreLollipopNetworkObservingStrategy()
30 | // when
31 | assertThatIsConnected(strategy)
32 | }
33 |
34 | private fun assertThatIsConnected(strategy: NetworkObservingStrategy) = runTest {
35 | // given
36 | val context = ApplicationProvider.getApplicationContext()
37 | // when
38 | strategy.observeNetworkConnectivity(context).map { it.state }.test {
39 | // then
40 | assertThat(awaitItem()).isEqualTo(NetworkInfo.State.CONNECTED)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FlowReactiveNetwork on Coroutines
2 | [ ](https://repo1.maven.org/maven2/ru/beryukhov/flowreactivenetwork/1.0.4/)
3 | [](https://kotlinlang.org)
4 |
5 | [](https://mailchi.mp/kotlinweekly/kotlin-weekly-204)
6 | 
7 |
8 | FlowReactiveNetwork is an Android library listening **network connection state** and **Internet connectivity** with Coroutines Flow. It's a port of [ReactiveNetwork](https://github.com/pwittchen/ReactiveNetwork) library rewritten with Reactive Programming approach. Library supports both new and legacy network monitoring strategies. Min sdk version = 14.
9 |
10 | Usage
11 | -----
12 | See [ReactiveNetwork](https://github.com/pwittchen/ReactiveNetwork) docs for Usage. API is the same except for return data types:
13 | - `Observable` replaced by `Flow`
14 | - `Single` replaced by `suspend fun():T`
15 |
16 | Download
17 | --------
18 |
19 | You can depend on the library through Gradle:
20 |
21 | ```groovy
22 | dependencies {
23 | implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
24 | }
25 | // now the library is available in mavenCentral()
26 | allprojects {
27 | repositories {
28 | //...
29 | mavenCentral() // should probably be here already
30 | }
31 | }
32 | ```
33 |
34 | Tests
35 | -----
36 |
37 | Tests are available in `reactiveNetwork/src/test/kotlin/` directory and can be executed on JVM without any emulator or Android device from Android Studio or CLI with the following command:
38 |
39 | ```
40 | ./gradlew test
41 | ```
42 |
43 | Warning
44 | -----
45 |
46 | There are some problems with working on PreLollipop devices visible by unit-tests and tests on cancellation of Flow.
47 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | publishedLibVersion = "1.0.2"
3 |
4 | targetSdk = "33"
5 | compileSdk = "34"
6 | minSdk = "21"
7 |
8 | kotlin = "1.9.23" # https://kotlinlang.org/docs/releases.html#release-details
9 | agp = "8.2.2" # https://developer.android.com/studio/releases/gradle-plugin
10 | bintray = "1.8.5"
11 |
12 | coroutines = "1.8.0" # https://github.com/Kotlin/kotlinx.coroutines
13 |
14 | detekt = "1.23.6" # https://github.com/detekt/detekt
15 |
16 | [libraries]
17 | kotlinGradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
18 | androidGradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
19 | bintrayGradle = { module = "com.jfrog.bintray.gradle:gradle-bintray-plugin", version.ref = "bintray" }
20 | detektGradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
21 |
22 | detektFormatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
23 | coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
24 | coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
25 | coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
26 |
27 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
28 |
29 | coreKtx = "androidx.core:core-ktx:1.12.0"
30 | material = "com.google.android.material:material:1.11.0"
31 |
32 | truth = "com.google.truth:truth:1.0.1"
33 | robolectric = "org.robolectric:robolectric:4.12"
34 | mockk = "io.mockk:mockk:1.13.10" # https://github.com/mockk/mockk
35 | turbine = "app.cash.turbine:turbine:1.1.0"
36 |
37 | androidx-annotation = "androidx.annotation:annotation:1.7.1" # https://mvnrepository.com/artifact/androidx.annotation/annotation
38 | androidx-test="androidx.test:core:1.5.0"
39 |
40 | flowreactivenetwork = { module = "ru.beryukhov:flowreactivenetwork", version.ref = "publishedLibVersion" }
41 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/network/observing/strategy/PreLollipopNetworkObservingStrategy.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.network.observing.strategy
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.IntentFilter
7 | import android.net.ConnectivityManager
8 | import android.util.Log
9 | import kotlinx.coroutines.channels.awaitClose
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.callbackFlow
12 | import kotlinx.coroutines.flow.distinctUntilChanged
13 | import kotlinx.coroutines.flow.onStart
14 | import ru.beryukhov.reactivenetwork.Connectivity
15 | import ru.beryukhov.reactivenetwork.ReactiveNetwork
16 | import ru.beryukhov.reactivenetwork.network.observing.NetworkObservingStrategy
17 |
18 | /**
19 | * Network observing strategy for Android devices before Lollipop (API 20 or lower).
20 | * Uses Broadcast Receiver.
21 | */
22 | public class PreLollipopNetworkObservingStrategy : NetworkObservingStrategy {
23 |
24 | override fun observeNetworkConnectivity(context: Context): Flow {
25 | val filter = IntentFilter()
26 | filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
27 | return callbackFlow {
28 | val receiver: BroadcastReceiver = object : BroadcastReceiver() {
29 | override fun onReceive(
30 | context: Context,
31 | intent: Intent
32 | ) {
33 | trySend(Connectivity.create(context))
34 | }
35 | }
36 | context.registerReceiver(receiver, filter)
37 | awaitClose {
38 | tryToUnregisterReceiver(context, receiver)
39 | }
40 | }.onStart { emit(Connectivity.create(context)) }.distinctUntilChanged()
41 | }
42 |
43 | internal fun tryToUnregisterReceiver(
44 | context: Context,
45 | receiver: BroadcastReceiver?
46 | ) {
47 | try {
48 | context.unregisterReceiver(receiver)
49 | } catch (exception: Exception) {
50 | onError("receiver was already unregistered", exception)
51 | }
52 | }
53 |
54 | override fun onError(
55 | message: String,
56 | exception: Exception
57 | ) {
58 | Log.e(ReactiveNetwork.LOG_TAG, message, exception)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/internet/observing/InternetObservingStrategy.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
5 |
6 | /**
7 | * Internet observing strategy allows to implement different strategies for monitoring connectivity
8 | * with the Internet.
9 | */
10 | public interface InternetObservingStrategy {
11 | /**
12 | * Observes connectivity with the Internet by opening socket connection with remote host in a
13 | * given interval infinitely
14 | *
15 | * @param initialIntervalInMs in milliseconds determining the delay of the first connectivity
16 | * check
17 | * @param intervalInMs in milliseconds determining how often we want to check connectivity
18 | * @param host for checking Internet connectivity
19 | * @param port for checking Internet connectivity
20 | * @param timeoutInMs for pinging remote host in milliseconds
21 | * @param errorHandler for handling errors while checking connectivity
22 | * @return Flow with Boolean - true, when we have connection with host and false if
23 | * not
24 | */
25 | public fun observeInternetConnectivity(
26 | initialIntervalInMs: Int,
27 | intervalInMs: Int,
28 | host: String,
29 | port: Int,
30 | timeoutInMs: Int,
31 | httpResponse: Int,
32 | errorHandler: ErrorHandler
33 | ): Flow
34 |
35 | /**
36 | * Observes connectivity with the Internet by opening socket connection with remote host once
37 | *
38 | * @param host for checking Internet connectivity
39 | * @param port for checking Internet connectivity
40 | * @param timeoutInMs for pinging remote host in milliseconds
41 | * @param errorHandler for handling errors while checking connectivity
42 | * @return Boolean - true, when we have connection with host and false if
43 | * not
44 | */
45 | public suspend fun checkInternetConnectivity(
46 | host: String,
47 | port: Int,
48 | timeoutInMs: Int,
49 | httpResponse: Int,
50 | errorHandler: ErrorHandler
51 | ): Boolean
52 |
53 | /**
54 | * Gets default remote ping host for a given Internet Observing Strategy
55 | *
56 | * @return String with a ping host used in the current strategy
57 | */
58 | public fun getDefaultPingHost(): String
59 | }
60 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/Preconditions.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | import android.os.Build
4 | import androidx.annotation.ChecksSdkIntAtLeast
5 |
6 | public object Preconditions {
7 | /**
8 | * Validation method, which checks if an object is null
9 | *
10 | * @param o object to verify
11 | * @param message to be thrown in exception
12 | */
13 | public fun checkNotNull(o: Any?, message: String) {
14 | if (o == null) {
15 | throw IllegalArgumentException(message)
16 | }
17 | }
18 |
19 | /**
20 | * Validation method, which checks if a string is null or empty
21 | *
22 | * @param string to verify
23 | * @param message to be thrown in exception
24 | */
25 | public fun checkNotNullOrEmpty(string: String?, message: String) {
26 | if (string.isNullOrEmpty()) {
27 | throw IllegalArgumentException(message)
28 | }
29 | }
30 |
31 | /**
32 | * Validation method, which checks is an integer number is positive
33 | *
34 | * @param number integer to verify
35 | * @param message to be thrown in exception
36 | */
37 | public fun checkGreaterOrEqualToZero(number: Int, message: String) {
38 | if (number < 0) {
39 | throw IllegalArgumentException(message)
40 | }
41 | }
42 |
43 | /**
44 | * Validation method, which checks is an integer number is non-zero or positive
45 | *
46 | * @param number integer to verify
47 | * @param message to be thrown in exception
48 | */
49 | public fun checkGreaterThanZero(number: Int, message: String) {
50 | if (number <= 0) {
51 | throw IllegalArgumentException(message)
52 | }
53 | }
54 |
55 | /**
56 | * Validation method, which checks if current Android version is at least Lollipop (API 21) or
57 | * higher
58 | *
59 | * @return boolean true if current Android version is Lollipop or higher
60 | */
61 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.LOLLIPOP)
62 | public fun isAtLeastAndroidLollipop(): Boolean {
63 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
64 | }
65 |
66 | /**
67 | * Validation method, which checks if current Android version is at least Marshmallow (API 23) or
68 | * higher
69 | *
70 | * @return boolean true if current Android version is Marshmallow or higher
71 | */
72 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M)
73 | public fun isAtLeastAndroidMarshmallow(): Boolean {
74 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/network/observing/strategy/LollipopNetworkObservingStrategy.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.network.observing.strategy
2 |
3 | import android.annotation.TargetApi
4 | import android.content.Context
5 | import android.net.ConnectivityManager
6 | import android.net.ConnectivityManager.NetworkCallback
7 | import android.net.Network
8 | import android.net.NetworkRequest
9 | import android.util.Log
10 | import kotlinx.coroutines.channels.awaitClose
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.callbackFlow
13 | import kotlinx.coroutines.flow.distinctUntilChanged
14 | import kotlinx.coroutines.flow.onStart
15 | import ru.beryukhov.reactivenetwork.Connectivity
16 | import ru.beryukhov.reactivenetwork.ReactiveNetwork
17 | import ru.beryukhov.reactivenetwork.network.observing.NetworkObservingStrategy
18 |
19 | /**
20 | * Network observing strategy for devices with Android Lollipop (API 21) or higher.
21 | * Uses Network Callback API.
22 | */
23 | @TargetApi(21)
24 | public class LollipopNetworkObservingStrategy : NetworkObservingStrategy {
25 | // it has to be initialized in the Observable due to Context
26 | private lateinit var networkCallback: NetworkCallback
27 |
28 | override fun observeNetworkConnectivity(context: Context): Flow {
29 | val service = Context.CONNECTIVITY_SERVICE
30 | val manager = context.getSystemService(service) as ConnectivityManager
31 | return callbackFlow {
32 | networkCallback = object : NetworkCallback() {
33 | override fun onAvailable(network: Network) {
34 | trySend(Connectivity.create(context))
35 | }
36 |
37 | override fun onLost(network: Network) {
38 | trySend(Connectivity.create(context))
39 | }
40 | }
41 |
42 | val networkRequest = NetworkRequest.Builder().build()
43 | manager.registerNetworkCallback(networkRequest, networkCallback)
44 |
45 | awaitClose { tryToUnregisterCallback(manager) }
46 | }.onStart { emit(Connectivity.create(context)) }.distinctUntilChanged()
47 | }
48 |
49 | private fun tryToUnregisterCallback(manager: ConnectivityManager) {
50 | try {
51 | manager.unregisterNetworkCallback(networkCallback)
52 | } catch (exception: Exception) {
53 | onError("could not unregister network callback", exception)
54 | }
55 | }
56 |
57 | override fun onError(
58 | message: String,
59 | exception: Exception
60 | ) {
61 | Log.e(ReactiveNetwork.LOG_TAG, message, exception)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/ConnectivityPredicate.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | import android.net.NetworkInfo
4 |
5 | /**
6 | * ConnectivityPredicate is a class containing predefined methods, which can be used for filtering
7 | * reactive streams of network connectivity
8 | */
9 | public object ConnectivityPredicate {
10 | /**
11 | * Filter, which returns true if at least one given state occurred
12 | *
13 | * @param states NetworkInfo.State, which can have one or more states
14 | * @return true if at least one given state occurred
15 | */
16 | @JvmStatic
17 | public fun hasState(vararg states: NetworkInfo.State): Predicate {
18 | return object : Predicate {
19 | @Throws(Exception::class)
20 | override fun test(connectivity: Connectivity): Boolean {
21 | for (state in states) {
22 | if (connectivity.state == state) {
23 | return true
24 | }
25 | }
26 | return false
27 | }
28 | }
29 | }
30 |
31 | /**
32 | * Filter, which returns true if at least one given type occurred
33 | *
34 | * @param types int, which can have one or more types
35 | * @return true if at least one given type occurred
36 | */
37 | @JvmStatic
38 | public fun hasType(vararg types: Int): Predicate {
39 | val extendedTypes =
40 | appendUnknownNetworkTypeToTypes(types)
41 | return object : Predicate {
42 | @Throws(Exception::class)
43 | override fun test(connectivity: Connectivity): Boolean {
44 | for (type in extendedTypes) {
45 | if (connectivity.type == type) {
46 | return true
47 | }
48 | }
49 | return false
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * Returns network types from the input with additional unknown type,
56 | * what helps during connections filtering when device
57 | * is being disconnected from a specific network
58 | *
59 | * @param types of the network as an array of ints
60 | * @return types of the network with unknown type as an array of ints
61 | */
62 | @JvmStatic
63 | public fun appendUnknownNetworkTypeToTypes(types: IntArray): IntArray {
64 | var i = 0
65 | val extendedTypes = IntArray(types.size + 1)
66 | for (type in types) {
67 | extendedTypes[i] = type
68 | i++
69 | }
70 | extendedTypes[i] = Connectivity.UNKNOWN_TYPE
71 | return extendedTypes
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/Connectivity.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkInfo
6 | import android.net.NetworkInfo.DetailedState
7 |
8 | /**
9 | * Connectivity class represents current connectivity status. It wraps NetworkInfo object.
10 | */
11 | public data class Connectivity(
12 | val state: NetworkInfo.State = NetworkInfo.State.DISCONNECTED,
13 | val detailedState: DetailedState? = DetailedState.IDLE,
14 | val type: Int = UNKNOWN_TYPE,
15 | val subType: Int = UNKNOWN_SUB_TYPE,
16 | val available: Boolean = false,
17 | val failover: Boolean = false,
18 | val roaming: Boolean = false,
19 | val typeName: String? = "NONE",
20 | val subTypeName: String? = "NONE",
21 | val reason: String? = "",
22 | val extraInfo: String? = ""
23 | ) {
24 | public companion object {
25 | public const val UNKNOWN_TYPE: Int = -1
26 | public const val UNKNOWN_SUB_TYPE: Int = -1
27 |
28 | public fun create(context: Context): Connectivity {
29 | Preconditions.checkNotNull(context, "context == null")
30 | return create(
31 | context,
32 | getConnectivityManager(context)
33 | )
34 | }
35 |
36 | private fun getConnectivityManager(context: Context): ConnectivityManager {
37 | val service = Context.CONNECTIVITY_SERVICE
38 | return context.getSystemService(service) as ConnectivityManager
39 | }
40 |
41 | internal fun create(
42 | context: Context,
43 | manager: ConnectivityManager?
44 | ): Connectivity {
45 | Preconditions.checkNotNull(context, "context == null")
46 | if (manager == null) {
47 | return Connectivity()
48 | }
49 | val networkInfo = manager.activeNetworkInfo
50 | return networkInfo?.let { create(it) } ?: Connectivity()
51 | }
52 |
53 | private fun create(networkInfo: NetworkInfo): Connectivity {
54 | return Connectivity(
55 | state = networkInfo.state,
56 | detailedState = networkInfo.detailedState,
57 | type = networkInfo.type,
58 | subType = networkInfo.subtype,
59 | available = networkInfo.isAvailable,
60 | failover = networkInfo.isFailover,
61 | roaming = networkInfo.isRoaming,
62 | typeName = networkInfo.typeName,
63 | subTypeName = networkInfo.subtypeName,
64 | reason = networkInfo.reason,
65 | extraInfo = networkInfo.extraInfo
66 | )
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/network/observing/strategy/PreLollipopNetworkObservingStrategyTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.network.observing.strategy
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.net.NetworkInfo
6 | import androidx.test.core.app.ApplicationProvider
7 | import app.cash.turbine.test
8 | import com.google.common.truth.Truth.assertThat
9 | import io.mockk.mockk
10 | import io.mockk.spyk
11 | import io.mockk.verify
12 | import kotlinx.coroutines.delay
13 | import kotlinx.coroutines.flow.map
14 | import kotlinx.coroutines.test.runTest
15 | import org.junit.Test
16 | import org.junit.runner.RunWith
17 | import org.robolectric.RobolectricTestRunner
18 | import ru.beryukhov.reactivenetwork.network.observing.NetworkObservingStrategy
19 |
20 | @RunWith(RobolectricTestRunner::class)
21 | open class PreLollipopNetworkObservingStrategyTest {
22 |
23 | @Test
24 | fun shouldObserveConnectivity() = runTest {
25 | // given
26 | val strategy: NetworkObservingStrategy = PreLollipopNetworkObservingStrategy()
27 | val context = ApplicationProvider.getApplicationContext()
28 | // when
29 | strategy.observeNetworkConnectivity(context).map { it.state }.test {
30 | delay(1000)
31 | // then
32 | assertThat(awaitItem()).isEqualTo(NetworkInfo.State.CONNECTED)
33 | }
34 | }
35 |
36 | @Test
37 | fun shouldCallOnError() {
38 | // given
39 | val message = "error message"
40 | val exception = Exception()
41 | val strategy = spyk(PreLollipopNetworkObservingStrategy())
42 | // when
43 | strategy.onError(message, exception)
44 | // then
45 | verify(exactly = 1) { strategy.onError(message, exception) }
46 | }
47 |
48 | @Test
49 | fun shouldTryToUnregisterReceiver() {
50 | // given
51 | val strategy = PreLollipopNetworkObservingStrategy()
52 | val context = spyk(ApplicationProvider.getApplicationContext())
53 | val broadcastReceiver = mockk(relaxed = true)
54 | // when
55 | strategy.tryToUnregisterReceiver(context, broadcastReceiver)
56 | // then
57 | verify { context.unregisterReceiver(broadcastReceiver) }
58 | }
59 |
60 | @Test
61 | fun shouldTryToUnregisterReceiverAfterDispose() = runTest {
62 | // given
63 | val context = ApplicationProvider.getApplicationContext()
64 | val strategy = spyk(PreLollipopNetworkObservingStrategy())
65 | // when
66 |
67 | strategy.observeNetworkConnectivity(context).test {
68 | cancelAndConsumeRemainingEvents()
69 | }
70 | // then
71 | verify { strategy.tryToUnregisterReceiver(context, any()) }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/scripts/publish-module.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'signing'
3 |
4 | ext {
5 | PUBLISH_GROUP_ID = 'ru.beryukhov'
6 | PUBLISH_VERSION = '1.0.4'
7 | PUBLISH_ARTIFACT_ID = 'flowreactivenetwork'
8 | }
9 |
10 | task androidSourcesJar(type: Jar) {
11 | archiveClassifier.set('sources')
12 | if (project.plugins.findPlugin("com.android.library")) {
13 | // For Android libraries
14 | from android.sourceSets.main.java.srcDirs
15 | from android.sourceSets.main.kotlin.srcDirs
16 | } else {
17 | // For pure Kotlin libraries, in case you have them
18 | from sourceSets.main.java.srcDirs
19 | from sourceSets.main.kotlin.srcDirs
20 | }
21 | }
22 |
23 | artifacts {
24 | archives androidSourcesJar
25 | }
26 |
27 | group = PUBLISH_GROUP_ID
28 | version = PUBLISH_VERSION
29 |
30 | afterEvaluate {
31 | publishing {
32 | publications {
33 | release(MavenPublication) {
34 | groupId PUBLISH_GROUP_ID
35 | artifactId PUBLISH_ARTIFACT_ID
36 | version PUBLISH_VERSION
37 |
38 | // Two artifacts, the `aar` (or `jar`) and the sources
39 | if (project.plugins.findPlugin("com.android.library")) {
40 | from components.release
41 | } else {
42 | from components.java
43 | }
44 |
45 | artifact androidSourcesJar
46 | //artifact javadocJar
47 |
48 | pom {
49 | name = PUBLISH_ARTIFACT_ID
50 | description = 'Android library listening network connection state and Internet connectivity with Coroutines Flow'
51 | url = 'https://github.com/phansier/FlowReactiveNetwork'
52 | licenses {
53 | license {
54 | name = 'Apache-2.0 License'
55 | url = 'https://github.com/phansier/FlowReactiveNetwork/blob/master/LICENSE'
56 | }
57 | }
58 | developers {
59 | developer {
60 | id = 'phansier'
61 | name = 'Andrey Beryukhov'
62 | email = 'beryukhov-andrey@yandex.ru'
63 | }
64 | }
65 | scm {
66 | connection = 'scm:git:github.com/phansier/FlowReactiveNetwork.git'
67 | developerConnection = 'scm:git:ssh://github.com/phansier/FlowReactiveNetwork.git'
68 | url = 'https://github.com/phansier/FlowReactiveNetwork/tree/main'
69 | }
70 | }
71 | }
72 | }
73 | }
74 | }
75 |
76 | signing {
77 | useInMemoryPgpKeys(
78 | //todo refine use of ext here
79 | "rootProject.ext[\"signing.keyId\"]",
80 | "rootProject.ext[\"signing.key\"]",
81 | "rootProject.ext[\"signing.password\"]",
82 | )
83 | sign publishing.publications
84 | }
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/internet/observing/InternetObservingSettingsTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 | import org.junit.runner.RunWith
6 | import org.robolectric.RobolectricTestRunner
7 | import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingSettings.Companion.builder
8 | import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingSettings.Companion.create
9 | import ru.beryukhov.reactivenetwork.internet.observing.error.DefaultErrorHandler
10 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
11 | import ru.beryukhov.reactivenetwork.internet.observing.strategy.SocketInternetObservingStrategy
12 | import ru.beryukhov.reactivenetwork.internet.observing.strategy.WalledGardenInternetObservingStrategy
13 |
14 | @RunWith(RobolectricTestRunner::class)
15 | class InternetObservingSettingsTest {
16 | @Test
17 | fun shouldCreateSettings() { // when
18 | val settings = create()
19 | // then
20 | assertThat(settings).isNotNull()
21 | }
22 |
23 | @Test
24 | fun shouldBuildSettingsWithDefaultValues() { // when
25 | val settings = create()
26 | // then
27 | assertThat(settings.initialInterval()).isEqualTo(0)
28 | assertThat(settings.interval()).isEqualTo(2000)
29 | assertThat(settings.host()).isEqualTo("http://clients3.google.com/generate_204")
30 | assertThat(settings.port()).isEqualTo(80)
31 | assertThat(settings.timeout()).isEqualTo(2000)
32 | assertThat(settings.httpResponse()).isEqualTo(204)
33 | assertThat(settings.errorHandler())
34 | .isInstanceOf(DefaultErrorHandler::class.java)
35 | assertThat(settings.strategy())
36 | .isInstanceOf(WalledGardenInternetObservingStrategy::class.java)
37 | }
38 |
39 | @Test
40 | fun shouldBuildSettings() {
41 | // given
42 | val initialInterval = 1
43 | val interval = 2
44 | val host = "www.test.com"
45 | val port = 90
46 | val timeout = 3
47 | val httpResponse = 200
48 | val testErrorHandler =
49 | createTestErrorHandler()
50 | val strategy = SocketInternetObservingStrategy()
51 | // when
52 | val settings = builder()
53 | .initialInterval(initialInterval)
54 | .interval(interval)
55 | .host(host)
56 | .port(port)
57 | .timeout(timeout)
58 | .httpResponse(httpResponse)
59 | .errorHandler(testErrorHandler)
60 | .strategy(strategy)
61 | .build()
62 | // then
63 | assertThat(settings.initialInterval()).isEqualTo(initialInterval)
64 | assertThat(settings.interval()).isEqualTo(interval)
65 | assertThat(settings.host()).isEqualTo(host)
66 | assertThat(settings.port()).isEqualTo(port)
67 | assertThat(settings.timeout()).isEqualTo(timeout)
68 | assertThat(settings.httpResponse()).isEqualTo(httpResponse)
69 | assertThat(settings.errorHandler()).isNotNull()
70 | assertThat(settings.errorHandler())
71 | .isNotInstanceOf(DefaultErrorHandler::class.java)
72 | assertThat(settings.strategy())
73 | .isInstanceOf(SocketInternetObservingStrategy::class.java)
74 | }
75 |
76 | private fun createTestErrorHandler(): ErrorHandler {
77 | return object : ErrorHandler {
78 | override fun handleError(
79 | exception: Exception?,
80 | message: String?
81 | ) {
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/PreconditionsTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 | import org.junit.runner.RunWith
6 | import org.robolectric.RobolectricTestRunner
7 | import org.robolectric.annotation.Config
8 | import ru.beryukhov.reactivenetwork.Preconditions.checkGreaterOrEqualToZero
9 | import ru.beryukhov.reactivenetwork.Preconditions.checkGreaterThanZero
10 | import ru.beryukhov.reactivenetwork.Preconditions.checkNotNullOrEmpty
11 | import ru.beryukhov.reactivenetwork.Preconditions.isAtLeastAndroidLollipop
12 | import ru.beryukhov.reactivenetwork.Preconditions.isAtLeastAndroidMarshmallow
13 |
14 | @RunWith(RobolectricTestRunner::class)
15 | class PreconditionsTest {
16 | @Test
17 | @Config(sdk = [21])
18 | fun shouldBeAtLeastAndroidLollipop() {
19 | val isAtLeastAndroidLollipop = isAtLeastAndroidLollipop()
20 | assertThat(isAtLeastAndroidLollipop).isTrue()
21 | }
22 |
23 | @Test
24 | @Config(sdk = [22])
25 | fun shouldBeAtLeastAndroidLollipopForHigherApi() {
26 | val isAtLeastAndroidLollipop = isAtLeastAndroidLollipop()
27 | assertThat(isAtLeastAndroidLollipop).isTrue()
28 | }
29 |
30 | @Test
31 | @Config(sdk = [22])
32 | fun shouldNotBeAtLeastAndroidMarshmallowForLowerApi() {
33 | val isAtLeastAndroidMarshmallow = isAtLeastAndroidMarshmallow()
34 | assertThat(isAtLeastAndroidMarshmallow).isFalse()
35 | }
36 |
37 | @Test
38 | @Config(sdk = [23])
39 | fun shouldBeAtLeastAndroidMarshmallow() {
40 | val isAtLeastAndroidMarshmallow = isAtLeastAndroidMarshmallow()
41 | assertThat(isAtLeastAndroidMarshmallow).isTrue()
42 | }
43 |
44 | @Test(expected = IllegalArgumentException::class)
45 | fun shouldThrowAnExceptionWhenStringIsNull() {
46 | checkNotNullOrEmpty(
47 | null,
48 | MSG_STRING_IS_NULL
49 | )
50 | }
51 |
52 | @Test(expected = IllegalArgumentException::class)
53 | fun shouldThrowAnExceptionWhenStringIsEmpty() {
54 | checkNotNullOrEmpty(
55 | "",
56 | MSG_STRING_IS_NULL
57 | )
58 | }
59 |
60 | @Test
61 | fun shouldNotThrowAnythingWhenStringIsNotEmpty() {
62 | checkNotNullOrEmpty(
63 | "notEmpty",
64 | MSG_STRING_IS_NULL
65 | )
66 | }
67 |
68 | @Test(expected = IllegalArgumentException::class)
69 | fun shouldThrowAnExceptionWhenValueIsZero() {
70 | checkGreaterThanZero(
71 | 0,
72 | MSG_VALUE_IS_NOT_GREATER_THAN_ZERO
73 | )
74 | }
75 |
76 | @Test(expected = IllegalArgumentException::class)
77 | fun shouldThrowAnExceptionWhenValueLowerThanZero() {
78 | checkGreaterThanZero(
79 | -1,
80 | MSG_VALUE_IS_NOT_GREATER_THAN_ZERO
81 | )
82 | }
83 |
84 | @Test
85 | fun shouldNotThrowAnythingWhenValueIsGreaterThanZero() {
86 | checkGreaterThanZero(
87 | 1,
88 | MSG_VALUE_IS_NOT_GREATER_THAN_ZERO
89 | )
90 | }
91 |
92 | @Test(expected = IllegalArgumentException::class)
93 | fun shouldThrowAnExceptionWhenValueLowerThanZeroForGreaterOrEqualCheck() {
94 | checkGreaterOrEqualToZero(
95 | -1,
96 | MSG_VALUE_IS_NOT_GREATER_THAN_ZERO
97 | )
98 | }
99 |
100 | @Test
101 | fun shouldNotThrowAnythingWhenValueIsGreaterThanZeroForGreaterOrEqualCheck() {
102 | checkGreaterOrEqualToZero(
103 | 1,
104 | MSG_VALUE_IS_NOT_GREATER_THAN_ZERO
105 | )
106 | }
107 |
108 | @Test
109 | fun shouldNotThrowAnythingWhenValueIsEqualToZero() {
110 | checkGreaterOrEqualToZero(
111 | 0,
112 | MSG_VALUE_IS_NOT_GREATER_THAN_ZERO
113 | )
114 | }
115 |
116 | companion object {
117 | private const val MSG_STRING_IS_NULL = "String is null"
118 | private const val MSG_VALUE_IS_NOT_GREATER_THAN_ZERO =
119 | "value is not greater than zero"
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/internet/observing/strategy/SocketInternetObservingStrategy.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing.strategy
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.distinctUntilChanged
5 | import kotlinx.coroutines.flow.map
6 | import ru.beryukhov.reactivenetwork.Preconditions
7 | import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingStrategy
8 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
9 | import ru.beryukhov.reactivenetwork.tickerFlow
10 | import java.io.IOException
11 | import java.net.InetSocketAddress
12 | import java.net.Socket
13 |
14 | /**
15 | * Socket strategy for monitoring connectivity with the Internet.
16 | * It monitors Internet connectivity via opening socket connection with the remote host.
17 | */
18 | public class SocketInternetObservingStrategy : InternetObservingStrategy {
19 | override fun getDefaultPingHost(): String {
20 | return DEFAULT_HOST
21 | }
22 |
23 | override fun observeInternetConnectivity(
24 | initialIntervalInMs: Int,
25 | intervalInMs: Int,
26 | host: String,
27 | port: Int,
28 | timeoutInMs: Int,
29 | httpResponse: Int,
30 | errorHandler: ErrorHandler
31 | ): Flow {
32 | Preconditions.checkGreaterOrEqualToZero(
33 | initialIntervalInMs,
34 | "initialIntervalInMs is not a positive number"
35 | )
36 | Preconditions.checkGreaterThanZero(
37 | intervalInMs,
38 | "intervalInMs is not a positive number"
39 | )
40 | checkGeneralPreconditions(host, port, timeoutInMs, errorHandler)
41 | val adjustedHost = adjustHost(host)
42 | return tickerFlow(
43 | period = intervalInMs.toLong(),
44 | initialDelay = initialIntervalInMs.toLong()
45 | ).map { isConnected(adjustedHost, port, timeoutInMs, errorHandler) }.distinctUntilChanged()
46 | }
47 |
48 | override suspend fun checkInternetConnectivity(
49 | host: String,
50 | port: Int,
51 | timeoutInMs: Int,
52 | httpResponse: Int,
53 | errorHandler: ErrorHandler
54 | ): Boolean {
55 | checkGeneralPreconditions(host, port, timeoutInMs, errorHandler)
56 | return isConnected(host, port, timeoutInMs, errorHandler)
57 | }
58 |
59 | /**
60 | * adjusts host to needs of SocketInternetObservingStrategy
61 | *
62 | * @return transformed host
63 | */
64 | internal fun adjustHost(host: String): String {
65 | if (host.startsWith(HTTP_PROTOCOL)) {
66 | return host.replace(
67 | HTTP_PROTOCOL,
68 | EMPTY_STRING
69 | )
70 | } else if (host.startsWith(HTTPS_PROTOCOL)) {
71 | return host.replace(
72 | HTTPS_PROTOCOL,
73 | EMPTY_STRING
74 | )
75 | }
76 | return host
77 | }
78 |
79 | private fun checkGeneralPreconditions(host: String, port: Int, timeoutInMs: Int, errorHandler: ErrorHandler) {
80 | Preconditions.checkNotNullOrEmpty(
81 | host,
82 | "host is null or empty"
83 | )
84 | Preconditions.checkGreaterThanZero(
85 | port,
86 | "port is not a positive number"
87 | )
88 | Preconditions.checkGreaterThanZero(
89 | timeoutInMs,
90 | "timeoutInMs is not a positive number"
91 | )
92 | Preconditions.checkNotNull(
93 | errorHandler,
94 | "errorHandler is null"
95 | )
96 | }
97 |
98 | /**
99 | * checks if device is connected to given host at given port
100 | *
101 | * @param host to connect
102 | * @param port to connect
103 | * @param timeoutInMs connection timeout
104 | * @param errorHandler error handler for socket connection
105 | * @return boolean true if connected and false if not
106 | */
107 | internal fun isConnected(host: String?, port: Int, timeoutInMs: Int, errorHandler: ErrorHandler): Boolean {
108 | val socket = Socket()
109 | return isConnected(socket, host, port, timeoutInMs, errorHandler)
110 | }
111 |
112 | /**
113 | * checks if device is connected to given host at given port
114 | *
115 | * @param socket to connect
116 | * @param host to connect
117 | * @param port to connect
118 | * @param timeoutInMs connection timeout
119 | * @param errorHandler error handler for socket connection
120 | * @return boolean true if connected and false if not
121 | */
122 | internal fun isConnected(
123 | socket: Socket,
124 | host: String?,
125 | port: Int,
126 | timeoutInMs: Int,
127 | errorHandler: ErrorHandler
128 | ): Boolean {
129 | return try {
130 | socket.connect(InetSocketAddress(host, port), timeoutInMs)
131 | socket.isConnected
132 | } catch (e: IOException) {
133 | java.lang.Boolean.FALSE
134 | } finally {
135 | try {
136 | socket.close()
137 | } catch (exception: IOException) {
138 | errorHandler.handleError(exception, "Could not close the socket")
139 | }
140 | }
141 | }
142 |
143 | private companion object {
144 | private const val EMPTY_STRING = ""
145 | private const val DEFAULT_HOST = "www.google.com"
146 | private const val HTTP_PROTOCOL = "http://"
147 | private const val HTTPS_PROTOCOL = "https://"
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/internet/observing/InternetObservingSettings.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing
2 |
3 | import ru.beryukhov.reactivenetwork.internet.observing.error.DefaultErrorHandler
4 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
5 | import ru.beryukhov.reactivenetwork.internet.observing.strategy.WalledGardenInternetObservingStrategy
6 | import java.net.HttpURLConnection
7 |
8 | /**
9 | * Contains state of internet connectivity settings.
10 | * We should use its Builder for creating new settings
11 | */
12 | // I want to have the same method names as variable names on purpose
13 | public class InternetObservingSettings private constructor(
14 | private val initialInterval: Int,
15 | private val interval: Int,
16 | private val host: String,
17 | private val port: Int,
18 | private val timeout: Int,
19 | private val httpResponse: Int,
20 | private val errorHandler: ErrorHandler,
21 | private val strategy: InternetObservingStrategy
22 | ) {
23 |
24 | private constructor(builder: Builder = builder()) : this(
25 | builder.initialInterval, builder.interval, builder.host, builder.port, builder.timeout,
26 | builder.httpResponse, builder.errorHandler, builder.strategy
27 | )
28 |
29 | /**
30 | * @return initial ping interval in milliseconds
31 | */
32 | public fun initialInterval(): Int {
33 | return initialInterval
34 | }
35 |
36 | /**
37 | * @return ping interval in milliseconds
38 | */
39 | public fun interval(): Int {
40 | return interval
41 | }
42 |
43 | /**
44 | * @return ping host
45 | */
46 | public fun host(): String {
47 | return host
48 | }
49 |
50 | /**
51 | * @return ping port
52 | */
53 | public fun port(): Int {
54 | return port
55 | }
56 |
57 | /**
58 | * @return ping timeout in milliseconds
59 | */
60 | public fun timeout(): Int {
61 | return timeout
62 | }
63 |
64 | public fun httpResponse(): Int {
65 | return httpResponse
66 | }
67 |
68 | /**
69 | * @return error handler for pings and connections
70 | */
71 | public fun errorHandler(): ErrorHandler {
72 | return errorHandler
73 | }
74 |
75 | /**
76 | * @return internet observing strategy
77 | */
78 | public fun strategy(): InternetObservingStrategy {
79 | return strategy
80 | }
81 |
82 | /**
83 | * Settings builder, which contains default parameters
84 | */
85 | public class Builder internal constructor() {
86 | internal var initialInterval = 0
87 | internal var interval = 2000
88 | internal var host = "http://clients3.google.com/generate_204"
89 | internal var port = 80
90 | internal var timeout = 2000
91 | internal var httpResponse = HttpURLConnection.HTTP_NO_CONTENT
92 | internal var errorHandler: ErrorHandler =
93 | DefaultErrorHandler()
94 | internal var strategy: InternetObservingStrategy = WalledGardenInternetObservingStrategy()
95 |
96 | /**
97 | * sets initial ping interval in milliseconds
98 | *
99 | * @param initialInterval in milliseconds
100 | * @return Builder
101 | */
102 | public fun initialInterval(initialInterval: Int): Builder {
103 | this.initialInterval = initialInterval
104 | return this
105 | }
106 |
107 | /**
108 | * sets ping interval in milliseconds
109 | *
110 | * @param interval in milliseconds
111 | * @return Builder
112 | */
113 | public fun interval(interval: Int): Builder {
114 | this.interval = interval
115 | return this
116 | }
117 |
118 | /**
119 | * sets ping host
120 | *
121 | * @return Builder
122 | */
123 | public fun host(host: String): Builder {
124 | this.host = host
125 | return this
126 | }
127 |
128 | /**
129 | * sets ping port
130 | *
131 | * @return Builder
132 | */
133 | public fun port(port: Int): Builder {
134 | this.port = port
135 | return this
136 | }
137 |
138 | /**
139 | * sets ping timeout in milliseconds
140 | *
141 | * @param timeout in milliseconds
142 | * @return Builder
143 | */
144 | public fun timeout(timeout: Int): Builder {
145 | this.timeout = timeout
146 | return this
147 | }
148 |
149 | /**
150 | * sets HTTP response code indicating that connection is established
151 | *
152 | * @param httpResponse as integer
153 | * @return Builder
154 | */
155 | public fun httpResponse(httpResponse: Int): Builder {
156 | this.httpResponse = httpResponse
157 | return this
158 | }
159 |
160 | /**
161 | * sets error handler for pings and connections
162 | *
163 | * @return Builder
164 | */
165 | public fun errorHandler(errorHandler: ErrorHandler): Builder {
166 | this.errorHandler = errorHandler
167 | return this
168 | }
169 |
170 | /**
171 | * sets internet observing strategy
172 | *
173 | * @param strategy for observing and internet connection
174 | * @return Builder
175 | */
176 | public fun strategy(strategy: InternetObservingStrategy): Builder {
177 | this.strategy = strategy
178 | return this
179 | }
180 |
181 | public fun build(): InternetObservingSettings {
182 | return InternetObservingSettings(this)
183 | }
184 | }
185 |
186 | public companion object {
187 | /**
188 | * @return settings with default parameters
189 | */
190 | public fun create(): InternetObservingSettings {
191 | return Builder()
192 | .build()
193 | }
194 |
195 | /**
196 | * Creates builder object
197 | * @return Builder
198 | */
199 | public fun builder(): Builder {
200 | return Builder()
201 | }
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/network/observing/strategy/MarshmallowNetworkObservingStrategy.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.network.observing.strategy
2 |
3 | import android.annotation.TargetApi
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.IntentFilter
8 | import android.net.ConnectivityManager
9 | import android.net.ConnectivityManager.NetworkCallback
10 | import android.net.Network
11 | import android.net.NetworkCapabilities
12 | import android.net.NetworkInfo
13 | import android.net.NetworkRequest
14 | import android.os.PowerManager
15 | import android.util.Log
16 | import kotlinx.coroutines.flow.Flow
17 | import kotlinx.coroutines.flow.MutableSharedFlow
18 | import kotlinx.coroutines.flow.distinctUntilChanged
19 | import kotlinx.coroutines.flow.flatMapConcat
20 | import kotlinx.coroutines.flow.flowOf
21 | import kotlinx.coroutines.flow.onCompletion
22 | import kotlinx.coroutines.flow.onStart
23 | import ru.beryukhov.reactivenetwork.Connectivity
24 | import ru.beryukhov.reactivenetwork.ReactiveNetwork
25 | import ru.beryukhov.reactivenetwork.network.observing.NetworkObservingStrategy
26 |
27 | /**
28 | * Network observing strategy for devices with Android Marshmallow (API 23) or higher.
29 | * Uses Network Callback API and handles Doze mode.
30 | */
31 |
32 | @TargetApi(23)
33 | public class MarshmallowNetworkObservingStrategy : NetworkObservingStrategy {
34 | // it has to be initialized in the Observable due to Context
35 | private lateinit var networkCallback: NetworkCallback
36 | private val connectivitySubject = MutableSharedFlow()
37 | private val idleReceiver: BroadcastReceiver = createIdleBroadcastReceiver()
38 | private var lastConnectivity = Connectivity()
39 |
40 | override fun observeNetworkConnectivity(context: Context): Flow {
41 | val service = Context.CONNECTIVITY_SERVICE
42 | val manager = context.getSystemService(service) as ConnectivityManager
43 | networkCallback = createNetworkCallback(context)
44 | registerIdleReceiver(context)
45 | val request =
46 | NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
47 | .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
48 | .build()
49 | manager.registerNetworkCallback(request, networkCallback)
50 | return connectivitySubject.flatMapConcat { connectivity ->
51 | propagateAnyConnectedState(lastConnectivity, connectivity)
52 | }.onStart { emit(Connectivity.create(context)) }
53 | .onCompletion {
54 | tryToUnregisterCallback(manager)
55 | tryToUnregisterReceiver(context)
56 | }
57 | .onCompletion {
58 | tryToUnregisterCallback(manager)
59 | tryToUnregisterReceiver(context)
60 | }.distinctUntilChanged()
61 | }
62 |
63 | internal fun propagateAnyConnectedState(
64 | last: Connectivity,
65 | current: Connectivity
66 | ): Flow {
67 | val typeChanged = last.type != current.type
68 | val wasConnected = last.state == NetworkInfo.State.CONNECTED
69 | val isDisconnected = current.state == NetworkInfo.State.DISCONNECTED
70 | val isNotIdle = current.detailedState != NetworkInfo.DetailedState.IDLE
71 | return if (typeChanged && wasConnected && isDisconnected && isNotIdle) {
72 | flowOf(current, last)
73 | } else {
74 | flowOf(current)
75 | }
76 | }
77 |
78 | private fun registerIdleReceiver(context: Context?) {
79 | val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
80 | context!!.registerReceiver(idleReceiver, filter)
81 | }
82 |
83 | internal fun createIdleBroadcastReceiver(): BroadcastReceiver {
84 | return object : BroadcastReceiver() {
85 | override fun onReceive(
86 | context: Context?,
87 | intent: Intent?
88 | ) {
89 | if (isIdleMode(context)) {
90 | onNext(Connectivity())
91 | } else {
92 | onNext(Connectivity.create(context!!))
93 | }
94 | }
95 | }
96 | }
97 |
98 | internal fun isIdleMode(context: Context?): Boolean {
99 | val packageName = context?.packageName
100 | val manager = context?.getSystemService(Context.POWER_SERVICE) as PowerManager
101 | val isIgnoringOptimizations = manager.isIgnoringBatteryOptimizations(packageName)
102 | return manager.isDeviceIdleMode && !isIgnoringOptimizations
103 | }
104 |
105 | internal fun tryToUnregisterCallback(manager: ConnectivityManager?) {
106 | try {
107 | networkCallback?.let { manager?.unregisterNetworkCallback(it) }
108 | } catch (exception: Exception) {
109 | onError(
110 | ERROR_MSG_NETWORK_CALLBACK,
111 | exception
112 | )
113 | }
114 | }
115 |
116 | internal fun tryToUnregisterReceiver(context: Context) {
117 | try {
118 | context.unregisterReceiver(idleReceiver)
119 | } catch (exception: Exception) {
120 | onError(ERROR_MSG_RECEIVER, exception)
121 | }
122 | }
123 |
124 | override fun onError(
125 | message: String,
126 | exception: Exception
127 | ) {
128 | Log.e(ReactiveNetwork.LOG_TAG, message, exception)
129 | }
130 |
131 | internal fun createNetworkCallback(context: Context): NetworkCallback {
132 | return object : NetworkCallback() {
133 | override fun onAvailable(network: Network) {
134 | onNext(Connectivity.create(context))
135 | }
136 |
137 | override fun onLost(network: Network) {
138 | onNext(Connectivity.create(context))
139 | }
140 | }
141 | }
142 |
143 | internal fun onNext(connectivity: Connectivity) {
144 | connectivitySubject?.tryEmit(connectivity)
145 | }
146 |
147 | internal companion object {
148 | internal const val ERROR_MSG_NETWORK_CALLBACK: String =
149 | "could not unregister network callback"
150 | internal const val ERROR_MSG_RECEIVER: String = "could not unregister receiver"
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/internet/observing/strategy/WalledGardenInternetObservingStrategy.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing.strategy
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.distinctUntilChanged
5 | import kotlinx.coroutines.flow.map
6 | import ru.beryukhov.reactivenetwork.Preconditions
7 | import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingStrategy
8 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
9 | import ru.beryukhov.reactivenetwork.tickerFlow
10 | import java.io.IOException
11 | import java.net.HttpURLConnection
12 | import java.net.URL
13 | import javax.net.ssl.HttpsURLConnection
14 |
15 | /**
16 | * Walled Garden Strategy for monitoring connectivity with the Internet.
17 | * This strategy handle use case of the countries behind Great Firewall (e.g. China),
18 | * which does not has access to several websites like Google. It such case, different HTTP responses
19 | * are generated. Instead HTTP 200 (OK), we got HTTP 204 (NO CONTENT), but it still can tell us
20 | * if a device is connected to the Internet or not.
21 | */
22 | public class WalledGardenInternetObservingStrategy : InternetObservingStrategy {
23 | override fun getDefaultPingHost(): String {
24 | return DEFAULT_HOST
25 | }
26 |
27 | override fun observeInternetConnectivity(
28 | initialIntervalInMs: Int,
29 | intervalInMs: Int,
30 | host: String,
31 | port: Int,
32 | timeoutInMs: Int,
33 | httpResponse: Int,
34 | errorHandler: ErrorHandler
35 | ): Flow {
36 | Preconditions.checkGreaterOrEqualToZero(
37 | initialIntervalInMs,
38 | "initialIntervalInMs is not a positive number"
39 | )
40 | Preconditions.checkGreaterThanZero(
41 | intervalInMs,
42 | "intervalInMs is not a positive number"
43 | )
44 | checkGeneralPreconditions(host, port, timeoutInMs, httpResponse, errorHandler)
45 | val adjustedHost = adjustHost(host)
46 | return tickerFlow(
47 | period = intervalInMs.toLong(),
48 | initialDelay = initialIntervalInMs.toLong()
49 | ).map {
50 | isConnected(
51 | adjustedHost,
52 | port,
53 | timeoutInMs,
54 | httpResponse,
55 | errorHandler
56 | )
57 | }.distinctUntilChanged()
58 | }
59 |
60 | override suspend fun checkInternetConnectivity(
61 | host: String,
62 | port: Int,
63 | timeoutInMs: Int,
64 | httpResponse: Int,
65 | errorHandler: ErrorHandler
66 | ): Boolean {
67 | checkGeneralPreconditions(host, port, timeoutInMs, httpResponse, errorHandler)
68 | return isConnected(host, port, timeoutInMs, httpResponse, errorHandler)
69 | }
70 |
71 | internal fun adjustHost(host: String): String {
72 | return if (!host.startsWith(HTTP_PROTOCOL) && !host.startsWith(
73 | HTTPS_PROTOCOL
74 | )
75 | ) {
76 | HTTPS_PROTOCOL + host
77 | } else {
78 | host
79 | }
80 | }
81 |
82 | private fun checkGeneralPreconditions(
83 | host: String,
84 | port: Int,
85 | timeoutInMs: Int,
86 | httpResponse: Int,
87 | errorHandler: ErrorHandler
88 | ) {
89 | Preconditions.checkNotNullOrEmpty(
90 | host,
91 | "host is null or empty"
92 | )
93 | Preconditions.checkGreaterThanZero(
94 | port,
95 | "port is not a positive number"
96 | )
97 | Preconditions.checkGreaterThanZero(
98 | timeoutInMs,
99 | "timeoutInMs is not a positive number"
100 | )
101 | Preconditions.checkNotNull(
102 | errorHandler,
103 | "errorHandler is null"
104 | )
105 | Preconditions.checkNotNull(
106 | httpResponse,
107 | "httpResponse is null"
108 | )
109 | Preconditions.checkGreaterThanZero(
110 | httpResponse,
111 | "httpResponse is not a positive number"
112 | )
113 | }
114 |
115 | internal fun isConnected(
116 | host: String,
117 | port: Int,
118 | timeoutInMs: Int,
119 | httpResponse: Int,
120 | errorHandler: ErrorHandler
121 | ): Boolean {
122 | var urlConnection: HttpURLConnection? = null
123 | return try {
124 | urlConnection = if (host.startsWith(HTTPS_PROTOCOL)) {
125 | createHttpsUrlConnection(host, port, timeoutInMs)
126 | } else {
127 | createHttpUrlConnection(host, port, timeoutInMs)
128 | }
129 | urlConnection.responseCode == httpResponse
130 | } catch (e: IOException) {
131 | errorHandler.handleError(e, "Could not establish connection with WalledGardenStrategy")
132 | java.lang.Boolean.FALSE
133 | } finally {
134 | urlConnection?.disconnect()
135 | }
136 | }
137 |
138 | @Throws(IOException::class)
139 | internal fun createHttpUrlConnection(
140 | host: String?,
141 | port: Int,
142 | timeoutInMs: Int
143 | ): HttpURLConnection {
144 | val initialUrl = URL(host)
145 | val url = URL(initialUrl.protocol, initialUrl.host, port, initialUrl.file)
146 | val urlConnection = url.openConnection() as HttpURLConnection
147 | urlConnection.connectTimeout = timeoutInMs
148 | urlConnection.readTimeout = timeoutInMs
149 | urlConnection.instanceFollowRedirects = false
150 | urlConnection.useCaches = false
151 | return urlConnection
152 | }
153 |
154 | @Throws(IOException::class)
155 | internal fun createHttpsUrlConnection(
156 | host: String?,
157 | port: Int,
158 | timeoutInMs: Int
159 | ): HttpsURLConnection {
160 | val initialUrl = URL(host)
161 | val url =
162 | URL(initialUrl.protocol, initialUrl.host, port, initialUrl.file)
163 | val urlConnection =
164 | url.openConnection() as HttpsURLConnection
165 | urlConnection.connectTimeout = timeoutInMs
166 | urlConnection.readTimeout = timeoutInMs
167 | urlConnection.instanceFollowRedirects = false
168 | urlConnection.useCaches = false
169 | return urlConnection
170 | }
171 |
172 | private companion object {
173 | private const val DEFAULT_HOST = "http://clients3.google.com/generate_204"
174 | private const val HTTP_PROTOCOL = "http://"
175 | private const val HTTPS_PROTOCOL = "https://"
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/ReactiveNetworkTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | import android.content.Context
4 | import android.net.NetworkInfo
5 | import androidx.test.core.app.ApplicationProvider
6 | import app.cash.turbine.test
7 | import com.google.common.truth.Truth.assertThat
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.test.runTest
12 | import org.junit.Test
13 | import org.junit.runner.RunWith
14 | import org.robolectric.RobolectricTestRunner
15 | import org.robolectric.annotation.Config
16 | import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingSettings.Companion.builder
17 | import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingStrategy
18 | import ru.beryukhov.reactivenetwork.internet.observing.error.DefaultErrorHandler
19 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
20 | import ru.beryukhov.reactivenetwork.internet.observing.strategy.SocketInternetObservingStrategy
21 | import ru.beryukhov.reactivenetwork.network.observing.NetworkObservingStrategy
22 | import ru.beryukhov.reactivenetwork.network.observing.strategy.LollipopNetworkObservingStrategy
23 |
24 | @RunWith(RobolectricTestRunner::class)
25 | class ReactiveNetworkTest {
26 | @Test
27 | fun testReactiveNetworkObjectShouldNotBeNull() {
28 | // given
29 | // when
30 | val reactiveNetwork = ReactiveNetwork()
31 | // then
32 | assertThat(reactiveNetwork).isNotNull()
33 | }
34 |
35 | @Test
36 | fun observeNetworkConnectivityShouldNotBeNull() {
37 | // given
38 | networkConnectivityObservableShouldNotBeNull()
39 | }
40 |
41 | @Test
42 | @Config(sdk = [23])
43 | fun observeNetworkConnectivityShouldNotBeNullForMarshmallow() {
44 | // given
45 | networkConnectivityObservableShouldNotBeNull()
46 | }
47 |
48 | @Test
49 | @Config(sdk = [21])
50 | fun observeNetworkConnectivityShouldNotBeNullForLollipop() {
51 | networkConnectivityObservableShouldNotBeNull()
52 | }
53 |
54 | private fun networkConnectivityObservableShouldNotBeNull() {
55 | // given
56 | val context: Context = ApplicationProvider.getApplicationContext()
57 | // when
58 | val observable = ReactiveNetwork().observeNetworkConnectivity(context)
59 | // then
60 | assertThat(observable).isNotNull()
61 | }
62 |
63 | @Test
64 | fun observeNetworkConnectivityWithStrategyShouldNotBeNull() {
65 | // given
66 | val context: Context = ApplicationProvider.getApplicationContext()
67 | val strategy: NetworkObservingStrategy = LollipopNetworkObservingStrategy()
68 | // when
69 | val observable = ReactiveNetwork().observeNetworkConnectivity(context, strategy)
70 | // then
71 | assertThat(observable).isNotNull()
72 | }
73 |
74 | @Test
75 | fun observeInternetConnectivityDefaultShouldNotBeNull() {
76 | // given
77 | // when
78 | val observable = ReactiveNetwork().observeInternetConnectivity()
79 | // then
80 | assertThat(observable).isNotNull()
81 | }
82 |
83 | @Test
84 | fun observeNetworkConnectivityShouldBeConnectedOnStartWhenNetworkIsAvailable() = runTest {
85 | // given
86 | val context = ApplicationProvider.getApplicationContext()
87 | // when
88 | val connectivityFlow =
89 | ReactiveNetwork().observeNetworkConnectivity(context).map { it.state }
90 | // then
91 | connectivityFlow.test {
92 | assertThat(awaitItem()).isEqualTo(NetworkInfo.State.CONNECTED)
93 | }
94 | }
95 |
96 | @Test
97 | fun observeInternetConnectivityShouldNotThrowAnExceptionWhenStrategyIsNotNull() {
98 | // given
99 | val strategy: InternetObservingStrategy = SocketInternetObservingStrategy()
100 | val errorHandler: ErrorHandler =
101 | DefaultErrorHandler()
102 | // when
103 | val observable = ReactiveNetwork().observeInternetConnectivity(
104 | strategy,
105 | TEST_VALID_INITIAL_INTERVAL,
106 | TEST_VALID_INTERVAL,
107 | TEST_VALID_HOST,
108 | TEST_VALID_PORT,
109 | TEST_VALID_TIMEOUT,
110 | TEST_VALID_HTTP_RESPONSE,
111 | errorHandler
112 | )
113 | // then
114 | assertThat(observable).isNotNull()
115 | }
116 |
117 | @Test
118 | fun shouldObserveInternetConnectivityWithCustomSettings() {
119 | // given
120 | val initialInterval = 1
121 | val interval = 2
122 | val host = "www.test.com"
123 | val port = 90
124 | val timeout = 3
125 | val testErrorHandler =
126 | createTestErrorHandler()
127 | val strategy = createTestInternetObservingStrategy()
128 | // when
129 | val settings = builder()
130 | .initialInterval(initialInterval)
131 | .interval(interval)
132 | .host(host)
133 | .port(port)
134 | .timeout(timeout)
135 | .errorHandler(testErrorHandler)
136 | .strategy(strategy)
137 | .build()
138 | // then
139 | val observable =
140 | ReactiveNetwork().observeInternetConnectivity(settings)
141 | assertThat(observable).isNotNull()
142 | }
143 |
144 | private fun createTestInternetObservingStrategy(): InternetObservingStrategy {
145 | return object : InternetObservingStrategy {
146 | override fun observeInternetConnectivity(
147 | initialIntervalInMs: Int,
148 | intervalInMs: Int,
149 | host: String,
150 | port: Int,
151 | timeoutInMs: Int,
152 | httpResponse: Int,
153 | errorHandler: ErrorHandler
154 | ): Flow {
155 | return flow {}
156 | }
157 |
158 | override suspend fun checkInternetConnectivity(
159 | host: String,
160 | port: Int,
161 | timeoutInMs: Int,
162 | httpResponse: Int,
163 | errorHandler: ErrorHandler
164 | ): Boolean {
165 | return true
166 | }
167 |
168 | override fun getDefaultPingHost(): String {
169 | return "null"
170 | }
171 | }
172 | }
173 |
174 | private fun createTestErrorHandler(): ErrorHandler {
175 | return object : ErrorHandler {
176 | override fun handleError(
177 | exception: Exception?,
178 | message: String?
179 | ) {
180 | }
181 | }
182 | }
183 |
184 | companion object {
185 | private const val TEST_VALID_HOST = "www.test.com"
186 | private const val TEST_VALID_PORT = 80
187 | private const val TEST_VALID_TIMEOUT = 1000
188 | private const val TEST_VALID_INTERVAL = 1000
189 | private const val TEST_VALID_INITIAL_INTERVAL = 1000
190 | private const val TEST_VALID_HTTP_RESPONSE = 204
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/internet/observing/strategy/SocketInternetObservingStrategyTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing.strategy
2 |
3 | import app.cash.turbine.test
4 | import com.google.common.truth.Truth.assertThat
5 | import io.mockk.every
6 | import io.mockk.mockk
7 | import io.mockk.spyk
8 | import io.mockk.verify
9 | import kotlinx.coroutines.test.runTest
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.robolectric.RobolectricTestRunner
13 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
14 | import java.io.IOException
15 | import java.net.InetSocketAddress
16 | import java.net.Socket
17 |
18 | @RunWith(RobolectricTestRunner::class)
19 | class SocketInternetObservingStrategyTest {
20 |
21 | private val strategy = spyk(SocketInternetObservingStrategy())
22 | private val errorHandler = mockk(relaxed = true)
23 | private val socket = mockk(relaxed = true)
24 |
25 | private val host: String = strategy.getDefaultPingHost()
26 |
27 | @Test
28 | fun shouldBeConnectedToTheInternet() = runTest {
29 | // given
30 | every {
31 | strategy.isConnected(
32 | host = host,
33 | port = PORT,
34 | timeoutInMs = TIMEOUT_IN_MS,
35 | errorHandler = errorHandler
36 | )
37 | } returns true
38 |
39 | // when
40 |
41 | strategy.observeInternetConnectivity(
42 | initialIntervalInMs = INITIAL_INTERVAL_IN_MS,
43 | intervalInMs = INTERVAL_IN_MS,
44 | host = host,
45 | port = PORT,
46 | timeoutInMs = TIMEOUT_IN_MS,
47 | httpResponse = HTTP_RESPONSE,
48 | errorHandler = errorHandler
49 | ).test {
50 | // then
51 | assertThat(awaitItem()).isEqualTo(true)
52 | }
53 | }
54 |
55 | @Test
56 | fun shouldNotBeConnectedToTheInternet() = runTest {
57 | // given
58 | every {
59 | strategy.isConnected(
60 | host = host,
61 | port = PORT,
62 | timeoutInMs = TIMEOUT_IN_MS,
63 | errorHandler = errorHandler
64 | )
65 | } returns false
66 | // when
67 |
68 | strategy.observeInternetConnectivity(
69 | initialIntervalInMs = INITIAL_INTERVAL_IN_MS,
70 | intervalInMs = INTERVAL_IN_MS,
71 | host = host,
72 | port = PORT,
73 | timeoutInMs = TIMEOUT_IN_MS,
74 | httpResponse = HTTP_RESPONSE,
75 | errorHandler = errorHandler
76 | ).test {
77 | // then
78 | assertThat(awaitItem()).isEqualTo(false)
79 | }
80 | }
81 |
82 | @Test
83 | @Throws(IOException::class)
84 | fun shouldNotBeConnectedToTheInternetWhenSocketThrowsAnExceptionOnConnect() {
85 | // given
86 | val address = InetSocketAddress(
87 | host,
88 | PORT
89 | )
90 | every { socket.connect(address, TIMEOUT_IN_MS) } throws IOException()
91 |
92 | // when
93 | val isConnected = strategy.isConnected(
94 | socket = socket,
95 | host = host,
96 | port = PORT,
97 | timeoutInMs = TIMEOUT_IN_MS,
98 | errorHandler = errorHandler
99 | )
100 | // then
101 | assertThat(isConnected).isFalse()
102 | }
103 |
104 | @Test
105 | @Throws(IOException::class)
106 | fun shouldHandleAnExceptionThrownDuringClosingTheSocket() {
107 | // given
108 | val errorMsg = "Could not close the socket"
109 | val givenException = IOException(errorMsg)
110 | every { socket.close() } throws givenException
111 |
112 | // when
113 | strategy.isConnected(
114 | socket = socket,
115 | host = host,
116 | port = PORT,
117 | timeoutInMs = TIMEOUT_IN_MS,
118 | errorHandler = errorHandler
119 | )
120 | // then
121 | verify(exactly = 1) { errorHandler.handleError(givenException, errorMsg) }
122 | }
123 |
124 | @Test
125 | fun shouldBeConnectedToTheInternetViaSingle() = runTest {
126 | // given
127 | every {
128 | strategy.isConnected(
129 | host = host,
130 | port = PORT,
131 | timeoutInMs = TIMEOUT_IN_MS,
132 | errorHandler = errorHandler
133 | )
134 | } returns true
135 | // when
136 | val isConnected = strategy.checkInternetConnectivity(
137 | host = host,
138 | port = PORT,
139 | timeoutInMs = TIMEOUT_IN_MS,
140 | httpResponse = HTTP_RESPONSE,
141 | errorHandler = errorHandler
142 | )
143 | // then
144 | assertThat(isConnected).isTrue()
145 | }
146 |
147 | @Test
148 | fun shouldNotBeConnectedToTheInternetViaSingle() = runTest {
149 | // given
150 | every {
151 | strategy.isConnected(
152 | host = host,
153 | port = PORT,
154 | timeoutInMs = TIMEOUT_IN_MS,
155 | errorHandler = errorHandler
156 | )
157 | } returns false
158 | // when
159 | val isConnected = strategy.checkInternetConnectivity(
160 | host = host,
161 | port = PORT,
162 | timeoutInMs = TIMEOUT_IN_MS,
163 | httpResponse = HTTP_RESPONSE,
164 | errorHandler = errorHandler
165 | )
166 | // then
167 | assertThat(isConnected).isFalse()
168 | }
169 |
170 | @Test
171 | fun shouldNotTransformHost() { // when
172 | val transformedHost =
173 | strategy.adjustHost(HOST_WITHOUT_HTTP)
174 | // then
175 | assertThat(transformedHost)
176 | .isEqualTo(HOST_WITHOUT_HTTP)
177 | }
178 |
179 | @Test
180 | fun shouldRemoveHttpProtocolFromHost() { // when
181 | val transformedHost =
182 | strategy.adjustHost(HOST_WITH_HTTP)
183 | // then
184 | assertThat(transformedHost)
185 | .isEqualTo(HOST_WITHOUT_HTTP)
186 | }
187 |
188 | @Test
189 | fun shouldRemoveHttpsProtocolFromHost() { // when
190 | val transformedHost =
191 | strategy.adjustHost(HOST_WITH_HTTP)
192 | // then
193 | assertThat(transformedHost)
194 | .isEqualTo(HOST_WITHOUT_HTTP)
195 | }
196 |
197 | @Test
198 | fun shouldAdjustHostDuringCheckingConnectivity() = runTest {
199 | // given
200 | val host = host
201 | every {
202 | strategy.isConnected(
203 | host = host,
204 | port = PORT,
205 | timeoutInMs = TIMEOUT_IN_MS,
206 | errorHandler = errorHandler
207 | )
208 | } returns true
209 |
210 | // when
211 |
212 | strategy.observeInternetConnectivity(
213 | initialIntervalInMs = INITIAL_INTERVAL_IN_MS,
214 | intervalInMs = INTERVAL_IN_MS,
215 | host = host,
216 | port = PORT,
217 | timeoutInMs = TIMEOUT_IN_MS,
218 | httpResponse = HTTP_RESPONSE,
219 | errorHandler = errorHandler
220 | ).test {
221 | cancelAndConsumeRemainingEvents()
222 | }
223 | // then
224 | verify { strategy.adjustHost(host) }
225 | }
226 |
227 | companion object {
228 | private const val INITIAL_INTERVAL_IN_MS = 0
229 | private const val INTERVAL_IN_MS = 2000
230 | private const val PORT = 80
231 | private const val TIMEOUT_IN_MS = 30
232 | private const val HTTP_RESPONSE = 204
233 | private const val HOST_WITH_HTTP = "http://www.website.com"
234 | private const val HOST_WITHOUT_HTTP = "www.website.com"
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/main/kotlin/ru/beryukhov/reactivenetwork/ReactiveNetwork.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import androidx.annotation.RequiresPermission
6 | import kotlinx.coroutines.flow.Flow
7 | import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingSettings
8 | import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingStrategy
9 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
10 | import ru.beryukhov.reactivenetwork.network.observing.NetworkObservingStrategy
11 | import ru.beryukhov.reactivenetwork.network.observing.strategy.LollipopNetworkObservingStrategy
12 | import ru.beryukhov.reactivenetwork.network.observing.strategy.MarshmallowNetworkObservingStrategy
13 | import ru.beryukhov.reactivenetwork.network.observing.strategy.PreLollipopNetworkObservingStrategy
14 |
15 | /**
16 | * ReactiveNetwork is an Android library
17 | * listening network connection state and change of the WiFi signal strength
18 | * with Coroutines Flow. It was backported from ReactiveNetwork with Java and RxJava inside.
19 | */
20 | public class ReactiveNetwork {
21 | /**
22 | * Observes network connectivity. Information about network state, type and typeName are contained
23 | * in
24 | * observed Connectivity object.
25 | *
26 | * @param context Context of the activity or an application
27 | * @return Flow with Connectivity class containing information about network state,
28 | * type and typeName
29 | */
30 | @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
31 | public fun observeNetworkConnectivity(context: Context): Flow {
32 | val strategy: NetworkObservingStrategy = when {
33 | Preconditions.isAtLeastAndroidMarshmallow() -> {
34 | MarshmallowNetworkObservingStrategy()
35 | }
36 |
37 | Preconditions.isAtLeastAndroidLollipop() -> {
38 | LollipopNetworkObservingStrategy()
39 | }
40 |
41 | else -> {
42 | PreLollipopNetworkObservingStrategy()
43 | }
44 | }
45 | return observeNetworkConnectivity(context, strategy)
46 | }
47 |
48 | /**
49 | * Observes network connectivity. Information about network state, type and typeName are contained
50 | * in observed Connectivity object. Moreover, allows you to define NetworkObservingStrategy.
51 | *
52 | * @param context Context of the activity or an application
53 | * @param strategy NetworkObserving strategy to be applied - you can use one of the existing
54 | * strategies [PreLollipopNetworkObservingStrategy],
55 | * [LollipopNetworkObservingStrategy] or create your own custom strategy
56 | * @return Flow with Connectivity class containing information about network state,
57 | * type and typeName
58 | */
59 | @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
60 | public fun observeNetworkConnectivity(
61 | context: Context,
62 | strategy: NetworkObservingStrategy
63 | ): Flow {
64 | Preconditions.checkNotNull(context, "context == null")
65 | Preconditions.checkNotNull(strategy, "strategy == null")
66 | return strategy.observeNetworkConnectivity(context)
67 | }
68 |
69 | /**
70 | * Observes connectivity with the Internet with default settings. It pings remote host
71 | * (www.google.com) at port 80 every 2 seconds with 2 seconds of timeout. This operation is used
72 | * for determining if device is connected to the Internet or not. Please note that this method is
73 | * less efficient than [.observeNetworkConnectivity] method and consumes data
74 | * transfer, but it gives you actual information if device is connected to the Internet or not.
75 | *
76 | * @return Flow with Boolean - true, when we have an access to the Internet
77 | * and false if not
78 | */
79 | @RequiresPermission(Manifest.permission.INTERNET)
80 | public fun observeInternetConnectivity(): Flow {
81 | val settings = InternetObservingSettings.create()
82 | return observeInternetConnectivity(
83 | settings.strategy(), settings.initialInterval(),
84 | settings.interval(), settings.host(), settings.port(),
85 | settings.timeout(), settings.httpResponse(), settings.errorHandler()
86 | )
87 | }
88 |
89 | /**
90 | * Observes connectivity with the Internet in a given time interval.
91 | *
92 | * @param settings Internet Observing Settings created via Builder pattern
93 | * @return Flow with Boolean - true, when we have connection with host and false if
94 | * not
95 | */
96 | @RequiresPermission(Manifest.permission.INTERNET)
97 | public fun observeInternetConnectivity(
98 | settings: InternetObservingSettings
99 | ): Flow {
100 | return observeInternetConnectivity(
101 | settings.strategy(), settings.initialInterval(),
102 | settings.interval(), settings.host(), settings.port(),
103 | settings.timeout(), settings.httpResponse(), settings.errorHandler()
104 | )
105 | }
106 |
107 | /**
108 | * Observes connectivity with the Internet in a given time interval.
109 | *
110 | * @param strategy for observing Internet connectivity
111 | * @param initialIntervalInMs in milliseconds determining the delay of the first connectivity
112 | * check
113 | * @param intervalInMs in milliseconds determining how often we want to check connectivity
114 | * @param host for checking Internet connectivity
115 | * @param port for checking Internet connectivity
116 | * @param timeoutInMs for pinging remote host in milliseconds
117 | * @param httpResponse expected HTTP response code indicating that connection is established
118 | * @param errorHandler for handling errors during connectivity check
119 | * @return Flow with Boolean - true, when we have connection with host and false if
120 | * not
121 | */
122 | @RequiresPermission(Manifest.permission.INTERNET)
123 | internal fun observeInternetConnectivity(
124 | strategy: InternetObservingStrategy,
125 | initialIntervalInMs: Int,
126 | intervalInMs: Int,
127 | host: String,
128 | port: Int,
129 | timeoutInMs: Int,
130 | httpResponse: Int,
131 | errorHandler: ErrorHandler
132 | ): Flow {
133 | checkStrategyIsNotNull(strategy)
134 | return strategy.observeInternetConnectivity(
135 | initialIntervalInMs, intervalInMs, host, port,
136 | timeoutInMs, httpResponse, errorHandler
137 | )
138 | }
139 |
140 | /**
141 | * Checks connectivity with the Internet. This operation is performed only once.
142 | *
143 | * @return Boolean - true, when we have an access to the Internet
144 | * and false if not
145 | */
146 | @RequiresPermission(Manifest.permission.INTERNET)
147 | public suspend fun checkInternetConnectivity(): Boolean {
148 | val settings = InternetObservingSettings.create()
149 | return checkInternetConnectivity(
150 | settings.strategy(), settings.host(), settings.port(),
151 | settings.timeout(), settings.httpResponse(), settings.errorHandler()
152 | )
153 | }
154 |
155 | /**
156 | * Checks connectivity with the Internet. This operation is performed only once.
157 | *
158 | * @param settings Internet Observing Settings created via Builder pattern
159 | * @return Boolean - true, when we have connection with host and false if
160 | * not
161 | */
162 | @RequiresPermission(Manifest.permission.INTERNET)
163 | public suspend fun checkInternetConnectivity(settings: InternetObservingSettings): Boolean {
164 | return checkInternetConnectivity(
165 | settings.strategy(), settings.host(), settings.port(),
166 | settings.timeout(), settings.httpResponse(), settings.errorHandler()
167 | )
168 | }
169 |
170 | /**
171 | * Checks connectivity with the Internet. This operation is performed only once.
172 | *
173 | * @param strategy for observing Internet connectivity
174 | * @param host for checking Internet connectivity
175 | * @param port for checking Internet connectivity
176 | * @param timeoutInMs for pinging remote host in milliseconds
177 | * @param httpResponse expected HTTP response code indicating that connection is established
178 | * @param errorHandler for handling errors during connectivity check
179 | * @return Boolean - true, when we have connection with host and false if
180 | * not
181 | */
182 | @RequiresPermission(Manifest.permission.INTERNET)
183 | internal suspend fun checkInternetConnectivity(
184 | strategy: InternetObservingStrategy,
185 | host: String,
186 | port: Int,
187 | timeoutInMs: Int,
188 | httpResponse: Int,
189 | errorHandler: ErrorHandler
190 | ): Boolean {
191 | checkStrategyIsNotNull(strategy)
192 | return strategy.checkInternetConnectivity(
193 | host,
194 | port,
195 | timeoutInMs,
196 | httpResponse,
197 | errorHandler
198 | )
199 | }
200 |
201 | private fun checkStrategyIsNotNull(strategy: InternetObservingStrategy) {
202 | Preconditions.checkNotNull(strategy, "strategy == null")
203 | }
204 |
205 | public companion object {
206 | public const val LOG_TAG: String = "ReactiveNetwork"
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/internet/observing/strategy/WalledGardenInternetObservingStrategyTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.internet.observing.strategy
2 |
3 | import app.cash.turbine.test
4 | import com.google.common.truth.Truth.assertThat
5 | import io.mockk.every
6 | import io.mockk.mockk
7 | import io.mockk.spyk
8 | import io.mockk.verify
9 | import kotlinx.coroutines.test.runTest
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.robolectric.RobolectricTestRunner
13 | import ru.beryukhov.reactivenetwork.internet.observing.error.ErrorHandler
14 | import java.io.IOException
15 | import java.net.HttpURLConnection
16 |
17 | @RunWith(RobolectricTestRunner::class)
18 | class WalledGardenInternetObservingStrategyTest {
19 |
20 | private val errorHandler = mockk(relaxed = true)
21 | private val strategy = spyk(WalledGardenInternetObservingStrategy())
22 |
23 | private val host: String = strategy.getDefaultPingHost()
24 |
25 | @Test
26 | fun shouldBeConnectedToTheInternet() = runTest {
27 | // given
28 | val errorHandlerStub = createErrorHandlerStub()
29 | every {
30 | strategy.isConnected(
31 | host,
32 | PORT,
33 | TIMEOUT_IN_MS,
34 | HTTP_RESPONSE,
35 | errorHandlerStub
36 | )
37 | } returns true
38 |
39 | // when
40 | strategy.observeInternetConnectivity(
41 | INITIAL_INTERVAL_IN_MS,
42 | INTERVAL_IN_MS,
43 | host,
44 | PORT,
45 | TIMEOUT_IN_MS,
46 | HTTP_RESPONSE,
47 | errorHandlerStub
48 | ).test {
49 | // then
50 | assertThat(awaitItem()).isEqualTo(true)
51 | }
52 | }
53 |
54 | @Test
55 | fun shouldNotBeConnectedToTheInternet() = runTest {
56 | // given
57 | val errorHandlerStub =
58 | createErrorHandlerStub()
59 | every {
60 | strategy.isConnected(
61 | host,
62 | PORT,
63 | TIMEOUT_IN_MS,
64 | HTTP_RESPONSE,
65 | errorHandlerStub
66 | )
67 | } returns false
68 | // when
69 | strategy.observeInternetConnectivity(
70 | INITIAL_INTERVAL_IN_MS,
71 | INTERVAL_IN_MS,
72 | host,
73 | PORT,
74 | TIMEOUT_IN_MS,
75 | HTTP_RESPONSE,
76 | errorHandlerStub
77 | ).test {
78 |
79 | // then
80 | assertThat(awaitItem()).isEqualTo(false)
81 | }
82 | }
83 |
84 | @Test
85 | fun shouldBeConnectedToTheInternetViaSingle() = runTest {
86 | // given
87 | val errorHandlerStub = createErrorHandlerStub()
88 | every {
89 | strategy.isConnected(
90 | host,
91 | PORT,
92 | TIMEOUT_IN_MS,
93 | HTTP_RESPONSE,
94 | errorHandlerStub
95 | )
96 | } returns true
97 |
98 | // when
99 | val isConnected = strategy.checkInternetConnectivity(
100 | host,
101 | PORT,
102 | TIMEOUT_IN_MS,
103 | HTTP_RESPONSE,
104 | errorHandlerStub
105 | )
106 | // then
107 | assertThat(isConnected).isTrue()
108 | }
109 |
110 | @Test
111 | fun shouldNotBeConnectedToTheInternetViaSingle() = runTest {
112 | // given
113 | val errorHandlerStub =
114 | createErrorHandlerStub()
115 | every {
116 | strategy.isConnected(
117 | host,
118 | PORT,
119 | TIMEOUT_IN_MS,
120 | HTTP_RESPONSE,
121 | errorHandlerStub
122 | )
123 | } returns false
124 | // when
125 | val isConnected = strategy.checkInternetConnectivity(
126 | host,
127 | PORT,
128 | TIMEOUT_IN_MS,
129 | HTTP_RESPONSE,
130 | errorHandlerStub
131 | )
132 | // then
133 | assertThat(isConnected).isFalse()
134 | }
135 |
136 | @Test
137 | @Throws(IOException::class)
138 | fun shouldCreateHttpUrlConnection() {
139 | // given
140 | val parsedDefaultHost = "clients3.google.com"
141 | // when
142 | val connection = strategy.createHttpUrlConnection(
143 | host,
144 | PORT,
145 | TIMEOUT_IN_MS
146 | )
147 | // then
148 | assertThat(connection).isNotNull()
149 | assertThat(connection.url.host).isEqualTo(parsedDefaultHost)
150 | assertThat(connection.url.port).isEqualTo(PORT)
151 | assertThat(connection.connectTimeout).isEqualTo(TIMEOUT_IN_MS)
152 | assertThat(connection.readTimeout).isEqualTo(TIMEOUT_IN_MS)
153 | assertThat(connection.instanceFollowRedirects).isFalse()
154 | assertThat(connection.useCaches).isFalse()
155 | }
156 |
157 | @Test
158 | @Throws(IOException::class)
159 | fun shouldHandleAnExceptionWhileCreatingHttpUrlConnection() {
160 | // given
161 | val errorMsg = "Could not establish connection with WalledGardenStrategy"
162 | val givenException = IOException(errorMsg)
163 | every {
164 | strategy.createHttpUrlConnection(
165 | HOST_WITH_HTTP,
166 | PORT,
167 | TIMEOUT_IN_MS
168 | )
169 | } throws givenException
170 | // when
171 | strategy.isConnected(
172 | HOST_WITH_HTTP,
173 | PORT,
174 | TIMEOUT_IN_MS,
175 | HTTP_RESPONSE,
176 | errorHandler
177 | )
178 | // then
179 | verify { errorHandler.handleError(givenException, errorMsg) }
180 | }
181 |
182 | @Test
183 | @Throws(IOException::class)
184 | fun shouldCreateHttpsUrlConnection() {
185 | // given
186 | val parsedDefaultHost = "clients3.google.com"
187 | // when
188 | val connection: HttpURLConnection = strategy.createHttpsUrlConnection(
189 | "https://clients3.google.com",
190 | PORT,
191 | TIMEOUT_IN_MS
192 | )
193 | // then
194 | assertThat(connection).isNotNull()
195 | assertThat(connection.url.host).isEqualTo(parsedDefaultHost)
196 | assertThat(connection.url.port).isEqualTo(PORT)
197 | assertThat(connection.connectTimeout).isEqualTo(TIMEOUT_IN_MS)
198 | assertThat(connection.readTimeout).isEqualTo(TIMEOUT_IN_MS)
199 | assertThat(connection.instanceFollowRedirects).isFalse()
200 | assertThat(connection.useCaches).isFalse()
201 | }
202 |
203 | @Test
204 | @Throws(IOException::class)
205 | fun shouldHandleAnExceptionWhileCreatingHttpsUrlConnection() {
206 | // given
207 | val errorMsg = "Could not establish connection with WalledGardenStrategy"
208 | val givenException = IOException(errorMsg)
209 | val host = "https://clients3.google.com"
210 | every {
211 | strategy.createHttpsUrlConnection(
212 | host,
213 | PORT,
214 | TIMEOUT_IN_MS
215 | )
216 | } throws givenException
217 | // when
218 | strategy.isConnected(
219 | host,
220 | PORT,
221 | TIMEOUT_IN_MS,
222 | HTTP_RESPONSE,
223 | errorHandler
224 | )
225 | // then
226 | verify { errorHandler.handleError(givenException, errorMsg) }
227 | }
228 |
229 | @Test
230 | fun shouldNotTransformHttpHost() { // when
231 | val transformedHost = strategy.adjustHost(HOST_WITH_HTTPS)
232 | // then
233 | assertThat(transformedHost).isEqualTo(HOST_WITH_HTTPS)
234 | }
235 |
236 | @Test
237 | fun shouldNotTransformHttpsHost() { // when
238 | val transformedHost = strategy.adjustHost(HOST_WITH_HTTPS)
239 | // then
240 | assertThat(transformedHost)
241 | .isEqualTo(HOST_WITH_HTTPS)
242 | }
243 |
244 | @Test
245 | fun shouldAddHttpsProtocolToHost() { // when
246 | val transformedHost = strategy.adjustHost(HOST_WITHOUT_HTTPS)
247 | // then
248 | assertThat(transformedHost).isEqualTo(HOST_WITH_HTTPS)
249 | }
250 |
251 | @Test
252 | fun shouldAdjustHostWhileCheckingConnectivity() = runTest {
253 | // given
254 | val errorHandlerStub =
255 | createErrorHandlerStub()
256 | val host = host
257 | every {
258 | strategy.isConnected(
259 | host,
260 | PORT,
261 | TIMEOUT_IN_MS,
262 | HTTP_RESPONSE,
263 | errorHandlerStub
264 | )
265 | } returns true
266 |
267 | // when
268 |
269 | strategy.observeInternetConnectivity(
270 | INITIAL_INTERVAL_IN_MS,
271 | INTERVAL_IN_MS,
272 | host,
273 | PORT,
274 | TIMEOUT_IN_MS,
275 | HTTP_RESPONSE,
276 | errorHandlerStub
277 | ).test {
278 | cancelAndConsumeRemainingEvents()
279 | }
280 | // then
281 | verify { strategy.adjustHost(host) }
282 | }
283 |
284 | private fun createErrorHandlerStub(): ErrorHandler {
285 | return object : ErrorHandler {
286 | override fun handleError(
287 | exception: Exception?,
288 | message: String?
289 | ) {
290 | }
291 | }
292 | }
293 |
294 | companion object {
295 | private const val INITIAL_INTERVAL_IN_MS = 0
296 | private const val INTERVAL_IN_MS = 2000
297 | private const val PORT = 80
298 | private const val TIMEOUT_IN_MS = 30
299 | private const val HTTP_RESPONSE = 204
300 | private const val HOST_WITH_HTTP = "http://www.website.com"
301 | private const val HOST_WITH_HTTPS = "https://www.website.com"
302 | private const val HOST_WITHOUT_HTTPS = "www.website.com"
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/ConnectivityTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkInfo
6 | import android.net.NetworkInfo.DetailedState
7 | import androidx.test.core.app.ApplicationProvider
8 | import com.google.common.truth.Truth.assertThat
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.robolectric.RobolectricTestRunner
12 | import ru.beryukhov.reactivenetwork.Connectivity.Companion.create
13 | import ru.beryukhov.reactivenetwork.ConnectivityPredicate.appendUnknownNetworkTypeToTypes
14 | import ru.beryukhov.reactivenetwork.ConnectivityPredicate.hasState
15 | import ru.beryukhov.reactivenetwork.ConnectivityPredicate.hasType
16 |
17 | @RunWith(RobolectricTestRunner::class)
18 | class ConnectivityTest {
19 | @Test
20 | fun shouldCreateConnectivity() { // when
21 | val connectivity = Connectivity()
22 | // then
23 | assertThat(connectivity).isNotNull()
24 | assertThat(connectivity.state)
25 | .isEqualTo(NetworkInfo.State.DISCONNECTED)
26 | assertThat(connectivity.detailedState)
27 | .isEqualTo(DetailedState.IDLE)
28 | assertThat(connectivity.type).isEqualTo(Connectivity.UNKNOWN_TYPE)
29 | assertThat(connectivity.subType).isEqualTo(Connectivity.UNKNOWN_SUB_TYPE)
30 | assertThat(connectivity.available).isFalse()
31 | assertThat(connectivity.failover).isFalse()
32 | assertThat(connectivity.roaming).isFalse()
33 | assertThat(connectivity.typeName)
34 | .isEqualTo(TYPE_NAME_NONE)
35 | assertThat(connectivity.subTypeName)
36 | .isEqualTo(TYPE_NAME_NONE)
37 | assertThat(connectivity.reason).isEmpty()
38 | assertThat(connectivity.extraInfo).isEmpty()
39 | }
40 |
41 | @Test
42 | @Throws(Exception::class)
43 | fun stateShouldBeEqualToGivenValue() {
44 | // given
45 | val connectivity = Connectivity(
46 | state = NetworkInfo.State.CONNECTED,
47 | type = ConnectivityManager.TYPE_WIFI,
48 | typeName = TYPE_NAME_WIFI
49 | )
50 |
51 | // when
52 | val equalTo =
53 | hasState(connectivity.state)
54 | val shouldBeEqualToGivenStatus = equalTo.test(connectivity)
55 | // then
56 | assertThat(shouldBeEqualToGivenStatus).isTrue()
57 | }
58 |
59 | @Test
60 | @Throws(Exception::class)
61 | fun stateShouldBeEqualToOneOfGivenMultipleValues() {
62 | // given
63 | val connectivity = Connectivity(
64 | state = NetworkInfo.State.CONNECTING,
65 | type = ConnectivityManager.TYPE_WIFI,
66 | typeName = TYPE_NAME_WIFI
67 | )
68 |
69 | val states = arrayOf(NetworkInfo.State.CONNECTED, NetworkInfo.State.CONNECTING)
70 | // when
71 | val equalTo = hasState(*states)
72 | val shouldBeEqualToGivenStatus = equalTo.test(connectivity)
73 | // then
74 | assertThat(shouldBeEqualToGivenStatus).isTrue()
75 | }
76 |
77 | @Test
78 | @Throws(Exception::class)
79 | fun stateShouldNotBeEqualToGivenValue() {
80 | // given
81 | val connectivity = Connectivity(
82 | state = NetworkInfo.State.DISCONNECTED,
83 | type = ConnectivityManager.TYPE_WIFI,
84 | typeName = TYPE_NAME_WIFI
85 | )
86 |
87 | // when
88 | val equalTo = hasState(NetworkInfo.State.CONNECTED)
89 | val shouldBeEqualToGivenStatus = equalTo.test(connectivity)
90 | // then
91 | assertThat(shouldBeEqualToGivenStatus).isFalse()
92 | }
93 |
94 | @Test
95 | @Throws(Exception::class)
96 | fun typeShouldBeEqualToGivenValue() {
97 | // given
98 | val connectivity = Connectivity(
99 | state = NetworkInfo.State.CONNECTED,
100 | type = ConnectivityManager.TYPE_WIFI,
101 | typeName = TYPE_NAME_WIFI
102 | )
103 | // note that unknown type is added initially by the ConnectivityPredicate#hasType method
104 | val givenTypes = intArrayOf(connectivity.type, Connectivity.UNKNOWN_TYPE)
105 | // when
106 | val equalTo = hasType(*givenTypes)
107 | val shouldBeEqualToGivenStatus = equalTo.test(connectivity)
108 | // then
109 | assertThat(shouldBeEqualToGivenStatus).isTrue()
110 | }
111 |
112 | @Test
113 | @Throws(Exception::class)
114 | fun typeShouldBeEqualToOneOfGivenMultipleValues() {
115 | // given
116 | val connectivity = Connectivity(
117 | state = NetworkInfo.State.CONNECTING,
118 | type = ConnectivityManager.TYPE_MOBILE,
119 | typeName = TYPE_NAME_MOBILE
120 | )
121 |
122 | // note that unknown type is added initially by the ConnectivityPredicate#hasType method
123 | val givenTypes = intArrayOf(
124 | ConnectivityManager.TYPE_WIFI,
125 | ConnectivityManager.TYPE_MOBILE,
126 | Connectivity.UNKNOWN_TYPE
127 | )
128 | // when
129 | val equalTo = hasType(*givenTypes)
130 | val shouldBeEqualToGivenStatus = equalTo.test(connectivity)
131 | // then
132 | assertThat(shouldBeEqualToGivenStatus).isTrue()
133 | }
134 |
135 | @Test
136 | @Throws(Exception::class)
137 | fun typeShouldNotBeEqualToGivenValue() {
138 | // given
139 | val connectivity = Connectivity(
140 | state = NetworkInfo.State.CONNECTED,
141 | type = ConnectivityManager.TYPE_WIFI,
142 | typeName = TYPE_NAME_WIFI
143 | )
144 |
145 | // note that unknown type is added initially by the ConnectivityPredicate#hasType method
146 | val givenTypes = intArrayOf(ConnectivityManager.TYPE_MOBILE, Connectivity.UNKNOWN_TYPE)
147 | // when
148 | val equalTo = hasType(*givenTypes)
149 | val shouldBeEqualToGivenStatus = equalTo.test(connectivity)
150 | // then
151 | assertThat(shouldBeEqualToGivenStatus).isFalse()
152 | }
153 |
154 | @Test
155 | fun theSameConnectivityObjectsShouldBeEqual() {
156 | // given
157 | val connectivityOne = Connectivity()
158 | val connectivityTwo = Connectivity()
159 | // when
160 | val objectsAreEqual = connectivityOne == connectivityTwo
161 | // then
162 | assertThat(objectsAreEqual).isTrue()
163 | }
164 |
165 | @Test
166 | fun twoDefaultObjectsShouldBeInTheSameBucket() {
167 | // given
168 | val connectivityOne = Connectivity()
169 | val connectivityTwo = Connectivity()
170 | // when
171 | val hashCodesAreEqual = connectivityOne.hashCode() == connectivityTwo.hashCode()
172 | // then
173 | assertThat(hashCodesAreEqual).isTrue()
174 | }
175 |
176 | @Test
177 | fun shouldAppendUnknownTypeWhileFilteringNetworkTypesInsidePredicate() {
178 | // given
179 | val types =
180 | intArrayOf(ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_WIFI)
181 | val expectedOutputTypes = intArrayOf(
182 | ConnectivityManager.TYPE_MOBILE,
183 | ConnectivityManager.TYPE_WIFI,
184 | Connectivity.UNKNOWN_TYPE
185 | )
186 | // when
187 | val outputTypes =
188 | appendUnknownNetworkTypeToTypes(types)
189 | // then
190 | assertThat(outputTypes).isEqualTo(expectedOutputTypes)
191 | }
192 |
193 | @Test
194 | fun shouldAppendUnknownTypeWhileFilteringNetworkTypesInsidePredicateForEmptyArray() {
195 | // given
196 | val types = intArrayOf()
197 | val expectedOutputTypes = intArrayOf(Connectivity.UNKNOWN_TYPE)
198 | // when
199 | val outputTypes = appendUnknownNetworkTypeToTypes(types)
200 | // then
201 | assertThat(outputTypes).isEqualTo(expectedOutputTypes)
202 | }
203 |
204 | @Test
205 | fun shouldCreateConnectivityWithBuilder() {
206 | // given
207 | val state = NetworkInfo.State.CONNECTED
208 | val detailedState = DetailedState.CONNECTED
209 | val type = ConnectivityManager.TYPE_WIFI
210 | val subType = ConnectivityManager.TYPE_WIMAX
211 | val typeName = TYPE_NAME_WIFI
212 | val subTypeName = "test subType"
213 | val reason = "no reason"
214 | val extraInfo = "extra info"
215 | // when
216 | val connectivity = Connectivity(
217 | state = state,
218 | detailedState = detailedState,
219 | type = type,
220 | subType = subType,
221 | available = true,
222 | failover = false,
223 | roaming = true,
224 | typeName = typeName,
225 | subTypeName = subTypeName,
226 | reason = reason,
227 | extraInfo = extraInfo
228 | )
229 |
230 | // then
231 | assertThat(connectivity.state).isEqualTo(state)
232 | assertThat(connectivity.detailedState).isEqualTo(detailedState)
233 | assertThat(connectivity.type).isEqualTo(type)
234 | assertThat(connectivity.subType).isEqualTo(subType)
235 | assertThat(connectivity.available).isTrue()
236 | assertThat(connectivity.failover).isFalse()
237 | assertThat(connectivity.roaming).isTrue()
238 | assertThat(connectivity.typeName).isEqualTo(typeName)
239 | assertThat(connectivity.subTypeName).isEqualTo(subTypeName)
240 | assertThat(connectivity.reason).isEqualTo(reason)
241 | assertThat(connectivity.extraInfo).isEqualTo(extraInfo)
242 | }
243 |
244 | @Test
245 | fun connectivityShouldNotBeEqualToAnotherOne() {
246 | // given
247 | val connectivityOne = Connectivity(
248 | state = NetworkInfo.State.CONNECTED,
249 | detailedState = DetailedState.CONNECTED,
250 | type = ConnectivityManager.TYPE_WIFI,
251 | subType = 1,
252 | available = true,
253 | failover = true,
254 | roaming = true,
255 | typeName = TYPE_NAME_WIFI,
256 | subTypeName = "subtypeOne",
257 | reason = "reasonOne",
258 | extraInfo = "extraInfoOne"
259 | )
260 |
261 | val connectivityTwo = Connectivity(
262 | state = NetworkInfo.State.DISCONNECTED,
263 | detailedState = DetailedState.DISCONNECTED,
264 | type = ConnectivityManager.TYPE_MOBILE,
265 | subType = 2,
266 | available = false,
267 | failover = false,
268 | roaming = false,
269 | typeName = TYPE_NAME_MOBILE,
270 | subTypeName = "subtypeTwo",
271 | reason = "reasonTwo",
272 | extraInfo = "extraInfoTwo"
273 | )
274 | // when
275 | val isAnotherConnectivityTheSame = connectivityOne == connectivityTwo
276 | // then
277 | assertThat(isAnotherConnectivityTheSame).isFalse()
278 | }
279 |
280 | @Test
281 | fun shouldCreateDefaultConnectivityWhenConnectivityManagerIsNull() {
282 | // given
283 | val context = ApplicationProvider.getApplicationContext()
284 | val connectivityManager: ConnectivityManager? = null
285 | // when
286 | val connectivity = create(context, connectivityManager)
287 | // then
288 | assertThat(connectivity.type).isEqualTo(Connectivity.UNKNOWN_TYPE)
289 | assertThat(connectivity.subType).isEqualTo(Connectivity.UNKNOWN_SUB_TYPE)
290 | assertThat(connectivity.state)
291 | .isEqualTo(NetworkInfo.State.DISCONNECTED)
292 | assertThat(connectivity.detailedState)
293 | .isEqualTo(DetailedState.IDLE)
294 | assertThat(connectivity.available).isFalse()
295 | assertThat(connectivity.failover).isFalse()
296 | assertThat(connectivity.roaming).isFalse()
297 | assertThat(connectivity.typeName)
298 | .isEqualTo(TYPE_NAME_NONE)
299 | assertThat(connectivity.subTypeName)
300 | .isEqualTo(TYPE_NAME_NONE)
301 | assertThat(connectivity.reason).isEmpty()
302 | assertThat(connectivity.extraInfo).isEmpty()
303 | }
304 |
305 | companion object {
306 | private const val TYPE_NAME_WIFI = "WIFI"
307 | private const val TYPE_NAME_MOBILE = "MOBILE"
308 | private const val TYPE_NAME_NONE = "NONE"
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/reactiveNetwork/src/test/kotlin/ru/beryukhov/reactivenetwork/network/observing/strategy/MarshmallowNetworkObservingStrategyTest.kt:
--------------------------------------------------------------------------------
1 | package ru.beryukhov.reactivenetwork.network.observing.strategy
2 |
3 | import android.annotation.TargetApi
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.ConnectivityManager
7 | import android.net.ConnectivityManager.NetworkCallback
8 | import android.net.Network
9 | import android.net.NetworkInfo
10 | import android.os.Build
11 | import android.os.PowerManager
12 | import androidx.test.core.app.ApplicationProvider
13 | import app.cash.turbine.test
14 | import com.google.common.truth.Truth.assertThat
15 | import io.mockk.every
16 | import io.mockk.mockk
17 | import io.mockk.spyk
18 | import io.mockk.verify
19 | import kotlinx.coroutines.flow.map
20 | import kotlinx.coroutines.test.runTest
21 | import org.junit.Before
22 | import org.junit.Test
23 | import org.junit.runner.RunWith
24 | import org.robolectric.RobolectricTestRunner
25 | import ru.beryukhov.reactivenetwork.Connectivity
26 |
27 | @RunWith(RobolectricTestRunner::class)
28 | open class MarshmallowNetworkObservingStrategyTest {
29 |
30 | private val strategy = spyk(MarshmallowNetworkObservingStrategy())
31 |
32 | private val powerManager = mockk(relaxed = true)
33 | private val connectivityManager = mockk(relaxed = true)
34 | private val contextMock = mockk(relaxed = true)
35 | private val intent = mockk(relaxed = true)
36 | private val network = mockk(relaxed = true)
37 |
38 | private lateinit var context: Context
39 |
40 | @Before
41 | fun setUp() {
42 | context = spyk(ApplicationProvider.getApplicationContext())
43 | }
44 |
45 | @Test
46 | fun shouldObserveConnectivity() = runTest {
47 | // given
48 | val context = ApplicationProvider.getApplicationContext()
49 |
50 | val testFlow = strategy.observeNetworkConnectivity(context).map { it.state }
51 | .test {
52 | assertThat(awaitItem()).isEqualTo(NetworkInfo.State.CONNECTED)
53 | }
54 | }
55 |
56 | @Test
57 | fun shouldCallOnError() {
58 | // given
59 | val message = "error message"
60 | val exception = Exception()
61 | // when
62 | strategy.onError(message, exception)
63 | // then
64 | verify(exactly = 1) { strategy.onError(message, exception) }
65 | }
66 |
67 | @Test
68 | fun shouldTryToUnregisterCallbackOnDispose() = runTest {
69 | // given
70 | // when
71 | strategy.observeNetworkConnectivity(context).test {
72 | cancelAndConsumeRemainingEvents()
73 | }
74 |
75 | // then
76 | verify { strategy.tryToUnregisterCallback(any()) }
77 | }
78 |
79 | @Test
80 | fun shouldTryToUnregisterReceiverOnDispose() = runTest {
81 | // given
82 | // when
83 | strategy.observeNetworkConnectivity(context).test {
84 | cancelAndConsumeRemainingEvents()
85 | }
86 |
87 | // then
88 | verify { strategy.tryToUnregisterReceiver(context) }
89 | }
90 |
91 | @Test
92 | fun shouldNotBeInIdleModeWhenDeviceIsNotInIdleAndIsNotIgnoringBatteryOptimizations() {
93 | // given
94 | preparePowerManagerMocks(idleMode = false, ignoreOptimizations = false)
95 | // when
96 | val isIdleMode = strategy.isIdleMode(contextMock)
97 | // then
98 | assertThat(isIdleMode).isFalse()
99 | }
100 |
101 | @Test
102 | fun shouldBeInIdleModeWhenDeviceIsNotIgnoringBatteryOptimizations() {
103 | // given
104 | preparePowerManagerMocks(idleMode = true, ignoreOptimizations = false)
105 | // when
106 | val isIdleMode = strategy.isIdleMode(contextMock)
107 | // then
108 | assertThat(isIdleMode).isTrue()
109 | }
110 |
111 | @Test
112 | fun shouldNotBeInIdleModeWhenDeviceIsInIdleModeAndIgnoringBatteryOptimizations() {
113 | // given
114 | preparePowerManagerMocks(idleMode = true, ignoreOptimizations = true)
115 | // when
116 | val isIdleMode = strategy.isIdleMode(contextMock)
117 | // then
118 | assertThat(isIdleMode).isFalse()
119 | }
120 |
121 | @Test
122 | fun shouldNotBeInIdleModeWhenDeviceIsNotInIdleMode() {
123 | // given
124 | preparePowerManagerMocks(idleMode = false, ignoreOptimizations = true)
125 | // when
126 | val isIdleMode = strategy.isIdleMode(contextMock)
127 | // then
128 | assertThat(isIdleMode).isFalse()
129 | }
130 |
131 | @Test
132 | fun shouldReceiveIntentInIdleMode() {
133 | // given
134 | preparePowerManagerMocks(idleMode = true, ignoreOptimizations = false)
135 | val broadcastReceiver = strategy.createIdleBroadcastReceiver()
136 | // when
137 | broadcastReceiver.onReceive(contextMock, intent)
138 | // then
139 | verify { strategy.onNext(any()) }
140 | }
141 | @Test
142 | fun shouldReceiveIntentWhenIsNotInIdleMode() {
143 | // given
144 | preparePowerManagerMocks(idleMode = false, ignoreOptimizations = false)
145 | val broadcastReceiver = strategy.createIdleBroadcastReceiver()
146 | every { contextMock.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager
147 | every { connectivityManager.activeNetworkInfo } returns null
148 | // when
149 | broadcastReceiver.onReceive(contextMock, intent)
150 | // then
151 | verify { strategy.onNext(any()) }
152 | }
153 |
154 | @TargetApi(Build.VERSION_CODES.M)
155 | private fun preparePowerManagerMocks(
156 | idleMode: Boolean,
157 | ignoreOptimizations: Boolean
158 | ) {
159 | val packageName = "com.github.pwittchen.test"
160 | every { contextMock.packageName } returns packageName
161 | every { contextMock.getSystemService(Context.POWER_SERVICE) } returns powerManager
162 | every { powerManager.isDeviceIdleMode } returns idleMode
163 | every { powerManager.isIgnoringBatteryOptimizations(packageName) } returns ignoreOptimizations
164 | }
165 |
166 | @Test
167 | fun shouldCreateNetworkCallbackOnSubscribe() = runTest {
168 | // when
169 | strategy.observeNetworkConnectivity(context).test {
170 | cancelAndConsumeRemainingEvents()
171 | }
172 |
173 | // then
174 | verify { strategy.createNetworkCallback(context) }
175 | }
176 |
177 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
178 | @Test
179 | fun shouldInvokeOnNextOnNetworkAvailable() {
180 | // given
181 | val networkCallback = strategy.createNetworkCallback(context)
182 | // when
183 | networkCallback.onAvailable(network)
184 | // then
185 | verify { strategy.onNext(any()) }
186 | }
187 |
188 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
189 | @Test
190 | fun shouldInvokeOnNextOnNetworkLost() {
191 | // given
192 | val networkCallback = strategy.createNetworkCallback(context)
193 | // when
194 | networkCallback.onLost(network)
195 | // then
196 | verify { strategy.onNext(any()) }
197 | }
198 |
199 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
200 | @Test
201 | fun shouldHandleErrorWhileTryingToUnregisterCallback() {
202 | // given
203 | strategy.observeNetworkConnectivity(context)
204 | val exception = IllegalArgumentException()
205 | every { connectivityManager.unregisterNetworkCallback(any()) } throws exception
206 | // when
207 | strategy.tryToUnregisterCallback(connectivityManager)
208 | // then
209 | verify {
210 | strategy.onError(
211 | MarshmallowNetworkObservingStrategy.ERROR_MSG_NETWORK_CALLBACK,
212 | exception
213 | )
214 | }
215 | }
216 |
217 | @Test
218 | fun shouldHandleErrorWhileTryingToUnregisterReceiver() {
219 | // given
220 | strategy.observeNetworkConnectivity(context)
221 | val exception = RuntimeException()
222 | every { contextMock.unregisterReceiver(any()) } throws exception
223 | // when
224 | strategy.tryToUnregisterReceiver(contextMock)
225 | // then
226 | verify {
227 | strategy.onError(
228 | MarshmallowNetworkObservingStrategy.ERROR_MSG_RECEIVER,
229 | exception
230 | )
231 | }
232 | }
233 |
234 | @Test
235 | fun shouldPropagateCurrentAndLastConnectivityWhenSwitchingFromWifiToMobile() {
236 | val lastType = ConnectivityManager.TYPE_WIFI
237 | val currentType = ConnectivityManager.TYPE_MOBILE
238 | assertThatConnectivityIsPropagatedDuringChange(lastType, currentType)
239 | }
240 |
241 | @Test
242 | fun shouldPropagateCurrentAndLastConnectivityWhenSwitchingFromMobileToWifi() {
243 | val lastType = ConnectivityManager.TYPE_MOBILE
244 | val currentType = ConnectivityManager.TYPE_WIFI
245 | assertThatConnectivityIsPropagatedDuringChange(lastType, currentType)
246 | }
247 |
248 | private fun assertThatConnectivityIsPropagatedDuringChange(lastType: Int, currentType: Int) = runTest {
249 | // given
250 | val last = Connectivity(
251 | type = lastType,
252 | state = NetworkInfo.State.CONNECTED
253 | )
254 | val current = Connectivity(
255 | type = currentType,
256 | state = NetworkInfo.State.DISCONNECTED,
257 | detailedState = NetworkInfo.DetailedState.CONNECTED
258 | )
259 | // when
260 | strategy.propagateAnyConnectedState(last, current).test {
261 | // then
262 | assertThat(awaitItem()).isEqualTo(current)
263 | assertThat(awaitItem()).isEqualTo(last)
264 | cancelAndConsumeRemainingEvents()
265 | }
266 | }
267 |
268 | @Test
269 | fun shouldNotPropagateLastConnectivityEventWhenTypeIsNotChanged() = runTest {
270 | // given
271 | val last = Connectivity(
272 | type = ConnectivityManager.TYPE_WIFI,
273 | state = NetworkInfo.State.CONNECTED
274 | )
275 | val current = Connectivity(
276 | type = ConnectivityManager.TYPE_WIFI,
277 | state = NetworkInfo.State.DISCONNECTED,
278 | detailedState = NetworkInfo.DetailedState.CONNECTED
279 | )
280 | // when
281 |
282 | strategy.propagateAnyConnectedState(last, current).test {
283 | // then
284 | assertThat(awaitItem()).isEqualTo(current)
285 | awaitComplete()
286 | }
287 | }
288 |
289 | @Test
290 | fun shouldNotPropagateLastConnectivityWhenWasNotConnected() = runTest {
291 | // given
292 | val last = Connectivity(
293 | type = ConnectivityManager.TYPE_WIFI,
294 | state = NetworkInfo.State.DISCONNECTED
295 | )
296 | val current = Connectivity(
297 | type = ConnectivityManager.TYPE_MOBILE,
298 | state = NetworkInfo.State.CONNECTED,
299 | detailedState = NetworkInfo.DetailedState.CONNECTED
300 | )
301 | // when
302 | strategy.propagateAnyConnectedState(last, current).test {
303 | // then
304 | assertThat(awaitItem()).isEqualTo(current)
305 | awaitComplete()
306 | }
307 | }
308 |
309 | @Test
310 | fun shouldNotPropagateLastConnectivityWhenIsConnected() = runTest {
311 | val last = Connectivity(
312 | type = ConnectivityManager.TYPE_WIFI,
313 | state = NetworkInfo.State.CONNECTED
314 | )
315 | val current = Connectivity(
316 | type = ConnectivityManager.TYPE_MOBILE,
317 | state = NetworkInfo.State.CONNECTED,
318 | detailedState = NetworkInfo.DetailedState.CONNECTED
319 | )
320 | // when
321 | strategy.propagateAnyConnectedState(last, current).test {
322 | // then
323 | assertThat(awaitItem()).isEqualTo(current)
324 | awaitComplete()
325 | }
326 | }
327 |
328 | @Test
329 | fun shouldNotPropagateLastConnectivityWhenIsIdle() = runTest {
330 | // given
331 | val last = Connectivity(
332 | type = ConnectivityManager.TYPE_WIFI,
333 | state = NetworkInfo.State.CONNECTED
334 | )
335 | val current = Connectivity(
336 | type = ConnectivityManager.TYPE_MOBILE,
337 | state = NetworkInfo.State.DISCONNECTED,
338 | detailedState = NetworkInfo.DetailedState.IDLE
339 | )
340 | // when
341 |
342 | strategy.propagateAnyConnectedState(last, current).test {
343 | // then
344 | assertThat(awaitItem()).isEqualTo(current)
345 | awaitComplete()
346 | }
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/detektConfig.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 0
3 | excludeCorrectable: false
4 | weights:
5 | # complexity: 2
6 | # LongParameterList: 1
7 | # style: 1
8 | # comments: 1
9 |
10 | config:
11 | validation: true
12 | warningsAsErrors: true
13 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
14 | excludes: ''
15 |
16 | processors:
17 | active: true
18 | exclude:
19 | - 'DetektProgressListener'
20 | # - 'FunctionCountProcessor'
21 | # - 'PropertyCountProcessor'
22 | # - 'ClassCountProcessor'
23 | # - 'PackageCountProcessor'
24 | # - 'KtFileCountProcessor'
25 |
26 | console-reports:
27 | active: true
28 | exclude:
29 | - 'ProjectStatisticsReport'
30 | - 'ComplexityReport'
31 | - 'NotificationReport'
32 | # - 'FindingsReport'
33 | - 'FileBasedFindingsReport'
34 |
35 | output-reports:
36 | active: true
37 | exclude:
38 | # - 'TxtOutputReport'
39 | # - 'XmlOutputReport'
40 | # - 'HtmlOutputReport'
41 |
42 | comments:
43 | active: true
44 | # https://detekt.github.io/detekt/comments.html#absentorwrongfilelicense
45 | # we don't use licenses per file, only root one
46 | AbsentOrWrongFileLicense:
47 | active: false
48 | licenseTemplateFile: 'license.template'
49 | # https://detekt.github.io/detekt/comments.html#commentoverprivatefunction
50 | CommentOverPrivateFunction:
51 | active: false
52 | # https://detekt.github.io/detekt/comments.html#commentoverprivateproperty
53 | CommentOverPrivateProperty:
54 | active: false
55 | # https://detekt.github.io/detekt/comments.html#endofsentenceformat
56 | EndOfSentenceFormat:
57 | active: false
58 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
59 | # https://detekt.github.io/detekt/comments.html#undocumentedpublicclass
60 | UndocumentedPublicClass:
61 | active: false
62 | searchInNestedClass: false
63 | searchInInnerClass: false
64 | searchInInnerObject: false
65 | searchInInnerInterface: false
66 | # https://detekt.github.io/detekt/comments.html#undocumentedpublicfunction
67 | UndocumentedPublicFunction:
68 | active: false
69 | # https://detekt.github.io/detekt/comments.html#undocumentedpublicproperty
70 | UndocumentedPublicProperty:
71 | active: false
72 |
73 | complexity:
74 | active: false
75 | ComplexCondition:
76 | active: true
77 | threshold: 4
78 | ComplexInterface:
79 | active: false
80 | threshold: 10
81 | includeStaticDeclarations: false
82 | includePrivateDeclarations: false
83 | CyclomaticComplexMethod:
84 | active: true
85 | threshold: 15
86 | ignoreSingleWhenExpression: false
87 | ignoreSimpleWhenEntries: false
88 | ignoreNestingFunctions: false
89 | nestingFunctions: [ run, let, apply, with, also, use, forEach, isNotNull, ifNull ]
90 | LabeledExpression:
91 | active: false
92 | ignoredLabels: [ ]
93 | LargeClass:
94 | active: true
95 | threshold: 600
96 | LongMethod:
97 | active: true
98 | threshold: 60
99 | LongParameterList:
100 | active: true
101 | functionThreshold: 6
102 | constructorThreshold: 7
103 | ignoreDefaultParameters: false
104 | ignoreDataClasses: true
105 | ignoreAnnotated: [ ]
106 | MethodOverloading:
107 | active: false
108 | threshold: 6
109 | NamedArguments:
110 | active: false
111 | threshold: 3
112 | NestedBlockDepth:
113 | active: true
114 | threshold: 4
115 | ReplaceSafeCallChainWithRun:
116 | active: false
117 | StringLiteralDuplication:
118 | active: false
119 | excludes: [ '**/test/**', '**/androidTest/**' ]
120 | threshold: 3
121 | ignoreAnnotation: true
122 | excludeStringsWithLessThan5Characters: true
123 | ignoreStringsRegex: '$^'
124 | TooManyFunctions:
125 | active: true
126 | excludes: [ '**/test/**', '**/androidTest/**' ]
127 | thresholdInFiles: 11
128 | thresholdInClasses: 11
129 | thresholdInInterfaces: 11
130 | thresholdInObjects: 11
131 | thresholdInEnums: 11
132 | ignoreDeprecated: false
133 | ignorePrivate: false
134 | ignoreOverridden: false
135 |
136 | coroutines:
137 | active: true
138 | GlobalCoroutineUsage:
139 | active: false
140 | InjectDispatcher:
141 | active: true
142 | dispatcherNames:
143 | - 'IO'
144 | - 'Default'
145 | - 'Unconfined'
146 | RedundantSuspendModifier:
147 | active: true
148 | SleepInsteadOfDelay:
149 | active: true
150 | SuspendFunSwallowedCancellation:
151 | active: false
152 | SuspendFunWithCoroutineScopeReceiver:
153 | active: false
154 | SuspendFunWithFlowReturnType:
155 | active: true
156 |
157 | empty-blocks:
158 | active: true
159 | # https://detekt.github.io/detekt/empty-blocks.html#emptycatchblock
160 | EmptyCatchBlock:
161 | active: true
162 | allowedExceptionNameRegex: '_|(ignore|expected).*'
163 | # https://detekt.github.io/detekt/empty-blocks.html#emptyclassblock
164 | EmptyClassBlock:
165 | active: true
166 | # https://detekt.github.io/detekt/empty-blocks.html#emptydefaultconstructor
167 | EmptyDefaultConstructor:
168 | active: true
169 | # https://detekt.github.io/detekt/empty-blocks.html#emptydowhileblock
170 | EmptyDoWhileBlock:
171 | active: true
172 | # https://detekt.github.io/detekt/empty-blocks.html#emptyelseblock
173 | EmptyElseBlock:
174 | active: true
175 | # https://detekt.github.io/detekt/empty-blocks.html#emptyfinallyblock
176 | EmptyFinallyBlock:
177 | active: true
178 | # https://detekt.github.io/detekt/empty-blocks.html#emptyforblock
179 | EmptyForBlock:
180 | active: true
181 | # https://detekt.github.io/detekt/empty-blocks.html#emptyfunctionblock
182 | # todo enable, 26 errors
183 | EmptyFunctionBlock:
184 | active: false
185 | ignoreOverridden: false
186 | # https://detekt.github.io/detekt/empty-blocks.html#emptyifblock
187 | EmptyIfBlock:
188 | active: true
189 | # https://detekt.github.io/detekt/empty-blocks.html#emptyinitblock
190 | EmptyInitBlock:
191 | active: true
192 | # https://detekt.github.io/detekt/empty-blocks.html#emptyktfile
193 | EmptyKtFile:
194 | active: true
195 | # https://detekt.github.io/detekt/empty-blocks.html#emptysecondaryconstructor
196 | EmptySecondaryConstructor:
197 | active: true
198 | # https://detekt.github.io/detekt/empty-blocks.html#emptytryblock
199 | EmptyTryBlock:
200 | active: true
201 | # https://detekt.github.io/detekt/empty-blocks.html#emptywhenblock
202 | EmptyWhenBlock:
203 | active: true
204 | # https://detekt.github.io/detekt/empty-blocks.html#emptywhileblock
205 | EmptyWhileBlock:
206 | active: true
207 |
208 | exceptions:
209 | active: false
210 | ExceptionRaisedInUnexpectedLocation:
211 | active: false
212 | methodNames: [ toString, hashCode, equals, finalize ]
213 | InstanceOfCheckForException:
214 | active: false
215 | excludes: [ '**/test/**', '**/androidTest/**' ]
216 | NotImplementedDeclaration:
217 | active: false
218 | PrintStackTrace:
219 | active: false
220 | RethrowCaughtException:
221 | active: false
222 | ReturnFromFinally:
223 | active: false
224 | ignoreLabeled: false
225 | SwallowedException:
226 | active: false
227 | ignoredExceptionTypes:
228 | - InterruptedException
229 | - NumberFormatException
230 | - ParseException
231 | - MalformedURLException
232 | allowedExceptionNameRegex: '_|(ignore|expected).*'
233 | ThrowingExceptionFromFinally:
234 | active: false
235 | ThrowingExceptionInMain:
236 | active: false
237 | ThrowingExceptionsWithoutMessageOrCause:
238 | active: false
239 | excludes: [ '**/test/**', '**/androidTest/**' ]
240 | exceptions:
241 | - IllegalArgumentException
242 | - IllegalStateException
243 | - IOException
244 | ThrowingNewInstanceOfSameException:
245 | active: false
246 | TooGenericExceptionCaught:
247 | active: true
248 | excludes: [ '**/test/**', '**/androidTest/**' ]
249 | exceptionNames:
250 | - ArrayIndexOutOfBoundsException
251 | - Error
252 | - Exception
253 | - IllegalMonitorStateException
254 | - NullPointerException
255 | - IndexOutOfBoundsException
256 | - RuntimeException
257 | - Throwable
258 | allowedExceptionNameRegex: '_|(ignore|expected).*'
259 | TooGenericExceptionThrown:
260 | active: true
261 | exceptionNames:
262 | - Error
263 | - Exception
264 | - Throwable
265 | - RuntimeException
266 |
267 | formatting:
268 | active: true
269 | android: true
270 | autoCorrect: false
271 |
272 | # todo rise an issue: false positive on kotlin @file annotations
273 | AnnotationOnSeparateLine:
274 | active: false
275 | AnnotationSpacing:
276 | active: true
277 | # todo fix and enable
278 | ArgumentListWrapping:
279 | active: false
280 | # questionable rule; && and || goes to the end of line, instead of beginning a new line as we do right now
281 | ChainWrapping:
282 | active: false
283 | CommentSpacing:
284 | active: true
285 | # duplicate of naming:EnumNaming
286 | EnumEntryNameCase:
287 | active: false
288 | # todo what is it?
289 | Filename:
290 | active: false
291 | # DUPLICATE of style:NewLineAtEndOfFile
292 | FinalNewline:
293 | active: false
294 | insertFinalNewLine: false
295 | ImportOrdering:
296 | active: true
297 | layout: '*,java.**,javax.**,kotlin.**,^'
298 | # blocked by bugs: https://github.com/pinterest/ktlint/issues?q=is%3Aissue+is%3Aopen+Indentation
299 | Indentation:
300 | active: false
301 | indentSize: 4
302 | # DUPLICATE of style:MaxLineLength
303 | MaximumLineLength:
304 | active: false
305 | maxLineLength: 120
306 | # https://ktlint.github.io/#rule-modifier-order
307 | ModifierOrdering:
308 | active: true
309 | MultiLineIfElse:
310 | active: true
311 | NoBlankLineBeforeRbrace:
312 | active: true
313 | # https://ktlint.github.io/#rule-blank
314 | NoConsecutiveBlankLines:
315 | active: true
316 | # https://ktlint.github.io/#rule-empty-class-body
317 | NoEmptyClassBody:
318 | active: true
319 | # questionable rule, it is good idea to have some visual space after function declaration
320 | NoEmptyFirstLineInMethodBlock:
321 | active: false
322 | NoLineBreakAfterElse:
323 | active: true
324 | NoLineBreakBeforeAssignment:
325 | active: true
326 | NoMultipleSpaces:
327 | active: true
328 | # https://ktlint.github.io/#rule-semi
329 | NoSemicolons:
330 | active: true
331 | # https://ktlint.github.io/#rule-trailing-whitespaces
332 | NoTrailingSpaces:
333 | active: true
334 | NoUnitReturn:
335 | active: true
336 |
337 | # DUPLICATE of style UnusedImports
338 | NoUnusedImports:
339 | active: false
340 | # DUPLICATE of style WildcardImports
341 | NoWildcardImports:
342 | active: false
343 |
344 | # DUPLICATE of naming:PackageNaming rule
345 | PackageName:
346 | active: false
347 | ParameterListWrapping:
348 | active: true
349 |
350 | # https://ktlint.github.io/#rule-spacing
351 | SpacingAroundColon:
352 | active: true
353 | SpacingAroundComma:
354 | active: true
355 | SpacingAroundCurly:
356 | active: true
357 | SpacingAroundDot:
358 | active: true
359 | SpacingAroundDoubleColon:
360 | active: true
361 | SpacingAroundKeyword:
362 | active: true
363 | SpacingAroundOperators:
364 | active: true
365 | SpacingAroundParens:
366 | active: true
367 | SpacingAroundRangeOperator:
368 | active: true
369 | # https://detekt.github.io/detekt/formatting.html#spacingbetweendeclarationswithannotations
370 | SpacingBetweenDeclarationsWithAnnotations:
371 | active: false
372 | # https://detekt.github.io/detekt/formatting.html#spacingbetweendeclarationswithcomments
373 | SpacingBetweenDeclarationsWithComments:
374 | active: true
375 | # https://ktlint.github.io/#rule-string-template
376 | StringTemplate:
377 | active: true
378 |
379 | naming:
380 | active: true
381 | # https://detekt.github.io/detekt/naming.html#classnaming
382 | ClassNaming:
383 | active: true
384 | classPattern: '[A-Z][a-zA-Z0-9]*'
385 | # https://detekt.github.io/detekt/naming.html#constructorparameternaming
386 | ConstructorParameterNaming:
387 | active: true
388 | parameterPattern: '[a-z][A-Za-z0-9]*'
389 | privateParameterPattern: '[a-z][A-Za-z0-9]*'
390 | excludeClassPattern: '$^'
391 | EnumNaming:
392 | active: false
393 | excludes: [ '**/test/**', '**/androidTest/**' ]
394 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
395 | ForbiddenClassName:
396 | active: false
397 | excludes: [ '**/test/**', '**/androidTest/**' ]
398 | forbiddenName: [ ]
399 | FunctionMaxLength:
400 | active: false
401 | excludes: [ '**/test/**', '**/androidTest/**' ]
402 | maximumFunctionNameLength: 30
403 | # blocked by `Is` functions
404 | FunctionMinLength:
405 | active: false
406 | excludes: [ '**/test/**', '**/androidTest/**' ]
407 | minimumFunctionNameLength: 3
408 | # blocked by `Is` functions
409 | FunctionNaming:
410 | active: false
411 | excludes: [ '**/test/**', '**/androidTest/**' ]
412 | functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
413 | excludeClassPattern: '$^'
414 | ignoreAnnotated: [ 'Composable' ]
415 | FunctionParameterNaming:
416 | active: false
417 | excludes: [ '**/test/**', '**/androidTest/**' ]
418 | parameterPattern: '[a-z][A-Za-z0-9]*'
419 | excludeClassPattern: '$^'
420 | # TODO: enable
421 | InvalidPackageDeclaration:
422 | active: false
423 | rootPackage: ''
424 | # https://detekt.github.io/detekt/naming.html#matchingdeclarationname
425 | MatchingDeclarationName:
426 | active: true
427 | mustBeFirst: true
428 | # https://detekt.github.io/detekt/naming.html#membernameequalsclassname
429 | MemberNameEqualsClassName:
430 | active: false
431 | ignoreOverridden: true
432 | NonBooleanPropertyPrefixedWithIs:
433 | active: false
434 | excludes: [ '**/test/**', '**/androidTest/**' ]
435 | ObjectPropertyNaming:
436 | active: false
437 | excludes: [ '**/test/**', '**/androidTest/**' ]
438 | constantPattern: '[A-Za-z][_A-Za-z0-9]*'
439 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
440 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
441 | PackageNaming:
442 | active: false
443 | excludes: [ '**/test/**', '**/androidTest/**' ]
444 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
445 | TopLevelPropertyNaming:
446 | active: false
447 | excludes: [ '**/test/**', '**/androidTest/**' ]
448 | constantPattern: '[A-Z][_A-Z0-9]*'
449 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
450 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
451 | VariableMaxLength:
452 | active: false
453 | excludes: [ '**/test/**', '**/androidTest/**' ]
454 | maximumVariableNameLength: 64
455 | VariableMinLength:
456 | active: false
457 | excludes: [ '**/test/**', '**/androidTest/**' ]
458 | minimumVariableNameLength: 1
459 | # https://detekt.github.io/detekt/naming.html#variablenaming
460 | VariableNaming:
461 | active: true
462 | variablePattern: '[a-z][A-Za-z0-9]*'
463 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
464 | excludeClassPattern: '$^'
465 |
466 | performance:
467 | active: false
468 | ArrayPrimitive:
469 | active: true
470 | ForEachOnRange:
471 | active: true
472 | excludes: [ '**/test/**', '**/androidTest/**' ]
473 | SpreadOperator:
474 | active: true
475 | excludes: [ '**/test/**', '**/androidTest/**' ]
476 | UnnecessaryTemporaryInstantiation:
477 | active: true
478 |
479 | potential-bugs:
480 | active: false
481 | Deprecation:
482 | active: false
483 | EqualsAlwaysReturnsTrueOrFalse:
484 | active: false
485 | EqualsWithHashCodeExist:
486 | active: false
487 | ExplicitGarbageCollectionCall:
488 | active: false
489 | HasPlatformType:
490 | active: false
491 | IgnoredReturnValue:
492 | active: false
493 | restrictToConfig: true
494 | returnValueAnnotations: [ '*.CheckReturnValue', '*.CheckResult' ]
495 | ImplicitDefaultLocale:
496 | active: false
497 | ImplicitUnitReturnType:
498 | active: false
499 | allowExplicitReturnType: true
500 | InvalidRange:
501 | active: false
502 | IteratorHasNextCallsNextMethod:
503 | active: false
504 | IteratorNotThrowingNoSuchElementException:
505 | active: false
506 | LateinitUsage:
507 | active: false
508 | excludes: [ '**/test/**', '**/androidTest/**' ]
509 | ignoreAnnotated: [ ]
510 | ignoreOnClassesPattern: ''
511 | MapGetWithNotNullAssertionOperator:
512 | active: false
513 | NullableToStringCall:
514 | active: false
515 | UnconditionalJumpStatementInLoop:
516 | active: false
517 | UnnecessaryNotNullOperator:
518 | active: false
519 | UnnecessarySafeCall:
520 | active: false
521 | UnreachableCode:
522 | active: false
523 | UnsafeCallOnNullableType:
524 | active: false
525 | UnsafeCast:
526 | active: false
527 | UselessPostfixExpression:
528 | active: false
529 | WrongEqualsTypeParameter:
530 | active: false
531 |
532 | style:
533 | active: true
534 | BracesOnIfStatements:
535 | active: true
536 | singleLine: 'never'
537 | multiLine: 'always'
538 | # https://detekt.github.io/detekt/style.html#classordering
539 | ClassOrdering:
540 | active: true
541 | # https://detekt.github.io/detekt/style.html#collapsibleifstatements
542 | # questionable rule, no need for now
543 | CollapsibleIfStatements:
544 | active: false
545 | # https://detekt.github.io/detekt/style.html#dataclasscontainsfunctions
546 | # probably a good idea, but seems too strict
547 | DataClassContainsFunctions:
548 | active: false
549 | conversionFunctionPrefix: [ 'to' ]
550 | # https://detekt.github.io/detekt/style.html#dataclassshouldbeimmutable
551 | # todo probably a good idea to enable it
552 | DataClassShouldBeImmutable:
553 | active: false
554 | # https://detekt.github.io/detekt/style.html#equalsnullcall
555 | EqualsNullCall:
556 | active: true
557 | # https://detekt.github.io/detekt/style.html#equalsonsignatureline
558 | EqualsOnSignatureLine:
559 | active: true
560 | # https://detekt.github.io/detekt/style.html#explicitcollectionelementaccessmethod
561 | ExplicitCollectionElementAccessMethod:
562 | active: true
563 | # https://detekt.github.io/detekt/style.html#explicititlambdaparameter
564 | ExplicitItLambdaParameter:
565 | active: true
566 | # https://detekt.github.io/detekt/style.html#expressionbodysyntax
567 | # sometimes it's harder to read
568 | ExpressionBodySyntax:
569 | active: false
570 | includeLineWrapping: true
571 | # https://detekt.github.io/detekt/style.html#forbiddencomment
572 | ForbiddenComment:
573 | active: true
574 | comments: [ 'STOPSHIP' ]
575 | allowedPatterns: ''
576 | # https://detekt.github.io/detekt/style.html#forbiddenimport
577 | # todo maybe use it to ban junit 4 in test code
578 | ForbiddenImport:
579 | active: true
580 | imports: [ ]
581 | forbiddenPatterns: 'gradle.kotlin.dsl.accessors.*'
582 | # https://detekt.github.io/detekt/style.html#forbiddenmethodcall
583 | # needs type resolution config https://github.com/detekt/detekt/issues/2259
584 | ForbiddenMethodCall:
585 | active: false
586 | methods: [ 'kotlin.io.println', 'kotlin.io.print' ]
587 | # https://detekt.github.io/detekt/style.html#forbiddenvoid
588 | # needs type resolution config https://github.com/detekt/detekt/issues/2259
589 | ForbiddenVoid:
590 | active: false
591 | ignoreOverridden: false
592 | ignoreUsageInGenerics: false
593 | # https://detekt.github.io/detekt/style.html#functiononlyreturningconstant
594 | FunctionOnlyReturningConstant:
595 | active: false
596 | ignoreOverridableFunction: true
597 | excludedFunctions: [ 'describeContents' ]
598 | ignoreAnnotated: [ 'dagger.Provides' ]
599 | # https://detekt.github.io/detekt/style.html#loopwithtoomanyjumpstatements
600 | LoopWithTooManyJumpStatements:
601 | active: true
602 | maxJumpCount: 1
603 | # https://detekt.github.io/detekt/style.html#magicnumber
604 | MagicNumber:
605 | active: false
606 | excludes: [ '**/build.gradle.kts', '**/test/**', '**/androidTest/**' ]
607 | ignoreNumbers: [ '-1', '0', '1', '2' ]
608 | ignoreHashCodeFunction: true
609 | ignorePropertyDeclaration: true
610 | ignoreLocalVariableDeclaration: true
611 | ignoreConstantDeclaration: true
612 | ignoreCompanionObjectPropertyDeclaration: true
613 | ignoreAnnotation: true
614 | ignoreNamedArgument: true
615 | ignoreEnums: true
616 | ignoreRanges: false
617 | # https://detekt.github.io/detekt/style.html#mandatorybracesloops
618 | MandatoryBracesLoops:
619 | active: true
620 | # https://detekt.github.io/detekt/style.html#maxlinelength
621 | MaxLineLength:
622 | active: true
623 | maxLineLength: 120
624 | excludePackageStatements: true
625 | excludeImportStatements: true
626 | excludeCommentStatements: true
627 | # https://detekt.github.io/detekt/style.html#maybeconst
628 | MayBeConst:
629 | active: true
630 | # https://detekt.github.io/detekt/style.html#modifierorder
631 | ModifierOrder:
632 | active: true
633 | # https://detekt.github.io/detekt/style.html#nestedclassesvisibility
634 | NestedClassesVisibility:
635 | active: true
636 | # https://detekt.github.io/detekt/style.html#newlineatendoffile
637 | NewLineAtEndOfFile:
638 | active: true
639 | # https://detekt.github.io/detekt/style.html#notabs
640 | NoTabs:
641 | active: true
642 | # https://detekt.github.io/detekt/style.html#optionalabstractkeyword
643 | OptionalAbstractKeyword:
644 | active: true
645 | # https://detekt.github.io/detekt/style.html#optionalunit
646 | OptionalUnit:
647 | active: false
648 | BracesOnWhenStatements:
649 | active: true
650 | # https://detekt.github.io/detekt/style.html#prefertooverpairsyntax
651 | PreferToOverPairSyntax:
652 | active: true
653 | # https://detekt.github.io/detekt/style.html#protectedmemberinfinalclass
654 | ProtectedMemberInFinalClass:
655 | active: true
656 | RedundantExplicitType:
657 | active: false
658 | RedundantHigherOrderMapUsage:
659 | active: false
660 | # https://detekt.github.io/detekt/style.html#redundantvisibilitymodifierrule
661 | # todo don't know about kotlin strict mode
662 | # fix in 1.15 https://github.com/detekt/detekt/issues/3125 only works per module, not in our detektAll task
663 | # because of how strict api detection works
664 | RedundantVisibilityModifierRule:
665 | active: false
666 | # https://detekt.github.io/detekt/style.html#returncount
667 | # todo enable (11 errors)
668 | ReturnCount:
669 | active: false
670 | max: 2
671 | excludedFunctions: [ 'equals' ]
672 | excludeLabeled: false
673 | excludeReturnFromLambda: true
674 | excludeGuardClauses: false
675 | # https://detekt.github.io/detekt/style.html#safecast
676 | SafeCast:
677 | active: false
678 | SerialVersionUIDInSerializableClass:
679 | active: false
680 | SpacingBetweenPackageAndImports:
681 | active: false
682 | ThrowsCount:
683 | active: false
684 | max: 2
685 | TrailingWhitespace:
686 | active: false
687 | UnderscoresInNumericLiterals:
688 | active: false
689 | acceptableLength: 5
690 | UnnecessaryAbstractClass:
691 | active: false
692 | ignoreAnnotated: [ 'dagger.Module' ]
693 | UnnecessaryAnnotationUseSiteTarget:
694 | active: false
695 | UnnecessaryApply:
696 | active: false
697 | UnnecessaryInheritance:
698 | active: false
699 | UnnecessaryLet:
700 | active: false
701 | # https://detekt.github.io/detekt/style.html#unnecessaryparentheses
702 | UnnecessaryParentheses:
703 | active: true
704 | UntilInsteadOfRangeTo:
705 | active: false
706 | # https://detekt.github.io/detekt/style.html#unusedimports
707 | UnusedImports:
708 | active: true
709 | # https://detekt.github.io/detekt/style.html#unusedprivateclass
710 | UnusedPrivateClass:
711 | active: true
712 | # https://detekt.github.io/detekt/style.html#unusedprivatemember
713 | UnusedPrivateMember:
714 | active: true
715 | allowedNames: '(_|ignored|expected|serialVersionUID)'
716 | # https://detekt.github.io/detekt/style.html#usearrayliteralsinannotations
717 | UseArrayLiteralsInAnnotations:
718 | active: true
719 | UseCheckNotNull:
720 | active: false
721 | UseCheckOrError:
722 | active: false
723 | UseDataClass:
724 | active: false
725 | ignoreAnnotated: [ ]
726 | allowVars: false
727 | UseEmptyCounterpart:
728 | active: false
729 | UseIfEmptyOrIfBlank:
730 | active: false
731 | UseIfInsteadOfWhen:
732 | active: false
733 | UseRequire:
734 | active: false
735 | UseRequireNotNull:
736 | active: false
737 | UselessCallOnNotNull:
738 | active: false
739 | UtilityClassWithPublicConstructor:
740 | active: false
741 | # https://detekt.github.io/detekt/style.html#varcouldbeval
742 | VarCouldBeVal:
743 | active: true
744 | # https://detekt.github.io/detekt/style.html#wildcardimport
745 | WildcardImport:
746 | active: true
747 | excludes: [ ]
748 | excludeImports: [ ]
749 |
--------------------------------------------------------------------------------