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