Skip to content

Commit f2c3137

Browse files
Merge pull request #15872 from nextcloud/fix/choose-correct-upload-ids-for-retry
fix: choose correct upload ids for retry
2 parents 661048d + e9dc260 commit f2c3137

File tree

6 files changed

+123
-119
lines changed

6 files changed

+123
-119
lines changed

app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ interface UploadDao {
7171
)
7272
suspend fun updateStatus(remotePath: String, accountName: String, status: Int): Int
7373

74+
@Query(
75+
"""
76+
SELECT * FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME}
77+
WHERE ${ProviderTableMeta.UPLOADS_STATUS} = :status
78+
AND (:nameCollisionPolicy IS NULL OR ${ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY} = :nameCollisionPolicy)
79+
"""
80+
)
81+
suspend fun getUploadsByStatus(status: Int, nameCollisionPolicy: Int? = null): List<UploadEntity>
82+
7483
@Query(
7584
"""
7685
SELECT * FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME}
@@ -79,7 +88,7 @@ interface UploadDao {
7988
AND (:nameCollisionPolicy IS NULL OR ${ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY} = :nameCollisionPolicy)
8089
"""
8190
)
82-
suspend fun getUploadsByStatus(
91+
suspend fun getUploadsByAccountNameAndStatus(
8392
accountName: String,
8493
status: Int,
8594
nameCollisionPolicy: Int? = null

app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt

Lines changed: 94 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -88,33 +88,57 @@ class FileUploadHelper {
8888
fun buildRemoteName(accountName: String, remotePath: String): String = accountName + remotePath
8989
}
9090

91+
/**
92+
* Retries all failed uploads across all user accounts.
93+
*
94+
* This function retrieves all uploads with the status [UploadStatus.UPLOAD_FAILED], including both
95+
* manual uploads and auto uploads. It runs in a background thread (Dispatcher.IO) and ensures
96+
* that only one retry operation runs at a time by using a semaphore to prevent concurrent execution.
97+
*
98+
* Once the failed uploads are retrieved, it calls [retryUploads], which triggers the corresponding
99+
* upload workers for each failed upload.
100+
*
101+
* The function returns `true` if there were any failed uploads to retry and the retry process was
102+
* started, or `false` if no uploads were retried.
103+
*
104+
* @param uploadsStorageManager Provides access to upload data and persistence.
105+
* @param connectivityService Checks the current network connectivity state.
106+
* @param accountManager Handles user account authentication and selection.
107+
* @param powerManagementService Ensures uploads respect power constraints.
108+
* @return `true` if any failed uploads were found and retried; `false` otherwise.
109+
*/
91110
fun retryFailedUploads(
92111
uploadsStorageManager: UploadsStorageManager,
93112
connectivityService: ConnectivityService,
94113
accountManager: UserAccountManager,
95114
powerManagementService: PowerManagementService
96-
) {
97-
if (retryFailedUploadsSemaphore.tryAcquire()) {
98-
try {
99-
val failedUploads = uploadsStorageManager.failedUploads
100-
if (failedUploads == null || failedUploads.isEmpty()) {
101-
Log_OC.d(TAG, "Failed uploads are empty or null")
102-
return
115+
): Boolean {
116+
if (!retryFailedUploadsSemaphore.tryAcquire()) {
117+
Log_OC.d(TAG, "skipping retryFailedUploads, already running")
118+
return true
119+
}
120+
121+
var isUploadStarted = false
122+
123+
try {
124+
getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED) {
125+
if (it.isNotEmpty()) {
126+
isUploadStarted = true
103127
}
104128

105129
retryUploads(
106130
uploadsStorageManager,
107131
connectivityService,
108132
accountManager,
109133
powerManagementService,
110-
failedUploads
134+
uploads = it
111135
)
112-
} finally {
113-
retryFailedUploadsSemaphore.release()
114136
}
115-
} else {
116-
Log_OC.d(TAG, "Skip retryFailedUploads since it is already running")
137+
} finally {
138+
retryFailedUploadsSemaphore.release()
117139
}
140+
141+
return isUploadStarted
118142
}
119143

120144
fun retryCancelledUploads(
@@ -123,18 +147,18 @@ class FileUploadHelper {
123147
accountManager: UserAccountManager,
124148
powerManagementService: PowerManagementService
125149
): Boolean {
126-
val cancelledUploads = uploadsStorageManager.cancelledUploadsForCurrentAccount
127-
if (cancelledUploads == null || cancelledUploads.isEmpty()) {
128-
return false
150+
var result = false
151+
getUploadsByStatus(accountManager.user.accountName, UploadStatus.UPLOAD_CANCELLED) {
152+
result = retryUploads(
153+
uploadsStorageManager,
154+
connectivityService,
155+
accountManager,
156+
powerManagementService,
157+
it
158+
)
129159
}
130160

131-
return retryUploads(
132-
uploadsStorageManager,
133-
connectivityService,
134-
accountManager,
135-
powerManagementService,
136-
cancelledUploads
137-
)
161+
return result
138162
}
139163

140164
@Suppress("ComplexCondition")
@@ -143,51 +167,51 @@ class FileUploadHelper {
143167
connectivityService: ConnectivityService,
144168
accountManager: UserAccountManager,
145169
powerManagementService: PowerManagementService,
146-
failedUploads: Array<OCUpload>
170+
uploads: Array<OCUpload>
147171
): Boolean {
148172
var showNotExistMessage = false
149173
val isOnline = checkConnectivity(connectivityService)
150174
val connectivity = connectivityService.connectivity
151175
val batteryStatus = powerManagementService.battery
152-
val accountNames = accountManager.accounts.filter { account ->
153-
accountManager.getUser(account.name).isPresent
154-
}.map { account ->
155-
account.name
156-
}.toHashSet()
157-
158-
for (failedUpload in failedUploads) {
159-
if (!accountNames.contains(failedUpload.accountName)) {
160-
uploadsStorageManager.removeUpload(failedUpload)
161-
continue
162-
}
163176

164-
val uploadResult =
165-
checkUploadConditions(failedUpload, connectivity, batteryStatus, powerManagementService, isOnline)
177+
val uploadsToRetry = mutableListOf<Long>()
178+
179+
for (upload in uploads) {
180+
val uploadResult = checkUploadConditions(
181+
upload,
182+
connectivity,
183+
batteryStatus,
184+
powerManagementService,
185+
isOnline
186+
)
166187

167188
if (uploadResult != UploadResult.UPLOADED) {
168-
if (failedUpload.lastResult != uploadResult) {
189+
if (upload.lastResult != uploadResult) {
169190
// Setting Upload status else cancelled uploads will behave wrong, when retrying
170191
// Needs to happen first since lastResult wil be overwritten by setter
171-
failedUpload.uploadStatus = UploadStatus.UPLOAD_FAILED
192+
upload.uploadStatus = UploadStatus.UPLOAD_FAILED
172193

173-
failedUpload.lastResult = uploadResult
174-
uploadsStorageManager.updateUpload(failedUpload)
194+
upload.lastResult = uploadResult
195+
uploadsStorageManager.updateUpload(upload)
175196
}
176197
if (uploadResult == UploadResult.FILE_NOT_FOUND) {
177198
showNotExistMessage = true
178199
}
179200
continue
180201
}
181202

182-
failedUpload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
183-
uploadsStorageManager.updateUpload(failedUpload)
203+
// Only uploads that passed checks get marked in progress and are collected for scheduling
204+
upload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
205+
uploadsStorageManager.updateUpload(upload)
206+
uploadsToRetry.add(upload.uploadId)
184207
}
185208

186-
accountNames.forEach { accountName ->
187-
val user = accountManager.getUser(accountName)
188-
if (user.isPresent) {
189-
backgroundJobManager.startFilesUploadJob(user.get(), failedUploads.getUploadIds(), false)
190-
}
209+
if (uploadsToRetry.isNotEmpty()) {
210+
backgroundJobManager.startFilesUploadJob(
211+
accountManager.user,
212+
uploadsToRetry.toLongArray(),
213+
false
214+
)
191215
}
192216

193217
return showNotExistMessage
@@ -235,21 +259,35 @@ class FileUploadHelper {
235259
}
236260
}
237261

262+
/**
263+
* Retrieves uploads filtered by their status, optionally for a specific account.
264+
*
265+
* This function queries the uploads database asynchronously to obtain a list of uploads
266+
* that match the specified [status]. If an [accountName] is provided, only uploads
267+
* belonging to that account are retrieved. If [accountName] is `null`, uploads with the
268+
* given [status] from **all user accounts** are returned.
269+
*
270+
* Once the uploads are fetched, the [onCompleted] callback is invoked with the resulting array.
271+
*
272+
* @param accountName The name of the account to filter uploads by.
273+
* If `null`, uploads matching the given [status] from all accounts are returned.
274+
* @param status The [UploadStatus] to filter uploads by (e.g., `UPLOAD_FAILED`).
275+
* @param nameCollisionPolicy The [NameCollisionPolicy] to filter uploads by (e.g., `SKIP`).
276+
* @param onCompleted A callback invoked with the resulting array of [OCUpload] objects.
277+
*/
238278
fun getUploadsByStatus(
239-
accountName: String,
279+
accountName: String?,
240280
status: UploadStatus,
241281
nameCollisionPolicy: NameCollisionPolicy? = null,
242282
onCompleted: (Array<OCUpload>) -> Unit
243283
) {
244284
ioScope.launch {
245-
val result = uploadsStorageManager.uploadDao
246-
.getUploadsByStatus(
247-
accountName,
248-
status.value,
249-
nameCollisionPolicy?.serialize()
250-
)
251-
.map { it.toOCUpload(null) }
252-
.toTypedArray()
285+
val dao = uploadsStorageManager.uploadDao
286+
val result = if (accountName != null) {
287+
dao.getUploadsByAccountNameAndStatus(accountName, status.value, nameCollisionPolicy?.serialize())
288+
} else {
289+
dao.getUploadsByStatus(status.value, nameCollisionPolicy?.serialize())
290+
}.map { it.toOCUpload(null) }.toTypedArray()
253291
onCompleted(result)
254292
}
255293
}

app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -483,35 +483,10 @@ public long[] getCurrentUploadIds(final @NonNull String accountName) {
483483
.toArray();
484484
}
485485

486-
/**
487-
* Get all failed uploads.
488-
*/
489-
public OCUpload[] getFailedUploads() {
490-
return getUploads("(" + ProviderTableMeta.UPLOADS_STATUS + IS_EQUAL +
491-
OR + ProviderTableMeta.UPLOADS_LAST_RESULT +
492-
EQUAL + UploadResult.DELAYED_FOR_WIFI.getValue() +
493-
OR + ProviderTableMeta.UPLOADS_LAST_RESULT +
494-
EQUAL + UploadResult.LOCK_FAILED.getValue() +
495-
OR + ProviderTableMeta.UPLOADS_LAST_RESULT +
496-
EQUAL + UploadResult.DELAYED_FOR_CHARGING.getValue() +
497-
OR + ProviderTableMeta.UPLOADS_LAST_RESULT +
498-
EQUAL + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() +
499-
" ) AND " + ProviderTableMeta.UPLOADS_LAST_RESULT +
500-
"!= " + UploadResult.VIRUS_DETECTED.getValue()
501-
, String.valueOf(UploadStatus.UPLOAD_FAILED.value));
502-
}
503-
504486
public OCUpload[] getUploadsForAccount(final @NonNull String accountName) {
505487
return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, accountName);
506488
}
507489

508-
public OCUpload[] getCancelledUploadsForCurrentAccount() {
509-
User user = currentAccountProvider.getUser();
510-
511-
return getUploads(ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_CANCELLED.value + AND +
512-
ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, user.getAccountName());
513-
}
514-
515490
private ContentResolver getDB() {
516491
return contentResolver;
517492
}

app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import com.owncloud.android.operations.CheckCurrentCredentialsOperation;
4242
import com.owncloud.android.ui.adapter.UploadListAdapter;
4343
import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
44-
import com.owncloud.android.utils.DisplayUtils;
4544
import com.owncloud.android.utils.FilesSyncHelper;
4645

4746
import javax.inject.Inject;
@@ -133,15 +132,13 @@ private void observeWorkerState() {
133132
WorkerStateLiveData.Companion.instance().observe(this, state -> {
134133
if (state instanceof WorkerState.UploadStarted) {
135134
Log_OC.d(TAG, "Upload worker started");
136-
handleUploadWorkerState();
135+
uploadListAdapter.loadUploadItemsFromDb();
136+
} else if (state instanceof WorkerState.UploadFinished) {
137+
uploadListAdapter.loadUploadItemsFromDb(() -> swipeListRefreshLayout.setRefreshing(false));
137138
}
138139
});
139140
}
140141

141-
private void handleUploadWorkerState() {
142-
uploadListAdapter.loadUploadItemsFromDb();
143-
}
144-
145142
private void setupContent() {
146143
binding.list.setEmptyView(binding.emptyList.getRoot());
147144
binding.emptyList.getRoot().setVisibility(View.GONE);
@@ -182,25 +179,15 @@ private void loadItems() {
182179
}
183180

184181
private void refresh() {
185-
FilesSyncHelper.startAutoUploadImmediately(syncedFolderProvider,
186-
backgroundJobManager,
187-
true);
188-
189-
if (uploadsStorageManager.getFailedUploads().length > 0) {
190-
new Thread(() -> {
191-
FileUploadHelper.Companion.instance().retryFailedUploads(
192-
uploadsStorageManager,
193-
connectivityService,
194-
accountManager,
195-
powerManagementService);
196-
uploadListAdapter.loadUploadItemsFromDb();
197-
}).start();
198-
DisplayUtils.showSnackMessage(this, R.string.uploader_local_files_uploaded);
182+
boolean isUploadStarted = FileUploadHelper.Companion.instance().retryFailedUploads(
183+
uploadsStorageManager,
184+
connectivityService,
185+
accountManager,
186+
powerManagementService);
187+
188+
if (!isUploadStarted) {
189+
uploadListAdapter.loadUploadItemsFromDb(() -> swipeListRefreshLayout.setRefreshing(false));
199190
}
200-
201-
202-
// update UI
203-
uploadListAdapter.loadUploadItemsFromDb(() -> swipeListRefreshLayout.setRefreshing(false));
204191
}
205192

206193
@Override

app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,16 +264,12 @@ private void showFailedPopupMenu(HeaderViewHolder headerViewHolder) {
264264
clearTempEncryptedFolder();
265265
loadUploadItemsFromDb();
266266
} else if (itemId == R.id.action_upload_list_failed_retry) {
267-
268-
// FIXME For e2e resume is not working
269-
new Thread(() -> {
270-
uploadHelper.retryFailedUploads(
271-
uploadsStorageManager,
272-
connectivityService,
273-
accountManager,
274-
powerManagementService);
275-
loadUploadItemsFromDb();
276-
}).start();
267+
uploadHelper.retryFailedUploads(
268+
uploadsStorageManager,
269+
connectivityService,
270+
accountManager,
271+
powerManagementService);
272+
loadUploadItemsFromDb();
277273
}
278274

279275
return true;

app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.nextcloud.client.network.ConnectivityService;
2525
import com.nextcloud.utils.extensions.UriExtensionsKt;
2626
import com.owncloud.android.MainApp;
27-
import com.owncloud.android.datamodel.FileDataStorageManager;
2827
import com.owncloud.android.datamodel.FilesystemDataProvider;
2928
import com.owncloud.android.datamodel.MediaFolderType;
3029
import com.owncloud.android.datamodel.SyncedFolder;
@@ -222,11 +221,11 @@ public static void restartUploadsIfNeeded(final UploadsStorageManager uploadsSto
222221
final ConnectivityService connectivityService,
223222
final PowerManagementService powerManagementService) {
224223
Log_OC.d(TAG, "restartUploadsIfNeeded, called");
225-
new Thread(() -> FileUploadHelper.Companion.instance().retryFailedUploads(
224+
FileUploadHelper.Companion.instance().retryFailedUploads(
226225
uploadsStorageManager,
227226
connectivityService,
228227
accountManager,
229-
powerManagementService)).start();
228+
powerManagementService);
230229
}
231230

232231
public static void scheduleFilesSyncForAllFoldersIfNeeded(Context context, SyncedFolderProvider syncedFolderProvider, BackgroundJobManager jobManager) {

0 commit comments

Comments
 (0)