Skip to content

Commit c6e6d6a

Browse files
authored
Merge pull request #9869 from wordpress-mobile/issue/9852-localdraftstarter-upload-from-anywhere
LocalDraftUploadStarter: Upload from anywhere
2 parents e04fc7c + 3e675a7 commit c6e6d6a

File tree

13 files changed

+561
-73
lines changed

13 files changed

+561
-73
lines changed

RELEASE-NOTES.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
12.5
22
-----
3-
* Fixed local drafts not automatically pushed to the server.
3+
* Fixed local drafts not automatically pushed to the server.
4+
* Local Drafts will be automatically uploaded to the server as soon as internet connection is available.
45

56
12.4
67
-----

WordPress/src/main/java/org/wordpress/android/WordPress.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,14 @@
7474
import org.wordpress.android.ui.stats.StatsWidgetProvider;
7575
import org.wordpress.android.ui.stats.datasets.StatsDatabaseHelper;
7676
import org.wordpress.android.ui.stats.datasets.StatsTable;
77+
import org.wordpress.android.ui.uploads.LocalDraftUploadStarter;
7778
import org.wordpress.android.ui.uploads.UploadService;
78-
import org.wordpress.android.util.CrashLoggingUtils;
7979
import org.wordpress.android.util.AppLog;
8080
import org.wordpress.android.util.AppLog.AppLogListener;
8181
import org.wordpress.android.util.AppLog.LogLevel;
8282
import org.wordpress.android.util.AppLog.T;
8383
import org.wordpress.android.util.BitmapLruCache;
84+
import org.wordpress.android.util.CrashLoggingUtils;
8485
import org.wordpress.android.util.DateTimeUtils;
8586
import org.wordpress.android.util.FluxCUtils;
8687
import org.wordpress.android.util.LocaleManager;
@@ -141,6 +142,7 @@ public class WordPress extends MultiDexApplication implements HasServiceInjector
141142
@Inject SiteStore mSiteStore;
142143
@Inject MediaStore mMediaStore;
143144
@Inject ZendeskHelper mZendeskHelper;
145+
@Inject LocalDraftUploadStarter mLocalDraftUploadStarter;
144146

145147
@Inject @Named("custom-ssl") RequestQueue mRequestQueue;
146148
public static RequestQueue sRequestQueue;
@@ -276,6 +278,9 @@ public void onLog(T tag, LogLevel logLevel, String message) {
276278
mApplicationLifecycleMonitor = new ApplicationLifecycleMonitor();
277279
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
278280

281+
// Make the UploadStarter observe the app process so it can auto-start uploads
282+
mLocalDraftUploadStarter.activateAutoUploading((ProcessLifecycleOwner) ProcessLifecycleOwner.get());
283+
279284
initAnalytics(SystemClock.elapsedRealtime() - startDate);
280285

281286
createNotificationChannelsOnSdk26();

WordPress/src/main/java/org/wordpress/android/modules/ThreadModule.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const val UI_SCOPE = "UI_SCOPE"
1212
const val DEFAULT_SCOPE = "DEFAULT_SCOPE"
1313
const val UI_THREAD = "UI_THREAD"
1414
const val BG_THREAD = "BG_THREAD"
15+
const val IO_THREAD = "IO_THREAD"
1516

1617
@Module
1718
class ThreadModule {
@@ -39,6 +40,12 @@ class ThreadModule {
3940
return Dispatchers.Default
4041
}
4142

43+
@Provides
44+
@Named(IO_THREAD)
45+
fun provideIoDispatcher(): CoroutineDispatcher {
46+
return Dispatchers.IO
47+
}
48+
4249
@Provides
4350
fun provideDebouncer(): Debouncer {
4451
return Debouncer()

WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import android.arch.lifecycle.LifecycleOwner
55
import android.arch.lifecycle.LifecycleRegistry
66
import android.arch.lifecycle.LiveData
77
import android.arch.lifecycle.MutableLiveData
8-
import android.arch.lifecycle.Observer
98
import android.arch.lifecycle.ViewModel
109
import android.content.Intent
1110
import kotlinx.coroutines.CoroutineDispatcher
@@ -44,7 +43,6 @@ import org.wordpress.android.util.NetworkUtilsWrapper
4443
import org.wordpress.android.util.ToastUtils.Duration
4544
import org.wordpress.android.util.analytics.AnalyticsUtils
4645
import org.wordpress.android.viewmodel.SingleLiveEvent
47-
import org.wordpress.android.viewmodel.helpers.ConnectionStatus
4846
import org.wordpress.android.viewmodel.helpers.DialogHolder
4947
import org.wordpress.android.viewmodel.helpers.ToastMessageHolder
5048
import org.wordpress.android.viewmodel.posts.PostFetcher
@@ -68,11 +66,10 @@ class PostListMainViewModel @Inject constructor(
6866
mediaStore: MediaStore,
6967
private val networkUtilsWrapper: NetworkUtilsWrapper,
7068
private val prefs: AppPrefsWrapper,
71-
private val localDraftUploadStarter: LocalDraftUploadStarter,
72-
private val connectionStatus: LiveData<ConnectionStatus>,
7369
private val postListEventListenerFactory: PostListEventListener.Factory,
7470
@Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
75-
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher
71+
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
72+
private val localDraftUploadStarter: LocalDraftUploadStarter
7673
) : ViewModel(), LifecycleOwner, CoroutineScope {
7774
private val lifecycleRegistry = LifecycleRegistry(this)
7875
override fun getLifecycle(): Lifecycle = lifecycleRegistry
@@ -217,9 +214,7 @@ class PostListMainViewModel @Inject constructor(
217214
)
218215
lifecycleRegistry.markState(Lifecycle.State.STARTED)
219216

220-
connectionStatus.observe(this, Observer {
221-
localDraftUploadStarter.uploadLocalDrafts(scope = this@PostListMainViewModel, site = site)
222-
})
217+
localDraftUploadStarter.queueUploadFromSite(site)
223218
}
224219

225220
override fun onCleared() {
Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,138 @@
11
package org.wordpress.android.ui.uploads
22

3+
import android.arch.lifecycle.Lifecycle.Event
4+
import android.arch.lifecycle.LifecycleObserver
5+
import android.arch.lifecycle.LiveData
6+
import android.arch.lifecycle.OnLifecycleEvent
7+
import android.arch.lifecycle.ProcessLifecycleOwner
38
import android.content.Context
49
import kotlinx.coroutines.CoroutineDispatcher
510
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.Job
12+
import kotlinx.coroutines.coroutineScope
613
import kotlinx.coroutines.launch
714
import org.wordpress.android.fluxc.model.SiteModel
815
import org.wordpress.android.fluxc.store.PostStore
16+
import org.wordpress.android.fluxc.store.SiteStore
917
import org.wordpress.android.modules.BG_THREAD
18+
import org.wordpress.android.modules.IO_THREAD
19+
import org.wordpress.android.util.CrashLoggingUtils
1020
import org.wordpress.android.util.NetworkUtilsWrapper
21+
import org.wordpress.android.util.skip
22+
import org.wordpress.android.viewmodel.helpers.ConnectionStatus
1123
import javax.inject.Inject
1224
import javax.inject.Named
25+
import javax.inject.Singleton
26+
import kotlin.coroutines.CoroutineContext
1327

1428
/**
15-
* Provides a way to find and upload all local drafts.
29+
* Automatically uploads local drafts.
30+
*
31+
* Auto-uploads happen when the app is placed in the foreground or when the internet connection is restored. In
32+
* addition to this, call sites can also request an immediate execution by calling [upload].
33+
*
34+
* The method [activateAutoUploading] must be called once, preferably during app creation, for the auto-uploads to work.
1635
*/
36+
@Singleton
1737
class LocalDraftUploadStarter @Inject constructor(
1838
/**
1939
* The Application context
2040
*/
2141
private val context: Context,
2242
private val postStore: PostStore,
43+
private val siteStore: SiteStore,
44+
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
45+
@Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher,
46+
private val uploadServiceFacade: UploadServiceFacade,
47+
private val networkUtilsWrapper: NetworkUtilsWrapper,
48+
private val connectionStatus: LiveData<ConnectionStatus>
49+
) : CoroutineScope {
50+
private val job = Job()
51+
52+
override val coroutineContext: CoroutineContext get() = job + bgDispatcher
53+
2354
/**
24-
* The Coroutine dispatcher used for querying in FluxC.
55+
* The hook for making this class automatically launch uploads whenever the app is placed in the foreground.
2556
*/
26-
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
27-
private val networkUtilsWrapper: NetworkUtilsWrapper
28-
) {
29-
fun uploadLocalDrafts(scope: CoroutineScope, site: SiteModel) = scope.launch(bgDispatcher) {
57+
private val processLifecycleObserver = object : LifecycleObserver {
58+
@OnLifecycleEvent(Event.ON_START)
59+
fun onAppComesFromBackground() {
60+
queueUploadFromAllSites()
61+
}
62+
}
63+
64+
/**
65+
* Activates the necessary observers for this class to start auto-uploading.
66+
*
67+
* This must be called during [org.wordpress.android.WordPress]' creation like so:
68+
*
69+
* ```
70+
* mLocalDraftUploadStarter.activateAutoUploading(ProcessLifecycleOwner.get())
71+
* ```
72+
*/
73+
fun activateAutoUploading(processLifecycleOwner: ProcessLifecycleOwner) {
74+
// Since this class is meant to be a Singleton, it should be fine (I think) to use observeForever in here.
75+
// We're skipping the first emitted value because the processLifecycleObserver below will also trigger an
76+
// immediate upload.
77+
connectionStatus.skip(1).observeForever {
78+
queueUploadFromAllSites()
79+
}
80+
81+
processLifecycleOwner.lifecycle.addObserver(processLifecycleObserver)
82+
}
83+
84+
private fun queueUploadFromAllSites() = launch {
85+
val sites = siteStore.sites
86+
try {
87+
checkConnectionAndUpload(sites = sites)
88+
} catch (e: Exception) {
89+
CrashLoggingUtils.log(e)
90+
}
91+
}
92+
93+
/**
94+
* Upload all local drafts from the given [site].
95+
*/
96+
fun queueUploadFromSite(site: SiteModel) = launch {
97+
try {
98+
checkConnectionAndUpload(sites = listOf(site))
99+
} catch (e: Exception) {
100+
CrashLoggingUtils.log(e)
101+
}
102+
}
103+
104+
/**
105+
* If there is an internet connection, uploads all local drafts belonging to [sites].
106+
*
107+
* This coroutine will suspend until all the [upload] operations have completed. If one of them fails, all query
108+
* and queuing attempts ([upload]) will be canceled. The exception will be thrown by this method.
109+
*/
110+
private suspend fun checkConnectionAndUpload(sites: List<SiteModel>) = coroutineScope {
30111
if (!networkUtilsWrapper.isNetworkAvailable()) {
31-
return@launch
112+
return@coroutineScope
32113
}
33114

115+
sites.forEach {
116+
launch(ioDispatcher) {
117+
upload(site = it)
118+
}
119+
}
120+
}
121+
122+
/**
123+
* This is meant to be used by [checkConnectionAndUpload] only.
124+
*/
125+
private fun upload(site: SiteModel) {
34126
postStore.getLocalDraftPosts(site)
35-
.filterNot { UploadService.isPostUploadingOrQueued(it) }
127+
.filterNot { uploadServiceFacade.isPostUploadingOrQueued(it) }
36128
.forEach { localDraft ->
37-
val intent = UploadService.getUploadPostServiceIntent(context, localDraft, false, false, true)
38-
context.startService(intent)
129+
uploadServiceFacade.uploadPost(
130+
context = context,
131+
post = localDraft,
132+
trackAnalytics = false,
133+
publish = false,
134+
isRetry = true
135+
)
39136
}
40137
}
41138
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.wordpress.android.ui.uploads
2+
3+
import android.content.Context
4+
import org.wordpress.android.fluxc.model.PostModel
5+
import javax.inject.Inject
6+
7+
/**
8+
* An injectable class built on top of [UploadService].
9+
*
10+
* The main purpose of this is to provide testability for classes that use [UploadService]. This should never
11+
* contain any static methods.
12+
*/
13+
class UploadServiceFacade @Inject constructor() {
14+
fun uploadPost(context: Context, post: PostModel, trackAnalytics: Boolean, publish: Boolean, isRetry: Boolean) {
15+
val intent = UploadService.getUploadPostServiceIntent(context, post, trackAnalytics, publish, isRetry)
16+
context.startService(intent)
17+
}
18+
19+
fun isPostUploadingOrQueued(post: PostModel) = UploadService.isPostUploadingOrQueued(post)
20+
}

WordPress/src/main/java/org/wordpress/android/util/LiveDataUtils.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.wordpress.android.util
22

33
import android.arch.lifecycle.LiveData
44
import android.arch.lifecycle.MediatorLiveData
5+
import android.arch.lifecycle.Observer
56
import android.arch.lifecycle.Transformations
67
import kotlinx.coroutines.CoroutineScope
78
import org.wordpress.android.viewmodel.SingleMediatorLiveEvent
@@ -224,3 +225,33 @@ fun <T> LiveData<T>.filter(predicate: (T) -> Boolean): LiveData<T> {
224225
}
225226
return mediator
226227
}
228+
229+
/**
230+
* Suppresses the first n items by this [LiveData].
231+
*
232+
* Consider this for example:
233+
*
234+
* ```
235+
* val connectionStatusLiveData = getConnectionStatusLiveData()
236+
* connectionStatusLiveData.skip(1).observe(this, Observer {
237+
* refresh()
238+
* })
239+
* ```
240+
*
241+
* The first value emitted by `connectionStatusLiveData` would be ignored and [Observer] will not be called.
242+
*/
243+
fun <T> LiveData<T>.skip(times: Int): LiveData<T> {
244+
check(times > 0) { "The number of times to skip must be greater than 0" }
245+
246+
var skipped = 0
247+
val mediator = MediatorLiveData<T>()
248+
mediator.addSource(this) { value ->
249+
skipped += 1
250+
251+
if (skipped > times) {
252+
mediator.value = value
253+
}
254+
}
255+
256+
return mediator
257+
}

WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListViewModel.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import android.arch.lifecycle.LifecycleRegistry
77
import android.arch.lifecycle.LiveData
88
import android.arch.lifecycle.MediatorLiveData
99
import android.arch.lifecycle.Observer
10+
import android.arch.lifecycle.ViewModel
1011
import android.arch.paging.PagedList
11-
import kotlinx.coroutines.CoroutineDispatcher
1212
import org.wordpress.android.fluxc.Dispatcher
1313
import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId
1414
import org.wordpress.android.fluxc.model.PostModel
@@ -20,7 +20,6 @@ import org.wordpress.android.fluxc.model.list.PostListDescriptor.PostListDescrip
2020
import org.wordpress.android.fluxc.store.AccountStore
2121
import org.wordpress.android.fluxc.store.ListStore
2222
import org.wordpress.android.fluxc.store.PostStore
23-
import org.wordpress.android.modules.BG_THREAD
2423
import org.wordpress.android.ui.posts.AuthorFilterSelection.EVERYONE
2524
import org.wordpress.android.ui.posts.AuthorFilterSelection.ME
2625
import org.wordpress.android.ui.posts.PostUtils
@@ -29,14 +28,12 @@ import org.wordpress.android.ui.uploads.LocalDraftUploadStarter
2928
import org.wordpress.android.util.AppLog
3029
import org.wordpress.android.util.NetworkUtilsWrapper
3130
import org.wordpress.android.util.SiteUtils
32-
import org.wordpress.android.viewmodel.ScopedViewModel
3331
import org.wordpress.android.viewmodel.SingleLiveEvent
3432
import org.wordpress.android.viewmodel.helpers.ConnectionStatus
3533
import org.wordpress.android.viewmodel.posts.PostListEmptyUiState.RefreshError
3634
import org.wordpress.android.viewmodel.posts.PostListItemIdentifier.LocalPostId
3735
import org.wordpress.android.viewmodel.posts.PostListItemType.PostListItemUiState
3836
import javax.inject.Inject
39-
import javax.inject.Named
4037

4138
typealias PagedPostList = PagedList<PostListItemType>
4239

@@ -49,9 +46,8 @@ class PostListViewModel @Inject constructor(
4946
private val listItemUiStateHelper: PostListItemUiStateHelper,
5047
private val networkUtilsWrapper: NetworkUtilsWrapper,
5148
private val localDraftUploadStarter: LocalDraftUploadStarter,
52-
connectionStatus: LiveData<ConnectionStatus>,
53-
@Named(BG_THREAD) bgDispatcher: CoroutineDispatcher
54-
) : ScopedViewModel(bgDispatcher), LifecycleOwner {
49+
connectionStatus: LiveData<ConnectionStatus>
50+
) : ViewModel(), LifecycleOwner {
5551
private val isStatsSupported: Boolean by lazy {
5652
SiteUtils.isAccessedViaWPComRest(connector.site) && connector.site.hasCapabilityViewStats
5753
}
@@ -154,7 +150,7 @@ class PostListViewModel @Inject constructor(
154150
// Public Methods
155151

156152
fun swipeToRefresh() {
157-
localDraftUploadStarter.uploadLocalDrafts(scope = this, site = connector.site)
153+
localDraftUploadStarter.queueUploadFromSite(connector.site)
158154
fetchFirstPage()
159155
}
160156

0 commit comments

Comments
 (0)