diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 23ec462..ab23cb3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,12 @@ + + + + + + diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/data/common/connection/ConnectionChecker.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/data/common/connection/ConnectionChecker.kt new file mode 100644 index 0000000..142d9e2 --- /dev/null +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/data/common/connection/ConnectionChecker.kt @@ -0,0 +1,38 @@ +package pro.averin.anton.clean.android.cookbook.data.common.connection + +import android.net.ConnectivityManager +import javax.inject.Inject +import javax.inject.Singleton + + +@Singleton +class ConnectionChecker @Inject constructor() { + + @Inject lateinit var connectivityManager: ConnectivityManager + + fun getNetworkStatus(): Int { + val networkInfo = connectivityManager.activeNetworkInfo + if (networkInfo != null && networkInfo.isConnectedOrConnecting) { + when (networkInfo.type) { + ConnectivityManager.TYPE_WIFI -> return WIFI + ConnectivityManager.TYPE_MOBILE -> return MOBILE + else -> return OFFLINE + } + } + + return OFFLINE; + } + + fun isOnline(): Boolean { + val activeNetwork = connectivityManager.activeNetworkInfo + + return activeNetwork != null && activeNetwork.isConnectedOrConnecting + } + + + companion object NetworkStatus { + val OFFLINE = 1 + val MOBILE = 2 + val WIFI = 3 + } +} \ No newline at end of file diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/di/AppComponent.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/di/AppComponent.kt index 347f293..99fa385 100644 --- a/app/src/main/java/pro/averin/anton/clean/android/cookbook/di/AppComponent.kt +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/di/AppComponent.kt @@ -1,11 +1,14 @@ package pro.averin.anton.clean.android.cookbook.di +import android.net.ConnectivityManager import com.tbruyelle.rxpermissions.RxPermissions import dagger.Component import pro.averin.anton.clean.android.cookbook.BaseContext +import pro.averin.anton.clean.android.cookbook.data.common.connection.ConnectionChecker import pro.averin.anton.clean.android.cookbook.data.common.rx.Schedulers import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.GlobalBusSubscriber import pro.averin.anton.clean.android.cookbook.data.flickr.FlickrRepo +import pro.averin.anton.clean.android.cookbook.ui.common.broadcasts.ConnectivityBroadcastReceiver import pro.averin.anton.clean.android.cookbook.ui.common.map.MapUtils import javax.inject.Singleton @@ -27,4 +30,8 @@ interface AppComponent { fun globalBusSubscriber(): GlobalBusSubscriber fun mapUtils(): MapUtils + + fun connectionChecker(): ConnectionChecker + fun connectivityManager(): ConnectivityManager + fun injectTo(connectivityBroadcastReceiver: ConnectivityBroadcastReceiver) } \ No newline at end of file diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/di/SystemServicesModule.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/di/SystemServicesModule.kt index 65f6dc6..1118094 100644 --- a/app/src/main/java/pro/averin/anton/clean/android/cookbook/di/SystemServicesModule.kt +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/di/SystemServicesModule.kt @@ -3,6 +3,7 @@ package pro.averin.anton.clean.android.cookbook.di import android.content.Context import android.content.SharedPreferences import android.location.LocationManager +import android.net.ConnectivityManager import android.preference.PreferenceManager import dagger.Module import dagger.Provides @@ -12,6 +13,12 @@ import javax.inject.Singleton @Module class SystemServicesModule(private val baseContext: BaseContext) { + @Provides + @Singleton + fun connectivityManager(): ConnectivityManager { + return baseContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + } + @Provides @Singleton fun locationManager(): LocationManager { diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/ConnectionEventsHandler.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/ConnectionEventsHandler.kt new file mode 100644 index 0000000..ebe1761 --- /dev/null +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/ConnectionEventsHandler.kt @@ -0,0 +1,37 @@ +package pro.averin.anton.clean.android.cookbook.ui.common + +import pro.averin.anton.clean.android.cookbook.data.common.connection.ConnectionChecker +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.GlobalBusSubscriber +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.events.ConnectivityAvailableGlobalEvent +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.events.ConnectivityUnavailableGlobalEvent +import pro.averin.anton.clean.android.cookbook.di.ActivityScope +import pro.averin.anton.clean.android.cookbook.ui.common.resolution.Resolution +import javax.inject.Inject + +@ActivityScope +class ConnectionEventsHandler @Inject constructor( + private val globalBusSubscriber: GlobalBusSubscriber, + private val connectionChecker: ConnectionChecker +) { + + fun start(resolution: Resolution) { + globalBusSubscriber.subscribe(ConnectivityAvailableGlobalEvent::class) { + resolution.onConnectivityAvailable() + } + globalBusSubscriber.subscribe(ConnectivityUnavailableGlobalEvent::class) { + resolution.onConnectivityUnavailable() + } + + if (!connectionChecker.isOnline()) { + resolution.onConnectivityUnavailable() + } else { + resolution.onConnectivityAvailable() + } + + } + + fun stop() { + globalBusSubscriber.unsubscribe() + } + +} diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/broadcasts/ConnectivityBroadcastReceiver.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/broadcasts/ConnectivityBroadcastReceiver.kt new file mode 100644 index 0000000..34bdc61 --- /dev/null +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/common/broadcasts/ConnectivityBroadcastReceiver.kt @@ -0,0 +1,28 @@ +package pro.averin.anton.clean.android.cookbook.ui.common.broadcasts + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import pro.averin.anton.clean.android.cookbook.BaseContext +import pro.averin.anton.clean.android.cookbook.data.common.connection.ConnectionChecker +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.GlobalBus +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.events.ConnectivityAvailableGlobalEvent +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.events.ConnectivityUnavailableGlobalEvent +import javax.inject.Inject + +class ConnectivityBroadcastReceiver : BroadcastReceiver() { + + @Inject lateinit var connectionChecker: ConnectionChecker + @Inject lateinit var globalBus: GlobalBus + + override fun onReceive(context: Context?, intent: Intent?) { + val baseContext = context?.applicationContext as BaseContext + baseContext.appComponent.injectTo(this) + + if (connectionChecker.isOnline()) { + globalBus.post(ConnectivityAvailableGlobalEvent()) + } else { + globalBus.post(ConnectivityUnavailableGlobalEvent()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/presenter/MainPresenter.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/presenter/MainPresenter.kt index 5f16403..aacd45b 100644 --- a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/presenter/MainPresenter.kt +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/presenter/MainPresenter.kt @@ -1,6 +1,8 @@ package pro.averin.anton.clean.android.cookbook.ui.main.presenter +import android.os.Bundle import pro.averin.anton.clean.android.cookbook.di.ActivityScope +import pro.averin.anton.clean.android.cookbook.ui.common.ConnectionEventsHandler import pro.averin.anton.clean.android.cookbook.ui.common.ExtraLifecycleDelegate import pro.averin.anton.clean.android.cookbook.ui.common.UINavigator import pro.averin.anton.clean.android.cookbook.ui.common.presenter.BasePresenter @@ -11,12 +13,23 @@ import javax.inject.Inject @ActivityScope class MainPresenter @Inject constructor( + private val connectionEventsHandler: ConnectionEventsHandler ) : BasePresenter(), ExtraLifecycleDelegate, NavigationDrawerViewExtensionDelegate { @Inject lateinit var uiNavigator: UINavigator lateinit var navigationDrawerViewExtension: NavigationDrawerViewExtensionContract + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + connectionEventsHandler.start(view?.getResolution()!!) + } + + override fun onDestroy() { + super.onDestroy() + connectionEventsHandler.stop() + } + override fun showInitialScreen() { uiNavigator.showGoogleMapsScreen() } diff --git a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/view/MainActivity.kt b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/view/MainActivity.kt index 53a4977..ea36e32 100644 --- a/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/view/MainActivity.kt +++ b/app/src/main/java/pro/averin/anton/clean/android/cookbook/ui/main/view/MainActivity.kt @@ -5,16 +5,19 @@ import android.support.v7.widget.Toolbar import pro.averin.anton.clean.android.cookbook.R import pro.averin.anton.clean.android.cookbook.databinding.ActivityMainBinding import pro.averin.anton.clean.android.cookbook.di.ActivityComponent +import pro.averin.anton.clean.android.cookbook.ui.common.resolution.Resolution +import pro.averin.anton.clean.android.cookbook.ui.common.resolution.UIResolution import pro.averin.anton.clean.android.cookbook.ui.common.view.BaseActivity -import pro.averin.anton.clean.android.cookbook.ui.common.view.ScreenContract +import pro.averin.anton.clean.android.cookbook.ui.common.view.ResolvedScreenContract import pro.averin.anton.clean.android.cookbook.ui.main.presenter.MainPresenter import javax.inject.Inject -interface MainScreenContract : ScreenContract +interface MainScreenContract : ResolvedScreenContract class MainActivity : BaseActivity(), MainScreenContract { @Inject lateinit var presenter: MainPresenter + @Inject lateinit var uiResolution: UIResolution @Inject lateinit var navigationDrawerViewExtension: NavigationDrawerViewExtension override fun doInjections(activityComponent: ActivityComponent) { @@ -41,4 +44,7 @@ class MainActivity : BaseActivity(), MainScreenContract { } } + override fun getResolution(): Resolution? { + return uiResolution + } } \ No newline at end of file diff --git a/app/src/test/java/pro/averin/anton/clean/android/cookbook/data/common/connection/ConnectionCheckerTest.kt b/app/src/test/java/pro/averin/anton/clean/android/cookbook/data/common/connection/ConnectionCheckerTest.kt new file mode 100644 index 0000000..63a7db8 --- /dev/null +++ b/app/src/test/java/pro/averin/anton/clean/android/cookbook/data/common/connection/ConnectionCheckerTest.kt @@ -0,0 +1,102 @@ +package pro.averin.anton.clean.android.cookbook.data.common.connection + +import android.net.ConnectivityManager +import android.net.NetworkInfo +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.powermock.modules.junit4.PowerMockRunner +import pro.averin.anton.clean.android.cookbook.kotlin.test.assertThat +import pro.averin.anton.clean.android.cookbook.kotlin.test.given +import pro.averin.anton.clean.android.cookbook.kotlin.test.isEqualTo + +@RunWith(PowerMockRunner::class) +class ConnectionCheckerTest { + + @Mock lateinit var connectivityManager: ConnectivityManager + @Mock lateinit var activeNetworkInfo: NetworkInfo + + @InjectMocks lateinit var classToTest: ConnectionChecker + + @Test + fun networkStatusOfflineWhenNoNetworksConnected() { + // given + given(connectivityManager.activeNetworkInfo).willReturn(null) + + // when + val status = classToTest.getNetworkStatus() + + // then + assertThat(status, isEqualTo(ConnectionChecker.OFFLINE)) + } + + @Test + fun networkStatusOfflineWhenNotWifiOrMobileNetworkConnected() { + // given + given(connectivityManager.activeNetworkInfo).willReturn(activeNetworkInfo) + given(activeNetworkInfo.isConnectedOrConnecting).willReturn(true) + given(activeNetworkInfo.type).willReturn(ConnectivityManager.TYPE_DUMMY) + + // when + val status = classToTest.getNetworkStatus() + + // then + assertThat(status, isEqualTo(ConnectionChecker.OFFLINE)) + } + + @Test + fun networkStatusWifiWhenWifiConnected() { + // given + given(connectivityManager.activeNetworkInfo).willReturn(activeNetworkInfo) + given(activeNetworkInfo.type).willReturn(ConnectivityManager.TYPE_WIFI) + given(activeNetworkInfo.isConnectedOrConnecting).willReturn(true) + + // when + val status = classToTest.getNetworkStatus() + + // then + assertThat(status, isEqualTo(ConnectionChecker.WIFI)) + } + + @Test + fun offlineWhenActiveNetworkNotConnected() { + // given + given(connectivityManager.activeNetworkInfo).willReturn(activeNetworkInfo) + given(activeNetworkInfo.isConnectedOrConnecting).willReturn(false) + + // when + val result = classToTest.isOnline() + + // then + assertThat(result, isEqualTo(false)) + } + + + @Test + fun onlineWhenActiveNetworkConnected() { + // given + given(connectivityManager.activeNetworkInfo).willReturn(activeNetworkInfo) + given(activeNetworkInfo.isConnectedOrConnecting).willReturn(true) + + // when + val result = classToTest.isOnline() + + // then + assertThat(result, isEqualTo(true)) + } + + @Test + fun networkStatusMobileWhenMobileConnected() { + // given + given(connectivityManager.activeNetworkInfo).willReturn(activeNetworkInfo) + given(activeNetworkInfo.type).willReturn(ConnectivityManager.TYPE_MOBILE) + given(activeNetworkInfo.isConnectedOrConnecting).willReturn(true) + + // when + val status = classToTest.getNetworkStatus() + + // then + assertThat(status, isEqualTo(ConnectionChecker.MOBILE)) + } +} \ No newline at end of file diff --git a/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/ConnectionEventsHandlerTest.kt b/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/ConnectionEventsHandlerTest.kt new file mode 100644 index 0000000..57e375b --- /dev/null +++ b/app/src/test/java/pro/averin/anton/clean/android/cookbook/ui/common/ConnectionEventsHandlerTest.kt @@ -0,0 +1,124 @@ +package pro.averin.anton.clean.android.cookbook.ui.common + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner +import pro.averin.anton.clean.android.cookbook.data.common.connection.ConnectionChecker +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.GlobalBus +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.GlobalBusSubscriber +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.events.ConnectivityAvailableGlobalEvent +import pro.averin.anton.clean.android.cookbook.data.common.rx.bus.events.ConnectivityUnavailableGlobalEvent +import pro.averin.anton.clean.android.cookbook.kotlin.test.* +import pro.averin.anton.clean.android.cookbook.ui.common.resolution.Resolution + +@RunWith(PowerMockRunner::class) +@PrepareForTest( + ConnectionChecker::class +) +class ConnectionEventsHandlerTest { + + @Mock private lateinit var connectionChecker: ConnectionChecker + @Mock private lateinit var resolution: Resolution + + private val globalBus = GlobalBus() + private val globalBusSubscriber = GlobalBusSubscriber(globalBus) + + private lateinit var classToTest: ConnectionEventsHandler + + @Before + fun setup() { + classToTest = ConnectionEventsHandler( + globalBusSubscriber, + connectionChecker + ) + } + + @Test + fun startSubscribes2Events() { + // when + classToTest.start(resolution) + + // then + assertThat(globalBusSubscriber.subscriptions.size, isEqualTo(2)) + } + + @Test + fun startResolvesConnectivityAvailableOnStart() { + // given + given(connectionChecker.isOnline()).willReturn(true) + + // when + classToTest.start(resolution) + + // then + verify(resolution).onConnectivityAvailable() + } + + @Test + fun startResolvesConnectivityUnavailableOnStart() { + // given + given(connectionChecker.isOnline()).willReturn(false) + + // when + classToTest.start(resolution) + + // then + verify(resolution).onConnectivityUnavailable() + } + + @Test + fun connectivityAvailableEventWhenStoppedDoesNothing() { + // when + globalBus.post(ConnectivityAvailableGlobalEvent()) + + // then + verify(resolution, never()).onConnectivityAvailable() + } + + @Test + fun connectivityUnavailableEventWhenStoppedDoesNothing() { + // when + globalBus.post(ConnectivityUnavailableGlobalEvent()) + + // then + verify(resolution, never()).onConnectivityUnavailable() + } + + @Test + fun connectivityAvailableEventWhenStartedResolved() { + // given + classToTest.start(resolution) + + // when + globalBus.post(ConnectivityAvailableGlobalEvent()) + + // then + verify(resolution).onConnectivityAvailable() + } + + @Test + fun connectivityUnavailableEventWhenStartedResolved() { + // given + classToTest.start(resolution) + + // when + reset(resolution) + globalBus.post(ConnectivityUnavailableGlobalEvent()) + + // then + verify(resolution).onConnectivityUnavailable() + } + + @Test + fun stopUnsubscribesEvents() { + // when + classToTest.stop() + + // then + assertThat(globalBusSubscriber.subscriptions.size, isEqualTo(0)) + } + +} \ No newline at end of file