Sync: add progress indicator for sync, need UI inputs.

This commit is contained in:
ganfra 2019-06-11 16:54:44 +02:00
parent 6323183119
commit 3dd161d65a
9 changed files with 123 additions and 39 deletions

View File

@ -19,6 +19,7 @@ package im.vector.matrix.rx
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.sync.SyncState
import io.reactivex.Observable import io.reactivex.Observable


class RxSession(private val session: Session) { class RxSession(private val session: Session) {
@ -31,6 +32,10 @@ class RxSession(private val session: Session) {
return session.liveGroupSummaries().asObservable() return session.liveGroupSummaries().asObservable()
} }


fun liveSyncState(): Observable<SyncState> {
return session.syncState().asObservable()
}

} }


fun Session.rx(): RxSession { fun Session.rx(): RxSession {

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session package im.vector.matrix.android.api.session


import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@ -27,6 +28,7 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService


/** /**
@ -66,6 +68,12 @@ interface Session :
@MainThread @MainThread
fun stopSync() fun stopSync()


/**
* This method allows to listen the sync state.
* @return a [LiveData] of [SyncState].
*/
fun syncState(): LiveData<SyncState>

/** /**
* This method allow to close a session. It does stop some services. * This method allow to close a session. It does stop some services.
*/ */
@ -102,6 +110,7 @@ interface Session :
* The access token is not valid anymore * The access token is not valid anymore
*/ */
fun onInvalidToken() fun onInvalidToken()

} }


} }

View File

@ -0,0 +1,27 @@
/*
*
* * 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.matrix.android.api.session.sync

sealed class SyncState {
object IDLE : SyncState()
data class RUNNING(val catchingUp: Boolean) : SyncState()
object PAUSED : SyncState()
object KILLING : SyncState()
object KILLED : SyncState()
}

View File

@ -39,6 +39,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
@ -124,6 +125,10 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
isOpen = false isOpen = false
} }


override fun syncState(): LiveData<SyncState> {
return syncThread.liveState()
}

@MainThread @MainThread
override fun signOut(callback: MatrixCallback<Unit>) { override fun signOut(callback: MatrixCallback<Unit>) {
assert(isOpen) assert(isOpen)

View File

@ -16,10 +16,13 @@


package im.vector.matrix.android.internal.session.sync.job package im.vector.matrix.android.internal.session.sync.job


import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.session.sync.SyncTask
@ -42,61 +45,52 @@ internal class SyncThread(private val syncTask: SyncTask,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { ) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {


enum class State { private var state: SyncState = SyncState.IDLE
IDLE, private var liveState = MutableLiveData<SyncState>()
RUNNING,
PAUSED,
KILLING,
KILLED
}

private var state: State = State.IDLE
private val lock = Object() private val lock = Object()
private var nextBatch = syncTokenStore.getLastToken() private var nextBatch = syncTokenStore.getLastToken()
private var cancelableTask: Cancelable? = null private var cancelableTask: Cancelable? = null


fun restart() { init {
synchronized(lock) { updateStateTo(SyncState.IDLE)
if (state != State.PAUSED) {
return@synchronized
} }
Timber.v("Resume sync...")


fun restart() = synchronized(lock) {
if (state is SyncState.PAUSED) {
Timber.v("Resume sync...")
// Retrieve the last token, it may have been deleted in case of a clear cache // Retrieve the last token, it may have been deleted in case of a clear cache
nextBatch = syncTokenStore.getLastToken() nextBatch = syncTokenStore.getLastToken()

updateStateTo(SyncState.RUNNING(catchingUp = true))
state = State.RUNNING
lock.notify() lock.notify()
} }
} }


fun pause() { fun pause() = synchronized(lock) {
synchronized(lock) { if (state is SyncState.RUNNING) {
if (state != State.RUNNING) {
return@synchronized
}
Timber.v("Pause sync...") Timber.v("Pause sync...")
state = State.PAUSED updateStateTo(SyncState.PAUSED)
} }
} }


fun kill() { fun kill() = synchronized(lock) {
synchronized(lock) {
Timber.v("Kill sync...") Timber.v("Kill sync...")
state = State.KILLING updateStateTo(SyncState.KILLING)
cancelableTask?.cancel() cancelableTask?.cancel()
lock.notify() lock.notify()
} }
}


fun liveState(): LiveData<SyncState> {
return liveState
}


override fun run() { override fun run() {
Timber.v("Start syncing...") Timber.v("Start syncing...")
networkConnectivityChecker.register(this) networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this) backgroundDetectionObserver.register(this)
state = State.RUNNING updateStateTo(SyncState.RUNNING(catchingUp = true))
while (state != State.KILLING) {
if (!networkConnectivityChecker.isConnected() || state == State.PAUSED) { while (state != SyncState.KILLING) {
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
Timber.v("Waiting...") Timber.v("Waiting...")
synchronized(lock) { synchronized(lock) {
lock.wait() lock.wait()
@ -133,7 +127,7 @@ internal class SyncThread(private val syncTask: SyncTask,
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// No token or invalid token, stop the thread // No token or invalid token, stop the thread
state = State.KILLING updateStateTo(SyncState.KILLING)
} }


latch.countDown() latch.countDown()
@ -141,16 +135,23 @@ internal class SyncThread(private val syncTask: SyncTask,


}) })
.executeBy(taskExecutor) .executeBy(taskExecutor)

latch.await() latch.await()
if (state is SyncState.RUNNING) {
updateStateTo(SyncState.RUNNING(catchingUp = false))
}
} }
} }
Timber.v("Sync killed") Timber.v("Sync killed")
state = State.KILLED updateStateTo(SyncState.KILLED)
backgroundDetectionObserver.unregister(this) backgroundDetectionObserver.unregister(this)
networkConnectivityChecker.unregister(this) networkConnectivityChecker.unregister(this)
} }


private fun updateStateTo(newState: SyncState) {
state = newState
liveState.postValue(newState)
}

override fun onConnect() { override fun onConnect() {
synchronized(lock) { synchronized(lock) {
lock.notify() lock.notify()

View File

@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import androidx.core.view.forEachIndexed import androidx.core.view.forEachIndexed
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
@ -26,6 +27,7 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
@ -145,6 +147,10 @@ class HomeDetailFragment : VectorBaseFragment() {
unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup)) unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople)) unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
syncProgressBar.visibility = when (it.syncState) {
is SyncState.RUNNING -> if (it.syncState.catchingUp) View.VISIBLE else View.GONE
else -> View.GONE
}
} }


companion object { companion object {

View File

@ -18,6 +18,8 @@ package im.vector.riotredesign.features.home


import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.platform.VectorViewModel
import org.koin.android.ext.android.get import org.koin.android.ext.android.get


@ -25,6 +27,7 @@ import org.koin.android.ext.android.get
* View model used to update the home bottom bar notification counts * View model used to update the home bottom bar notification counts
*/ */
class HomeDetailViewModel(initialState: HomeDetailViewState, class HomeDetailViewModel(initialState: HomeDetailViewState,
private val session: Session,
private val homeRoomListStore: HomeRoomListObservableStore) private val homeRoomListStore: HomeRoomListObservableStore)
: VectorViewModel<HomeDetailViewState>(initialState) { : VectorViewModel<HomeDetailViewState>(initialState) {


@ -33,16 +36,29 @@ class HomeDetailViewModel(initialState: HomeDetailViewState,
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? { override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? {
val homeRoomListStore = viewModelContext.activity.get<HomeRoomListObservableStore>() val homeRoomListStore = viewModelContext.activity.get<HomeRoomListObservableStore>()
return HomeDetailViewModel(state, homeRoomListStore) val session = viewModelContext.activity.get<Session>()
return HomeDetailViewModel(state, session, homeRoomListStore)
} }
} }


init { init {
observeSyncState()
observeRoomSummaries() observeRoomSummaries()
} }


// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************


private fun observeSyncState() {
session.rx()
.liveSyncState()
.subscribe { syncState ->
setState {
copy(syncState = syncState)
}
}
.disposeOnClear()
}

private fun observeRoomSummaries() { private fun observeRoomSummaries() {
homeRoomListStore homeRoomListStore
.observe() .observe()

View File

@ -17,6 +17,7 @@
package im.vector.riotredesign.features.home package im.vector.riotredesign.features.home


import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import im.vector.matrix.android.api.session.sync.SyncState


data class HomeDetailViewState( data class HomeDetailViewState(
val notificationCountCatchup: Int = 0, val notificationCountCatchup: Int = 0,
@ -24,5 +25,6 @@ data class HomeDetailViewState(
val notificationCountPeople: Int = 0, val notificationCountPeople: Int = 0,
val notificationHighlightPeople: Boolean = false, val notificationHighlightPeople: Boolean = false,
val notificationCountRooms: Int = 0, val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false val notificationHighlightRooms: Boolean = false,
val syncState: SyncState = SyncState.IDLE
) : MvRxState ) : MvRxState

View File

@ -44,13 +44,26 @@


</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>


<ProgressBar
android:id="@+id/syncProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
android:background="?riotx_header_panel_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/groupToolbar"
tools:visibility="visible" />

<FrameLayout <FrameLayout
android:id="@+id/roomListContainer" android:id="@+id/roomListContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:background="?riotx_header_panel_background" android:background="?riotx_header_panel_background"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView" app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintTop_toBottomOf="@+id/groupToolbar" /> app:layout_constraintTop_toBottomOf="@+id/syncProgressBar" />


<com.google.android.material.bottomnavigation.BottomNavigationView <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView" android:id="@+id/bottomNavigationView"