├── .editorconfig ├── .github └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── build-logic ├── build.gradle.kts ├── checks │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── convention.detekt.gradle.kts ├── gradle-ext │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── ru │ │ └── beryukhov │ │ └── android │ │ └── VersionCatalog.kt └── settings.gradle.kts ├── build.gradle.kts ├── buildSrc ├── .gitignore └── build.gradle.kts ├── detektConfig.yml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── reactiveNetwork ├── .gitignore ├── build.gradle.kts └── src │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── ru │ │ │ └── beryukhov │ │ │ └── reactivenetwork │ │ │ ├── Connectivity.kt │ │ │ ├── ConnectivityPredicate.kt │ │ │ ├── Preconditions.kt │ │ │ ├── Predicate.kt │ │ │ ├── ReactiveNetwork.kt │ │ │ ├── TickerFlow.kt │ │ │ ├── internet │ │ │ └── observing │ │ │ │ ├── InternetObservingSettings.kt │ │ │ │ ├── InternetObservingStrategy.kt │ │ │ │ ├── error │ │ │ │ ├── DefaultErrorHandler.kt │ │ │ │ └── ErrorHandler.kt │ │ │ │ └── strategy │ │ │ │ ├── SocketInternetObservingStrategy.kt │ │ │ │ └── WalledGardenInternetObservingStrategy.kt │ │ │ └── network │ │ │ └── observing │ │ │ ├── NetworkObservingStrategy.kt │ │ │ └── strategy │ │ │ ├── LollipopNetworkObservingStrategy.kt │ │ │ ├── MarshmallowNetworkObservingStrategy.kt │ │ │ └── PreLollipopNetworkObservingStrategy.kt │ └── res │ │ └── xml │ │ └── network_security_config.xml │ └── test │ ├── kotlin │ └── ru │ │ └── beryukhov │ │ └── reactivenetwork │ │ ├── ConnectivityTest.kt │ │ ├── PreconditionsTest.kt │ │ ├── ReactiveNetworkTest.kt │ │ ├── internet │ │ └── observing │ │ │ ├── InternetObservingSettingsTest.kt │ │ │ ├── error │ │ │ └── DefaultErrorHandlerTest.kt │ │ │ └── strategy │ │ │ ├── SocketInternetObservingStrategyTest.kt │ │ │ └── WalledGardenInternetObservingStrategyTest.kt │ │ └── network │ │ └── observing │ │ ├── NetworkObservingStrategyTest.kt │ │ └── strategy │ │ ├── LollipopNetworkObservingStrategyTest.kt │ │ ├── MarshmallowNetworkObservingStrategyTest.kt │ │ └── PreLollipopNetworkObservingStrategyTest.kt │ └── resources │ └── robolectric.properties ├── sample ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── ru │ │ └── beryukhov │ │ └── sample │ │ └── MainActivity.kt │ └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap │ └── ic_launcher.png │ └── values │ ├── colors.xml │ └── strings.xml ├── scripts ├── publish-module.gradle └── publish-root.gradle ├── settings.gradle.kts └── utils.gradle /.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.**, ^ -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /build-logic/build 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | path := ./ 2 | 3 | detekt: 4 | $(path)gradlew detektAll 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlowReactiveNetwork on Coroutines 2 | [![Download](https://img.shields.io/maven-central/v/ru.beryukhov/flowreactivenetwork?versionPrefix=1.0.4) ](https://repo1.maven.org/maven2/ru/beryukhov/flowreactivenetwork/1.0.4/) 3 | [![Kotlin Version](https://img.shields.io/badge/Kotlin-1.5.20-blue.svg)](https://kotlinlang.org) 4 | 5 | [![kotlinweekly](https://img.shields.io/badge/kotlinweekly.net-204-blue.svg)](https://mailchi.mp/kotlinweekly/kotlin-weekly-204) 6 | ![CI](https://github.com/AndreySBer/FlowReactiveNetwork/workflows/Android%20CI/badge.svg) 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 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | -------------------------------------------------------------------------------- /build-logic/checks/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /build-logic/checks/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | // group = "com.avito.android.buildlogic" 6 | 7 | dependencies { 8 | implementation(projects.gradleExt) 9 | implementation(libs.detektGradle) 10 | // workaround for https://github.com/gradle/gradle/issues/15383 11 | implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) 12 | } 13 | -------------------------------------------------------------------------------- /build-logic/checks/src/main/kotlin/convention.detekt.gradle.kts: -------------------------------------------------------------------------------- 1 | import io.gitlab.arturbosch.detekt.Detekt 2 | import ru.beryukhov.android.withVersionCatalog 3 | 4 | plugins { 5 | /** 6 | * https://docs.gradle.org/current/userguide/base_plugin.html 7 | * base plugin added to add wiring on check->build tasks 8 | */ 9 | base 10 | id("io.gitlab.arturbosch.detekt") 11 | } 12 | 13 | // workaround for https://github.com/gradle/gradle/issues/15383 14 | project.withVersionCatalog { libs -> 15 | dependencies { 16 | detektPlugins(libs.detektFormatting) 17 | } 18 | } 19 | 20 | val detektAll = tasks.register("detektAll") { 21 | description = "Runs over whole code base without the starting overhead for each module." 22 | parallel = true 23 | setSource(files(projectDir)) 24 | 25 | config.setFrom(files(project.rootDir.resolve("detektConfig.yml"))) 26 | buildUponDefaultConfig = false 27 | 28 | include("**/*.kt") 29 | include("**/*.kts") 30 | exclude("**/resources/**") 31 | exclude("**/build/**") 32 | reports { 33 | xml.required.set(false) 34 | html.required.set(false) 35 | } 36 | } 37 | 38 | tasks.named("check").configure { 39 | dependsOn(detektAll) 40 | } 41 | -------------------------------------------------------------------------------- /build-logic/gradle-ext/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /build-logic/gradle-ext/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | // group = "com.avito.android.buildlogic" 6 | 7 | dependencies { 8 | // workaround for https://github.com/gradle/gradle/issues/15383 9 | implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) 10 | } 11 | -------------------------------------------------------------------------------- /build-logic/gradle-ext/src/main/kotlin/ru/beryukhov/android/VersionCatalog.kt: -------------------------------------------------------------------------------- 1 | package ru.beryukhov.android 2 | 3 | import org.gradle.accessors.dm.LibrariesForLibs 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.the 6 | 7 | /** 8 | * workaround to make version catalog accessible in convention plugins 9 | * https://github.com/gradle/gradle/issues/15383 10 | */ 11 | fun Project.withVersionCatalog(block: (libs: LibrariesForLibs) -> Unit) { 12 | if (project.name != "gradle-kotlin-dsl-accessors") { 13 | val libs = the() 14 | block.invoke(libs) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "build-logic" 2 | 3 | @Suppress("UnstableApiUsage") 4 | dependencyResolutionManagement { 5 | 6 | versionCatalogs { 7 | create("libs") { 8 | from(files("../gradle/libs.versions.toml")) 9 | } 10 | } 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | include("checks") 19 | /** 20 | * renamed from 'gradle' to prevent IDE resolution conflict: 21 | * usages of "typesafe project accessors", e.g. `projects.gradle.someProject` was red in IDE 22 | * build was fine however 23 | */ 24 | include("gradle-ext") 25 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("convention.detekt") 3 | // id("io.github.gradle-nexus.publish-plugin") version "1.1.0" 4 | } 5 | 6 | buildscript { 7 | repositories { 8 | google() 9 | mavenCentral() 10 | maven("https://plugins.gradle.org/m2/") 11 | } 12 | dependencies { 13 | classpath(libs.androidGradle) 14 | classpath(libs.kotlinGradle) 15 | classpath(libs.bintrayGradle) 16 | } 17 | } 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | maven("https://dl.bintray.com/andreyberyukhov/FlowReactiveNetwork") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phansier/FlowReactiveNetwork/f7275af48eba29f0a721fe5ae16a48fb43789e75/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | clients3.google.com 5 | 6 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /reactiveNetwork/src/test/resources/robolectric.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | sdk=23 -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phansier/FlowReactiveNetwork/f7275af48eba29f0a721fe5ae16a48fb43789e75/sample/src/main/res/mipmap/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Sample 3 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------