diff --git a/fe2-android/app/src/main/AndroidManifest.xml b/fe2-android/app/src/main/AndroidManifest.xml index 1308555139..522f582dc8 100644 --- a/fe2-android/app/src/main/AndroidManifest.xml +++ b/fe2-android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + { + return Observable.create { emitter -> + val callback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: android.net.Network) { + emitter.onNext(true) + } + + override fun onLost(network: android.net.Network) { + emitter.onNext(false) + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + + emitter.setCancellable { connectivityManager.unregisterNetworkCallback(callback) } + } + .subscribeOn(Schedulers.io()) + } +} diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/NetworkStatusView.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/NetworkStatusView.kt new file mode 100644 index 0000000000..39a3e2c808 --- /dev/null +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/NetworkStatusView.kt @@ -0,0 +1,42 @@ +package com.github.dedis.popstellar.ui + +import android.content.Context +import android.transition.Slide +import android.transition.TransitionManager +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.view.isVisible +import com.github.dedis.popstellar.databinding.NetworkStatusBinding + +class NetworkStatusView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + + private val binding = NetworkStatusBinding.inflate(LayoutInflater.from(context), this, true) + + fun setIsNetworkConnected(isVisible: Boolean) { + setVisibility(isVisible) + } + + private fun setVisibility(isVisible: Boolean) { + TransitionManager.beginDelayedTransition( + this.parent as ViewGroup, + Slide(Gravity.TOP).apply { + addTarget(binding.networkConnectionContainer) + duration = VISIBILITY_CHANGE_DURATION + }) + binding.networkConnectionContainer.isVisible = !isVisible + } + + companion object { + private const val VISIBILITY_CHANGE_DURATION = 400L + } +} diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeActivity.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeActivity.kt index b57a7b4163..c455db6d79 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeActivity.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeActivity.kt @@ -51,8 +51,12 @@ class HomeActivity : AppCompatActivity() { // When back to the home activity set connecting in view model to false viewModel.disableConnectingFlag() + viewModel.observeInternetConnection() + handleTopAppBar() + observeInternetConnection() + // Load all the json schemas in background when the app is started. GlobalScope.launch { loadSchema(JsonUtils.ROOT_SCHEMA) @@ -77,6 +81,12 @@ class HomeActivity : AppCompatActivity() { } } + private fun observeInternetConnection() { + viewModel.isInternetConnected.observe(this) { + binding.networkStatusView.setIsNetworkConnected(it) + } + } + private fun handleTopAppBar() { viewModel.pageTitle.observe(this) { resId: Int -> binding.topAppBar.setTitle(resId) } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeViewModel.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeViewModel.kt index cfd35dbf61..f8150f5ef0 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeViewModel.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/home/HomeViewModel.kt @@ -11,10 +11,12 @@ import com.github.dedis.popstellar.model.objects.Wallet import com.github.dedis.popstellar.model.objects.view.LaoView import com.github.dedis.popstellar.model.qrcode.ConnectToLao import com.github.dedis.popstellar.model.qrcode.ConnectToLao.Companion.extractFrom +import com.github.dedis.popstellar.repository.ConnectivityRepository import com.github.dedis.popstellar.repository.LAORepository import com.github.dedis.popstellar.repository.database.AppDatabase import com.github.dedis.popstellar.repository.remote.GlobalNetworkManager import com.github.dedis.popstellar.ui.PopViewModel +import com.github.dedis.popstellar.ui.lao.LaoViewModel import com.github.dedis.popstellar.ui.qrcode.QRCodeScanningViewModel import com.github.dedis.popstellar.utility.ActivityUtils.saveWalletRoutine import com.github.dedis.popstellar.utility.error.ErrorUtils.logAndShow @@ -24,6 +26,7 @@ import com.google.gson.Gson import com.google.gson.JsonParseException import dagger.hilt.android.lifecycle.HiltViewModel import io.reactivex.BackpressureStrategy +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import java.security.GeneralSecurityException @@ -42,6 +45,7 @@ constructor( private val wallet: Wallet, private val laoRepository: LAORepository, private val networkManager: GlobalNetworkManager, + private val connectivityRepository: ConnectivityRepository, private val appDatabase: AppDatabase ) : AndroidViewModel(application), QRCodeScanningViewModel, PopViewModel { /** LiveData objects that represent the state in a fragment */ @@ -56,6 +60,8 @@ constructor( /** This LiveData boolean is used to indicate whether the HomeFragment is displayed */ val isHome = MutableLiveData(java.lang.Boolean.TRUE) + val isInternetConnected = MutableLiveData(java.lang.Boolean.TRUE) + val isWitnessingEnabled = MutableLiveData(java.lang.Boolean.FALSE) /** @@ -190,6 +196,18 @@ constructor( } } + fun observeInternetConnection() { + addDisposable( + connectivityRepository + .observeConnectivity() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { isConnected -> isInternetConnected.value = isConnected }, + { error: Throwable -> + Timber.tag(LaoViewModel.TAG).e(error, "error connection status") + })) + } + /** * Function to set the liveData isWitnessingEnabled. * diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoActivity.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoActivity.kt index 9e309ef927..13052b413c 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoActivity.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoActivity.kt @@ -69,6 +69,7 @@ class LaoActivity : AppCompatActivity() { laoViewModel.laoId = laoId laoViewModel.observeLao(laoId) laoViewModel.observeRollCalls(laoId) + laoViewModel.observeInternetConnection() witnessingViewModel = obtainWitnessingViewModel(this, laoId) @@ -78,6 +79,7 @@ class LaoActivity : AppCompatActivity() { observeRoles() observeToolBar() + observeInternetConnection() observeDrawer() setupDrawerHeader() observeWitnessPopup() @@ -110,6 +112,12 @@ class LaoActivity : AppCompatActivity() { laoViewModel.role.observe(this) { role: Role -> setupHeaderRole(role) } } + private fun observeInternetConnection() { + laoViewModel.isInternetConnected.observe(this) { + binding.networkStatusView.setIsNetworkConnected(it) + } + } + private fun observeToolBar() { // Listen to click on left icon of toolbar binding.laoAppBar.setNavigationOnClickListener { diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoViewModel.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoViewModel.kt index 42b2b03fb8..112a9f6289 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoViewModel.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/LaoViewModel.kt @@ -11,6 +11,7 @@ import com.github.dedis.popstellar.model.objects.Wallet import com.github.dedis.popstellar.model.objects.security.PoPToken import com.github.dedis.popstellar.model.objects.security.PublicKey import com.github.dedis.popstellar.model.objects.view.LaoView +import com.github.dedis.popstellar.repository.ConnectivityRepository import com.github.dedis.popstellar.repository.LAORepository import com.github.dedis.popstellar.repository.RollCallRepository import com.github.dedis.popstellar.repository.WitnessingRepository @@ -32,6 +33,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import javax.inject.Inject +import kotlinx.coroutines.flow.observeOn import timber.log.Timber @HiltViewModel @@ -46,6 +48,7 @@ constructor( private val laoRepo: LAORepository, private val rollCallRepo: RollCallRepository, private val witnessingRepo: WitnessingRepository, + private val connectivityRepository: ConnectivityRepository, private val networkManager: GlobalNetworkManager, private val keyManager: KeyManager, private val wallet: Wallet, @@ -65,6 +68,8 @@ constructor( val isAttendee = MutableLiveData(java.lang.Boolean.FALSE) val role = MutableLiveData(Role.MEMBER) + val isInternetConnected = MutableLiveData(java.lang.Boolean.TRUE) + private val disposables = CompositeDisposable() private val subscriptionsDao: SubscriptionsDao = appDatabase.subscriptionsDao() @@ -219,6 +224,16 @@ constructor( })) } + fun observeInternetConnection() { + addDisposable( + connectivityRepository + .observeConnectivity() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { isConnected -> isInternetConnected.value = isConnected }, + { error: Throwable -> Timber.tag(TAG).e(error, "error connection status") })) + } + private fun isRollCallAttended(rollcall: RollCall, laoId: String): Boolean { return try { val pk = wallet.generatePoPToken(laoId, rollcall.persistentId).publicKey diff --git a/fe2-android/app/src/main/res/layout/home_activity.xml b/fe2-android/app/src/main/res/layout/home_activity.xml index 7f16c9ed1c..1cb3a53362 100644 --- a/fe2-android/app/src/main/res/layout/home_activity.xml +++ b/fe2-android/app/src/main/res/layout/home_activity.xml @@ -22,6 +22,13 @@ app:navigationIcon="@drawable/home_icon" /> + + + + + + + + + + + + + diff --git a/fe2-android/app/src/main/res/values/strings.xml b/fe2-android/app/src/main/res/values/strings.xml index 421c34beea..bfd2091f7e 100644 --- a/fe2-android/app/src/main/res/values/strings.xml +++ b/fe2-android/app/src/main/res/values/strings.xml @@ -375,4 +375,6 @@ Invalid QRCode laoData Invalid URL + No internet connection + diff --git a/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/repository/ConnectivityRepositoryTest.kt b/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/repository/ConnectivityRepositoryTest.kt new file mode 100644 index 0000000000..3bde2b5736 --- /dev/null +++ b/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/repository/ConnectivityRepositoryTest.kt @@ -0,0 +1,88 @@ +package com.github.dedis.popstellar.repository + +import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import io.reactivex.Observable +import io.reactivex.observers.TestObserver +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.* +import org.mockito.Mockito.* +import org.mockito.junit.MockitoJUnitRunner + + +@RunWith(MockitoJUnitRunner::class) +class ConnectivityRepositoryTest { + @Mock + private lateinit var application: Application + + @Mock + private lateinit var context: Context + + @Mock + lateinit var connectivityManager: ConnectivityManager + + private lateinit var connectivityRepository: ConnectivityRepository + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + `when`(application.applicationContext).thenReturn(context) + `when`(context.getSystemService(Context.CONNECTIVITY_SERVICE)) + .thenReturn(connectivityManager) + connectivityRepository = ConnectivityRepository(application) + } + + @Test + fun testObserveConnectivity_onAvailable() { + val callbackCaptor = ArgumentCaptor.forClass( + NetworkCallback::class.java + ) + + val observable: Observable = connectivityRepository.observeConnectivity() + val testObserver: TestObserver = observable.test() + + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + val callback = callbackCaptor.value + + callback.onAvailable(mock(Network::class.java)) + testObserver.assertValue(true) + } + + @Test + fun testObserveConnectivity_onLost() { + val callbackCaptor = ArgumentCaptor.forClass( + NetworkCallback::class.java + ) + + val observable = connectivityRepository.observeConnectivity() + val testObserver = observable.test() + + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + val callback = callbackCaptor.value + + callback.onLost(mock(Network::class.java)) + testObserver.assertValue(false) + } + + @Test + fun testObserveConnectivity_cancellable() { + val callbackCaptor = ArgumentCaptor.forClass( + NetworkCallback::class.java + ) + + val observable = connectivityRepository.observeConnectivity() + val testObserver = observable.test() + + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + val callback = callbackCaptor.value + + testObserver.dispose() + verify(connectivityManager).unregisterNetworkCallback(callback) + } +} \ No newline at end of file