Merge pull request #560 from vector-im/feature/no_network

Display a "No network" banner when the device has no network
This commit is contained in:
Benoit Marty 2019-09-17 14:40:42 +02:00 committed by GitHub
commit 4453f0ced9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 145 additions and 91 deletions

View file

@ -7,6 +7,7 @@ Features:
Improvements:
- Reduce default release build log level, and lab option to enable more logs.
- Display a no network indicator when there is no network (#559)
Other changes:
-

View file

@ -22,4 +22,5 @@ sealed class SyncState {
object PAUSED : SyncState()
object KILLING : SyncState()
object KILLED : SyncState()
object NO_NETWORK : SyncState()
}

View file

@ -18,12 +18,10 @@ package im.vector.matrix.android.internal.network
import android.content.Context
import com.novoda.merlin.Merlin
import com.novoda.merlin.MerlinsBeard
import im.vector.matrix.android.internal.di.MatrixScope
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@ -35,29 +33,38 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context)
.withDisconnectableCallbacks()
.build(context)
private val merlinsBeard = MerlinsBeard.Builder().build(context)
private val listeners = Collections.synchronizedList(ArrayList<Listener>())
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
// True when internet is available
var hasInternetAccess = false
private set
init {
merlin.bind()
merlin.registerDisconnectable {
Timber.v("On Disconnect")
val localListeners = listeners.toList()
localListeners.forEach {
it.onDisconnect()
if (hasInternetAccess) {
Timber.v("On Disconnect")
hasInternetAccess = false
val localListeners = listeners.toList()
localListeners.forEach {
it.onDisconnect()
}
}
}
merlin.registerConnectable {
Timber.v("On Connect")
val localListeners = listeners.toList()
localListeners.forEach {
it.onConnect()
if (!hasInternetAccess) {
Timber.v("On Connect")
hasInternetAccess = true
val localListeners = listeners.toList()
localListeners.forEach {
it.onConnect()
}
}
}
}
suspend fun waitUntilConnected() {
if (isConnected()) {
if (hasInternetAccess) {
return
} else {
suspendCoroutine<Unit> { continuation ->
@ -79,10 +86,6 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context)
listeners.remove(listener)
}
fun isConnected(): Boolean {
return merlinsBeard.isConnected
}
interface Listener {
fun onConnect() {

View file

@ -93,8 +93,8 @@ open class SyncService : Service() {
}
fun doSync(once: Boolean = false) {
if (!networkConnectivityChecker.isConnected()) {
Timber.v("Sync is Paused. Waiting...")
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No internet access. Waiting...")
//TODO Retry in ?
timer.schedule(object : TimerTask() {
override fun run() {

View file

@ -50,6 +50,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val lock = Object()
private var cancelableTask: Cancelable? = null
private var isStarted = false
init {
updateStateTo(SyncState.IDLE)
}
@ -60,19 +62,18 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
fun restart() = synchronized(lock) {
if (state is SyncState.PAUSED) {
if (!isStarted) {
Timber.v("Resume sync...")
updateStateTo(SyncState.RUNNING(afterPause = true))
isStarted = true
lock.notify()
}
}
fun pause() = synchronized(lock) {
if (state is SyncState.RUNNING) {
if (isStarted) {
Timber.v("Pause sync...")
updateStateTo(SyncState.PAUSED)
isStarted = false
cancelableTask?.cancel()
lock.notify()
}
}
@ -87,19 +88,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
return liveState
}
override fun onConnect() {
Timber.v("Network is back")
synchronized(lock) {
lock.notify()
}
}
override fun run() {
Timber.v("Start syncing...")
isStarted = true
networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this)
while (state != SyncState.KILLING) {
Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
Timber.v("No network or sync is Paused. Waiting...")
synchronized(lock) {
lock.wait()
}
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No network. Waiting...")
updateStateTo(SyncState.NO_NETWORK)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isStarted) {
Timber.v("Sync is Paused. Waiting...")
updateStateTo(SyncState.PAUSED)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else {
if (state !is SyncState.RUNNING) {
@ -167,16 +180,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
private fun updateStateTo(newState: SyncState) {
Timber.v("Update state to $newState")
Timber.v("Update state from $state to $newState")
state = newState
liveState.postValue(newState)
}
override fun onConnect() {
synchronized(lock) {
lock.notify()
}
}
override fun onMoveToForeground() {
restart()

View file

@ -33,7 +33,7 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
private set
private
val listeners = ArrayList<Listener>()
val listeners = LinkedHashSet<Listener>()
fun register(listener: Listener) {
listeners.add(listener)

View file

@ -3,4 +3,7 @@
<string name="no_network_indicator">There is no network connection right now</string>
</resources>

View file

@ -19,7 +19,6 @@ package im.vector.riotx.features.home
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.forEachIndexed
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
@ -30,7 +29,6 @@ import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.ToolbarConfigurable
@ -208,11 +206,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
syncProgressBar.visibility = when (it.syncState) {
is SyncState.RUNNING -> if (it.syncState.afterPause) View.VISIBLE else View.GONE
else -> View.GONE
}
syncProgressBarWrap.visibility = syncProgressBar.visibility
syncStateView.render(it.syncState)
}
companion object {

View file

@ -65,7 +65,6 @@ import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
@ -77,8 +76,8 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.*
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
@ -251,11 +250,7 @@ class RoomDetailFragment :
}
roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState ->
syncProgressBar.visibility = when (syncState) {
is SyncState.RUNNING -> if (syncState.afterPause) View.VISIBLE else View.GONE
else -> View.GONE
}
syncProgressBarWrap.visibility = syncProgressBar.visibility
syncStateView.render(syncState)
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.sync.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.core.view.isVisible
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.riotx.R
import kotlinx.android.synthetic.main.view_sync_state.view.*
class SyncStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
init {
View.inflate(context, R.layout.view_sync_state, this)
}
fun render(newState: SyncState) {
syncStateProgressBar.visibility = when (newState) {
is SyncState.RUNNING -> if (newState.afterPause) View.VISIBLE else View.GONE
else -> View.GONE
}
syncStateNoNetwork.isVisible = newState == SyncState.NO_NETWORK
}
}

View file

@ -44,29 +44,13 @@
</androidx.appcompat.widget.Toolbar>
<!-- Trick to remove surrounding padding (clip frome wrapping frame) -->
<FrameLayout
android:id="@+id/syncProgressBarWrap"
<im.vector.riotx.features.sync.widget.SyncStateView
android:id="@+id/syncStateView"
android:layout_width="match_parent"
android:layout_height="3dp"
android:visibility="gone"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/groupToolbar"
tools:visibility="visible">
<ProgressBar
android:id="@+id/syncProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:background="?riotx_header_panel_background"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
app:layout_constraintTop_toBottomOf="@id/groupToolbar" />
<im.vector.riotx.core.ui.views.KeysBackupBanner
android:id="@+id/homeKeysBackupBanner"
@ -76,7 +60,7 @@
android:minHeight="67dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap" />
app:layout_constraintTop_toBottomOf="@id/syncStateView" />
<FrameLayout
android:id="@+id/roomListContainer"

View file

@ -71,28 +71,13 @@
</androidx.appcompat.widget.Toolbar>
<!-- Trick to remove surrounding padding (clip frome wrapping frame) -->
<FrameLayout
android:id="@+id/syncProgressBarWrap"
<im.vector.riotx.features.sync.widget.SyncStateView
android:id="@+id/syncStateView"
android:layout_width="match_parent"
android:layout_height="3dp"
android:visibility="gone"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
tools:visibility="visible">
<ProgressBar
android:id="@+id/syncProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:background="?riotx_header_panel_background"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
app:layout_constraintTop_toBottomOf="@id/roomToolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recyclerView"
@ -102,7 +87,7 @@
app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap"
app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:listitem="@layout/item_timeline_event_base" />
<androidx.constraintlayout.widget.Barrier

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:orientation="vertical"
tools:parentTag="android.widget.LinearLayout">
<!-- Trick to remove surrounding padding (clip from wrapping frame) -->
<FrameLayout
android:id="@+id/syncStateProgressBar"
android:layout_width="match_parent"
android:layout_height="3dp"
android:visibility="gone"
tools:visibility="visible">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:background="?riotx_header_panel_background"
android:indeterminate="true" />
</FrameLayout>
<TextView
android:id="@+id/syncStateNoNetwork"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/vector_warning_color"
android:gravity="center"
android:text="@string/no_network_indicator"
android:textColor="@color/white"
android:visibility="gone"
tools:visibility="visible" />
</merge>