├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── misc.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── vvinogra │ │ └── internetconnectionmonitor │ │ ├── App.kt │ │ ├── MainActivity.kt │ │ ├── data │ │ └── NetworkConnectionManager.kt │ │ └── di │ │ └── AppModule.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values-night │ └── themes.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | InternetConnectionMonitor -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | compileSdk 32 10 | 11 | defaultConfig { 12 | applicationId "com.github.vvinogra.internetconnectionmonitor" 13 | minSdk 26 14 | targetSdk 32 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | } 31 | kotlinOptions { 32 | jvmTarget = '1.8' 33 | } 34 | } 35 | 36 | dependencies { 37 | 38 | implementation 'androidx.core:core-ktx:1.7.0' 39 | implementation 'androidx.appcompat:appcompat:1.4.2' 40 | implementation 'com.google.android.material:material:1.6.1' 41 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 42 | testImplementation 'junit:junit:4.13.2' 43 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 45 | 46 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2' 47 | implementation 'androidx.activity:activity-ktx:1.5.1' 48 | implementation 'com.jakewharton.timber:timber:5.0.1' 49 | implementation 'com.google.dagger:hilt-android:2.42' 50 | kapt 'com.google.dagger:hilt-compiler:2.42' 51 | } -------------------------------------------------------------------------------- /app/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 -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/vvinogra/internetconnectionmonitor/App.kt: -------------------------------------------------------------------------------- 1 | package com.github.vvinogra.internetconnectionmonitor 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App: Application() -------------------------------------------------------------------------------- /app/src/main/java/com/github/vvinogra/internetconnectionmonitor/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.vvinogra.internetconnectionmonitor 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import android.widget.Button 6 | import android.widget.TextView 7 | import androidx.annotation.StringRes 8 | import androidx.lifecycle.lifecycleScope 9 | import com.github.vvinogra.internetconnectionmonitor.data.NetworkConnectionManager 10 | import dagger.hilt.android.AndroidEntryPoint 11 | import kotlinx.coroutines.flow.launchIn 12 | import kotlinx.coroutines.flow.onEach 13 | import javax.inject.Inject 14 | 15 | @AndroidEntryPoint 16 | class MainActivity : AppCompatActivity() { 17 | 18 | @Inject 19 | lateinit var networkConnectionManager: NetworkConnectionManager 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | setContentView(R.layout.activity_main) 25 | 26 | val tvIsNetworkConnected: TextView = findViewById(R.id.tvIsNetworkConnected) 27 | 28 | val btnStartListen: Button = findViewById(R.id.btnStartListen) 29 | val btnStopListen: Button = findViewById(R.id.btnStopListen) 30 | 31 | btnStartListen.setOnClickListener { 32 | networkConnectionManager.startListenNetworkState() 33 | } 34 | 35 | btnStopListen.setOnClickListener { 36 | networkConnectionManager.stopListenNetworkState() 37 | } 38 | 39 | networkConnectionManager.isNetworkConnectedFlow 40 | .onEach { 41 | @StringRes val res = if (it) { 42 | R.string.network_is_connected 43 | } else { 44 | R.string.network_is_disconnected 45 | } 46 | 47 | tvIsNetworkConnected.setText(res) 48 | } 49 | .launchIn(lifecycleScope) 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/vvinogra/internetconnectionmonitor/data/NetworkConnectionManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.vvinogra.internetconnectionmonitor.data 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.net.Network 6 | import android.net.NetworkCapabilities 7 | import androidx.core.content.getSystemService 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.flow.* 11 | import javax.inject.Inject 12 | import javax.inject.Singleton 13 | 14 | interface NetworkConnectionManager { 15 | /** 16 | * Emits [Boolean] value when the current network becomes available or unavailable. 17 | */ 18 | val isNetworkConnectedFlow: StateFlow 19 | 20 | val isNetworkConnected: Boolean 21 | 22 | fun startListenNetworkState() 23 | 24 | fun stopListenNetworkState() 25 | } 26 | 27 | @Singleton 28 | class NetworkConnectionManagerImpl @Inject constructor( 29 | @ApplicationContext context: Context, 30 | coroutineScope: CoroutineScope 31 | ) : NetworkConnectionManager { 32 | 33 | private val connectivityManager: ConnectivityManager = context.getSystemService()!! 34 | 35 | private val networkCallback = NetworkCallback() 36 | 37 | private val _currentNetwork = MutableStateFlow(provideDefaultCurrentNetwork()) 38 | 39 | override val isNetworkConnectedFlow: StateFlow = 40 | _currentNetwork 41 | .map { it.isConnected() } 42 | .stateIn( 43 | scope = coroutineScope, 44 | started = SharingStarted.WhileSubscribed(), 45 | initialValue = _currentNetwork.value.isConnected() 46 | ) 47 | 48 | override val isNetworkConnected: Boolean 49 | get() = isNetworkConnectedFlow.value 50 | 51 | override fun startListenNetworkState() { 52 | if (_currentNetwork.value.isListening) { 53 | return 54 | } 55 | 56 | // Reset state before start listening 57 | _currentNetwork.update { 58 | provideDefaultCurrentNetwork() 59 | .copy(isListening = true) 60 | } 61 | 62 | connectivityManager.registerDefaultNetworkCallback(networkCallback) 63 | } 64 | 65 | override fun stopListenNetworkState() { 66 | if (!_currentNetwork.value.isListening) { 67 | return 68 | } 69 | 70 | _currentNetwork.update { 71 | it.copy(isListening = false) 72 | } 73 | 74 | connectivityManager.unregisterNetworkCallback(networkCallback) 75 | } 76 | 77 | private inner class NetworkCallback : ConnectivityManager.NetworkCallback() { 78 | override fun onAvailable(network: Network) { 79 | _currentNetwork.update { 80 | it.copy(isAvailable = true) 81 | } 82 | } 83 | 84 | override fun onLost(network: Network) { 85 | _currentNetwork.update { 86 | it.copy( 87 | isAvailable = false, 88 | networkCapabilities = null 89 | ) 90 | } 91 | } 92 | 93 | override fun onUnavailable() { 94 | _currentNetwork.update { 95 | it.copy( 96 | isAvailable = false, 97 | networkCapabilities = null 98 | ) 99 | } 100 | } 101 | 102 | override fun onCapabilitiesChanged( 103 | network: Network, 104 | networkCapabilities: NetworkCapabilities 105 | ) { 106 | _currentNetwork.update { 107 | it.copy(networkCapabilities = networkCapabilities) 108 | } 109 | } 110 | 111 | override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { 112 | _currentNetwork.update { 113 | it.copy(isBlocked = blocked) 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * On Android 9, [ConnectivityManager.NetworkCallback.onBlockedStatusChanged] is not called when 120 | * we call the [ConnectivityManager.registerDefaultNetworkCallback] function. 121 | * Hence we assume that the network is unblocked by default. 122 | */ 123 | private fun provideDefaultCurrentNetwork(): CurrentNetwork { 124 | return CurrentNetwork( 125 | isListening = false, 126 | networkCapabilities = null, 127 | isAvailable = false, 128 | isBlocked = false 129 | ) 130 | } 131 | 132 | private data class CurrentNetwork( 133 | val isListening: Boolean, 134 | val networkCapabilities: NetworkCapabilities?, 135 | val isAvailable: Boolean, 136 | val isBlocked: Boolean 137 | ) 138 | 139 | private fun CurrentNetwork.isConnected(): Boolean { 140 | // Since we don't know the network state if NetworkCallback is not registered. 141 | // We assume that it's disconnected. 142 | return isListening && 143 | isAvailable && 144 | !isBlocked && 145 | networkCapabilities.isNetworkCapabilitiesValid() 146 | } 147 | 148 | private fun NetworkCapabilities?.isNetworkCapabilitiesValid(): Boolean = when { 149 | this == null -> false 150 | hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && 151 | hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && 152 | (hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || 153 | hasTransport(NetworkCapabilities.TRANSPORT_VPN) || 154 | hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || 155 | hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) -> true 156 | else -> false 157 | } 158 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/vvinogra/internetconnectionmonitor/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.github.vvinogra.internetconnectionmonitor.di 2 | 3 | import com.github.vvinogra.internetconnectionmonitor.data.NetworkConnectionManager 4 | import com.github.vvinogra.internetconnectionmonitor.data.NetworkConnectionManagerImpl 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.SupervisorJob 13 | import javax.inject.Singleton 14 | 15 | @InstallIn(SingletonComponent::class) 16 | @Module 17 | abstract class AppModule { 18 | companion object { 19 | @Provides 20 | @Singleton 21 | fun provideCoroutineScope() = 22 | CoroutineScope(Dispatchers.Default + SupervisorJob()) 23 | } 24 | 25 | @Binds 26 | abstract fun bindNetworkConnectionManager(networkConnectionManagerImpl: NetworkConnectionManagerImpl): NetworkConnectionManager 27 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 |