@@ -53,6 +53,8 @@ import com.nextcloud.talk.utils.VideoCompressor
5353import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
5454import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
5555import com.nextcloud.talk.utils.preferences.AppPreferences
56+ import io.reactivex.Observable
57+ import io.reactivex.disposables.Disposable
5658import kotlinx.coroutines.ExperimentalCoroutinesApi
5759import kotlinx.coroutines.flow.MutableSharedFlow
5860import kotlinx.coroutines.flow.SharedFlow
@@ -62,7 +64,10 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
6264import okhttp3.OkHttpClient
6365import java.io.File
6466import java.io.IOException
67+ import java.util.Collections
6568import java.util.UUID
69+ import java.util.concurrent.ConcurrentHashMap
70+ import java.util.concurrent.CountDownLatch
6671import java.util.concurrent.TimeUnit
6772import javax.inject.Inject
6873
@@ -107,11 +112,35 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
107112 private var chunkedFileUploader: ChunkedFileUploader ? = null
108113 private var referenceId: String? = null
109114 private var internalConversationId: String? = null
115+ private var uploadDisposable: Disposable ? = null
116+ private var uploadLatch: CountDownLatch ? = null
117+
118+ /* *
119+ * WorkManager's own `isStopped`/`onStopped()` only becomes true/fires after
120+ * cancelUniqueWork() round-trips through WorkManager's internal executor and Room DB - for a
121+ * small/compressed image the whole upload+share can finish faster than that round-trip, so
122+ * isStopped alone arrives too late. [cancelledReferenceIds] is set synchronously by the UI
123+ * before cancelUniqueWork() is even called, so it's visible to doWork() immediately.
124+ */
125+ private fun isCancelled (): Boolean = referenceId?.let { cancelledReferenceIds.contains(it) } == true
110126
111- @Suppress(" Detekt.TooGenericExceptionCaught" , " Detekt.LongMethod" )
112127 override fun doWork (): Result {
113128 NextcloudTalkApplication .sharedApplication!! .componentApplication.inject(this )
114129
130+ try {
131+ return doUpload()
132+ } finally {
133+ referenceId?.let { cancelledReferenceIds.remove(it) }
134+ }
135+ }
136+
137+ @Suppress(
138+ " Detekt.TooGenericExceptionCaught" ,
139+ " Detekt.LongMethod" ,
140+ " Detekt.CyclomaticComplexMethod" ,
141+ " Detekt.ReturnCount"
142+ )
143+ private fun doUpload (): Result {
115144 return try {
116145 currentUser = currentUserProvider.currentUser.blockingGet()
117146 val sourceFile = inputData.getString(DEVICE_SOURCE_FILE )
@@ -149,6 +178,11 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
149178 useConversationSubfolders = useConversationSubfolders
150179 )
151180
181+ if (uploadSuccess && (isStopped || isCancelled())) {
182+ // Cancelled right as the upload finished - don't share a cancelled upload.
183+ return Result .failure()
184+ }
185+
152186 if (uploadSuccess) {
153187 // useConversationSubfolders already shares as part of uploadFile() via
154188 // postConversationAttachment, so only share explicitly for the plain upload path.
@@ -160,7 +194,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
160194 }
161195 Log .e(TAG , " Share operation failed after upload" )
162196 return failUpload()
163- } else if (isStopped) {
197+ } else if (isStopped || isCancelled() ) {
164198 // since work is cancelled the result would be ignored anyways
165199 return Result .failure()
166200 }
@@ -176,7 +210,9 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
176210 " Network error while uploading file (attempt ${runAttemptCount + 1 } /$MAX_UPLOAD_ATTEMPTS )" ,
177211 e
178212 )
179- if (runAttemptCount < MAX_UPLOAD_ATTEMPTS - 1 ) {
213+ if (isStopped || isCancelled()) {
214+ Result .failure()
215+ } else if (runAttemptCount < MAX_UPLOAD_ATTEMPTS - 1 ) {
180216 Result .retry()
181217 } else {
182218 failUpload()
@@ -235,7 +271,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
235271 chunkedFileUploader!! .upload(file!! , mimeType, remotePath)
236272 } else {
237273 Log .d(TAG , " starting normal upload (not chunked) of $fileName " )
238- FileUploader (
274+ val observable = FileUploader (
239275 okHttpClient,
240276 context,
241277 currentUser,
@@ -245,9 +281,37 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
245281 ncApiCoroutines
246282 )
247283 .upload(sourceFileUri, fileName, remotePath, null )
248- .blockingFirst( )
284+ blockingUpload(observable )
249285 }
250286
287+ // unlike .blockingFirst(), keeps a Disposable so onStopped() can cancel the underlying OkHttp call
288+ private fun blockingUpload (observable : Observable <Boolean >): Boolean {
289+ val latch = CountDownLatch (1 )
290+ uploadLatch = latch
291+ var result = false
292+ var error: Throwable ? = null
293+ uploadDisposable = observable.subscribe(
294+ { success ->
295+ result = success
296+ latch.countDown()
297+ },
298+ { throwable ->
299+ error = throwable
300+ latch.countDown()
301+ },
302+ { latch.countDown() }
303+ )
304+ latch.await()
305+ uploadDisposable = null
306+ uploadLatch = null
307+
308+ if (isStopped || isCancelled()) {
309+ return false
310+ }
311+ error?.let { throw it }
312+ return result
313+ }
314+
251315 private fun uploadUsingConversationSubfolders (sourceFileUri : Uri , metaData : String? ): Boolean =
252316 runBlocking {
253317 val credentials = ApiUtils .getCredentials(
@@ -287,7 +351,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
287351 .uploadToConversationSubfolder(sourceFileUri, tempRemotePath)
288352 }
289353
290- if (! uploadSuccess) {
354+ if (! uploadSuccess || isStopped || isCancelled() ) {
291355 return @runBlocking false
292356 }
293357
@@ -352,6 +416,8 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
352416 if (file != null && isChunkedUploading) {
353417 chunkedFileUploader?.abortUpload {}
354418 }
419+ uploadDisposable?.dispose()
420+ uploadLatch?.countDown()
355421 super .onStopped()
356422 }
357423
@@ -399,6 +465,11 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
399465 private const val MAX_UPLOAD_ATTEMPTS = 4
400466 const val REQUEST_PERMISSION = 3123
401467
468+ // referenceIds the user cancelled - set synchronously here, before cancelUniqueWork() is
469+ // even called, so doWork() can see it immediately instead of waiting for isStopped, which
470+ // only becomes true once WorkManager's own async cancellation dispatch completes.
471+ private val cancelledReferenceIds: MutableSet <String > = Collections .newSetFromMap(ConcurrentHashMap ())
472+
402473 private val _uploadCompletedFlow : MutableSharedFlow <String > = MutableSharedFlow (
403474 replay = 1 ,
404475 extraBufferCapacity = 1
@@ -479,5 +550,10 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
479550 WorkManager .getInstance().enqueueUniqueWork(fileUri, ExistingWorkPolicy .KEEP , uploadWorker)
480551 return uploadWorker.id
481552 }
553+
554+ fun cancelUpload (referenceId : String , fileUri : String ) {
555+ cancelledReferenceIds.add(referenceId)
556+ WorkManager .getInstance().cancelUniqueWork(fileUri)
557+ }
482558 }
483559}
0 commit comments