Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7e0e43d
Add hooks for when to auto-upload local drafts
shiki Apr 29, 2019
f8dd996
Make PostListViewModel inherit from ScopedVM
shiki Apr 29, 2019
023c1ee
Auto upload local drafts in Posts list
shiki Apr 29, 2019
b612b2e
LocalDraftUploadStarter: Use FluxC call
shiki Apr 30, 2019
b88021d
Remove @Singleton from LocalDraftUploadStarter
shiki May 1, 2019
9135289
LocalDraftUploadStarter: Add network check
shiki May 1, 2019
d6f3de3
Add PostListViewModelTest for swipe to refresh
shiki May 1, 2019
d2218c9
Convert ConnectionStatus to data class
shiki May 1, 2019
a2432d2
PostListMainVM: Move connection status to start
shiki May 1, 2019
1ae0614
Make PostListMainViewModel useable in unit tests
shiki May 1, 2019
e8f0a79
Add PostListMainViewModelTest
shiki May 1, 2019
5e47049
Rename mocked* methods to create*
shiki May 1, 2019
b454336
Update FluxC to b6f8aa3
shiki May 1, 2019
9e73e5f
Update release notes (12.5)
shiki May 1, 2019
514ce10
LocalDraftUploadStarter: Remove unused import
shiki May 1, 2019
bcff503
LocalDraftUploadStarter: Use retry when uploading
shiki May 6, 2019
74b26b0
Update FluxC to 5aad0a2
shiki May 8, 2019
e26e540
Make PostListEventListener injectable
shiki May 8, 2019
56fb755
Merge branch 'issue/9568-connectionstatuslivedata-revamp' into issue/…
shiki May 10, 2019
f44e29e
Fix PostListMainViewModelTest
shiki May 10, 2019
fa94f00
LocalDraftUploader: Filter posts already in queue
shiki May 10, 2019
5aadbd9
Merge branch 'develop' into issue/9568-upload-local-drafts
shiki May 13, 2019
2e106cd
Merge develop into issue/9568-upload-local-drafts
shiki May 14, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
12.5
-----
* Fixed local drafts not automatically pushed to the server.

12.4
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,13 @@ import org.wordpress.android.ui.uploads.UploadService
import org.wordpress.android.ui.uploads.VideoOptimizer
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T

fun listenForPostListEvents(
lifecycle: Lifecycle,
dispatcher: Dispatcher,
postStore: PostStore,
site: SiteModel,
postActionHandler: PostActionHandler,
handlePostUpdatedWithoutError: () -> Unit,
handlePostUploadedWithoutError: (LocalId) -> Unit,
triggerPostUploadAction: (PostUploadAction) -> Unit,
invalidateUploadStatus: (List<Int>) -> Unit,
invalidateFeaturedMedia: (List<Long>) -> Unit
) {
PostListEventListener(
lifecycle = lifecycle,
dispatcher = dispatcher,
postStore = postStore,
site = site,
postActionHandler = postActionHandler,
handlePostUpdatedWithoutError = handlePostUpdatedWithoutError,
handlePostUploadedWithoutError = handlePostUploadedWithoutError,
triggerPostUploadAction = triggerPostUploadAction,
invalidateUploadStatus = invalidateUploadStatus,
invalidateFeaturedMedia = invalidateFeaturedMedia
)
}
import javax.inject.Inject

/**
* This is a temporary class to make the PostListViewModel more manageable. Please feel free to refactor it any way
* you see fit.
*/
private class PostListEventListener(
class PostListEventListener(
private val lifecycle: Lifecycle,
private val dispatcher: Dispatcher,
private val postStore: PostStore,
Expand Down Expand Up @@ -227,4 +202,32 @@ private class PostListEventListener(
private fun featuredMediaChanged(vararg featuredImageIds: Long) {
invalidateFeaturedMedia.invoke(featuredImageIds.toList())
}

class Factory @Inject constructor() {
fun createAndStartListening(
lifecycle: Lifecycle,
dispatcher: Dispatcher,
postStore: PostStore,
site: SiteModel,
postActionHandler: PostActionHandler,
handlePostUpdatedWithoutError: () -> Unit,
handlePostUploadedWithoutError: (LocalId) -> Unit,
triggerPostUploadAction: (PostUploadAction) -> Unit,
invalidateUploadStatus: (List<Int>) -> Unit,
invalidateFeaturedMedia: (List<Long>) -> Unit
) {
PostListEventListener(
lifecycle = lifecycle,
dispatcher = dispatcher,
postStore = postStore,
site = site,
postActionHandler = postActionHandler,
handlePostUpdatedWithoutError = handlePostUpdatedWithoutError,
handlePostUploadedWithoutError = handlePostUploadedWithoutError,
triggerPostUploadAction = triggerPostUploadAction,
invalidateUploadStatus = invalidateUploadStatus,
invalidateFeaturedMedia = invalidateFeaturedMedia
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class PostListFragment : Fragment() {
if (!NetworkUtils.isNetworkAvailable(nonNullActivity)) {
swipeRefreshLayout?.isRefreshing = false
} else {
viewModel.fetchFirstPage()
viewModel.swipeToRefresh()
}
}
return view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.LifecycleRegistry
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModel
import android.content.Intent
import kotlinx.coroutines.CoroutineDispatcher
Expand Down Expand Up @@ -38,10 +39,12 @@ import org.wordpress.android.ui.posts.PostListViewLayoutType.STANDARD
import org.wordpress.android.ui.posts.PostListViewLayoutTypeMenuUiState.CompactViewLayoutTypeMenuUiState
import org.wordpress.android.ui.posts.PostListViewLayoutTypeMenuUiState.StandardViewLayoutTypeMenuUiState
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.uploads.LocalDraftUploadStarter
import org.wordpress.android.util.NetworkUtilsWrapper
import org.wordpress.android.util.ToastUtils.Duration
import org.wordpress.android.util.analytics.AnalyticsUtils
import org.wordpress.android.viewmodel.SingleLiveEvent
import org.wordpress.android.viewmodel.helpers.ConnectionStatus
import org.wordpress.android.viewmodel.helpers.DialogHolder
import org.wordpress.android.viewmodel.helpers.ToastMessageHolder
import org.wordpress.android.viewmodel.posts.PostFetcher
Expand All @@ -65,6 +68,9 @@ class PostListMainViewModel @Inject constructor(
mediaStore: MediaStore,
private val networkUtilsWrapper: NetworkUtilsWrapper,
private val prefs: AppPrefsWrapper,
private val localDraftUploadStarter: LocalDraftUploadStarter,
private val connectionStatus: LiveData<ConnectionStatus>,
private val postListEventListenerFactory: PostListEventListener.Factory,
@Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher
) : ViewModel(), LifecycleOwner, CoroutineScope {
Expand Down Expand Up @@ -181,7 +187,7 @@ class PostListMainViewModel @Inject constructor(
AuthorFilterSelection.EVERYONE
}

listenForPostListEvents(
postListEventListenerFactory.createAndStartListening(
lifecycle = lifecycle,
dispatcher = dispatcher,
postStore = postStore,
Expand All @@ -201,6 +207,7 @@ class PostListMainViewModel @Inject constructor(
invalidateAllLists()
}
)

_updatePostsPager.value = authorFilterSelection
_viewState.value = PostListMainViewState(
isFabVisible = FAB_VISIBLE_POST_LIST_PAGES.contains(POST_LIST_PAGES.first()),
Expand All @@ -209,6 +216,10 @@ class PostListMainViewModel @Inject constructor(
authorFilterItems = getAuthorFilterItems(authorFilterSelection, accountStore.account?.avatarUrl)
)
lifecycleRegistry.markState(Lifecycle.State.STARTED)

connectionStatus.observe(this, Observer {
localDraftUploadStarter.uploadLocalDrafts(scope = this@PostListMainViewModel, site = site)
})
}

override fun onCleared() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.wordpress.android.ui.uploads

import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.PostStore
import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.util.NetworkUtilsWrapper
import javax.inject.Inject
import javax.inject.Named

/**
* Provides a way to find and upload all local drafts.
*/
class LocalDraftUploadStarter @Inject constructor(
/**
* The Application context
*/
private val context: Context,
private val postStore: PostStore,
/**
* The Coroutine dispatcher used for querying in FluxC.
*/
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
private val networkUtilsWrapper: NetworkUtilsWrapper
) {
fun uploadLocalDrafts(scope: CoroutineScope, site: SiteModel) = scope.launch(bgDispatcher) {
if (!networkUtilsWrapper.isNetworkAvailable()) {
return@launch
}

postStore.getLocalDraftPosts(site)
.filterNot { UploadService.isPostUploadingOrQueued(it) }
.forEach { localDraft ->
val intent = UploadService.getUploadPostServiceIntent(context, localDraft, false, false, true)
context.startService(intent)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import android.arch.lifecycle.LifecycleRegistry
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModel
import android.arch.paging.PagedList
import kotlinx.coroutines.CoroutineDispatcher
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId
import org.wordpress.android.fluxc.model.PostModel
Expand All @@ -20,19 +20,23 @@ import org.wordpress.android.fluxc.model.list.PostListDescriptor.PostListDescrip
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.store.ListStore
import org.wordpress.android.fluxc.store.PostStore
import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.ui.posts.AuthorFilterSelection.EVERYONE
import org.wordpress.android.ui.posts.AuthorFilterSelection.ME
import org.wordpress.android.ui.posts.PostUtils
import org.wordpress.android.ui.posts.trackPostListAction
import org.wordpress.android.ui.uploads.LocalDraftUploadStarter
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.NetworkUtilsWrapper
import org.wordpress.android.util.SiteUtils
import org.wordpress.android.viewmodel.ScopedViewModel
import org.wordpress.android.viewmodel.SingleLiveEvent
import org.wordpress.android.viewmodel.helpers.ConnectionStatus
import org.wordpress.android.viewmodel.posts.PostListEmptyUiState.RefreshError
import org.wordpress.android.viewmodel.posts.PostListItemIdentifier.LocalPostId
import org.wordpress.android.viewmodel.posts.PostListItemType.PostListItemUiState
import javax.inject.Inject
import javax.inject.Named

typealias PagedPostList = PagedList<PostListItemType>

Expand All @@ -44,8 +48,10 @@ class PostListViewModel @Inject constructor(
private val accountStore: AccountStore,
private val listItemUiStateHelper: PostListItemUiStateHelper,
private val networkUtilsWrapper: NetworkUtilsWrapper,
connectionStatus: LiveData<ConnectionStatus>
) : ViewModel(), LifecycleOwner {
private val localDraftUploadStarter: LocalDraftUploadStarter,
connectionStatus: LiveData<ConnectionStatus>,
@Named(BG_THREAD) bgDispatcher: CoroutineDispatcher
) : ScopedViewModel(bgDispatcher), LifecycleOwner {
private val isStatsSupported: Boolean by lazy {
SiteUtils.isAccessedViaWPComRest(connector.site) && connector.site.hasCapabilityViewStats
}
Expand Down Expand Up @@ -147,8 +153,9 @@ class PostListViewModel @Inject constructor(

// Public Methods

fun fetchFirstPage() {
pagedListWrapper.fetchFirstPage()
fun swipeToRefresh() {
localDraftUploadStarter.uploadLocalDrafts(scope = this, site = connector.site)
fetchFirstPage()
}

fun scrollToPost(localPostId: LocalPostId) {
Expand All @@ -163,6 +170,10 @@ class PostListViewModel @Inject constructor(

// Utils

private fun fetchFirstPage() {
pagedListWrapper.fetchFirstPage()
}

private fun onDataUpdated(data: PagedPostList) {
val localPostId = scrollToLocalPostId
if (localPostId != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.wordpress.android.ui.posts

import android.arch.core.executor.testing.InstantTaskExecutorRule
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import org.junit.Rule
import org.junit.Test
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.posts.PostListViewLayoutType.STANDARD
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.uploads.LocalDraftUploadStarter
import org.wordpress.android.viewmodel.helpers.ConnectionStatus
import org.wordpress.android.viewmodel.helpers.ConnectionStatus.AVAILABLE
import org.wordpress.android.viewmodel.helpers.ConnectionStatus.UNAVAILABLE

class PostListMainViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()

@Test
fun `when started, it uploads all local drafts`() {
// Given
val site = SiteModel()
val localDraftUploadStarter = mock<LocalDraftUploadStarter>()
val connectionStatus = MutableLiveData<ConnectionStatus>().apply { value = AVAILABLE }

val viewModel = createPostListMainViewModel(
localDraftUploadStarter = localDraftUploadStarter,
connectionStatus = connectionStatus
)

// When
viewModel.start(site = site)

// Then
verify(localDraftUploadStarter, times(1)).uploadLocalDrafts(scope = eq(viewModel), site = eq(site))
}

@Test
fun `when the internet connection changes, it uploads all local drafts`() {
// Given
val site = SiteModel()
val localDraftUploadStarter = mock<LocalDraftUploadStarter>()
val connectionStatus = MutableLiveData<ConnectionStatus>().apply { value = AVAILABLE }

val viewModel = createPostListMainViewModel(
localDraftUploadStarter = localDraftUploadStarter,
connectionStatus = connectionStatus
)
viewModel.start(site = site)

// When
connectionStatus.postValue(UNAVAILABLE)
connectionStatus.postValue(AVAILABLE)

// Then
// The upload should be executed 3 times because we have 2 connections status changes plus the auto-upload
// during `viewModel.start()`.
verify(localDraftUploadStarter, times(3)).uploadLocalDrafts(scope = eq(viewModel), site = eq(site))
}

private companion object {
fun createPostListMainViewModel(
localDraftUploadStarter: LocalDraftUploadStarter,
connectionStatus: LiveData<ConnectionStatus>
): PostListMainViewModel {
val prefs = mock<AppPrefsWrapper> {
on { postListViewLayoutType } doReturn STANDARD
}

return PostListMainViewModel(
dispatcher = mock(),
postStore = mock(),
accountStore = mock(),
uploadStore = mock(),
mediaStore = mock(),
networkUtilsWrapper = mock(),
prefs = prefs,
localDraftUploadStarter = localDraftUploadStarter,
connectionStatus = connectionStatus,
mainDispatcher = mock(),
bgDispatcher = mock(),
postListEventListenerFactory = mock()
)
}
}
}
Loading