Skip to content

Commit 64420ed

Browse files
committed
fix(workers): Fix DownloadEverythingWorker to properly download all files
- Root cause: refreshFolder() only returned changed files, not all - Solution: Use getFolderContent(folderId) after refresh to get ALL files from DB - Add recursive folder traversal with proper refresh before each folder scan - Improve LocalFileSyncWorker with better statistics and notifications - Remove setForegroundAsync to fix Android 14+ foreground service crash
1 parent 7d06f55 commit 64420ed

File tree

2 files changed

+286
-49
lines changed

2 files changed

+286
-49
lines changed

opencloudApp/src/main/java/eu/opencloud/android/workers/DownloadEverythingWorker.kt

Lines changed: 215 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,39 @@
2121
package eu.opencloud.android.workers
2222

2323
import android.accounts.AccountManager
24+
import android.app.NotificationChannel
25+
import android.app.NotificationManager
2426
import android.content.Context
27+
import android.os.Build
28+
import androidx.core.app.NotificationCompat
2529
import androidx.work.CoroutineWorker
2630
import androidx.work.WorkerParameters
2731
import eu.opencloud.android.MainApp
32+
import eu.opencloud.android.R
2833
import eu.opencloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
34+
import eu.opencloud.android.domain.files.FileRepository
2935
import eu.opencloud.android.domain.files.model.OCFile
3036
import eu.opencloud.android.domain.files.model.OCFile.Companion.ROOT_PATH
3137
import eu.opencloud.android.domain.files.usecases.GetFileByRemotePathUseCase
3238
import eu.opencloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase
3339
import eu.opencloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase
3440
import eu.opencloud.android.presentation.authentication.AccountUtils
35-
import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase
41+
import eu.opencloud.android.usecases.transfers.downloads.DownloadFileUseCase
3642
import org.koin.core.component.KoinComponent
3743
import org.koin.core.component.inject
3844
import timber.log.Timber
3945
import java.util.concurrent.TimeUnit
4046

4147
/**
42-
* Worker that downloads all files from all accounts for offline access.
48+
* Worker that downloads ALL files from all accounts for offline access.
4349
* This is an opt-in feature that can be enabled in Security Settings.
50+
*
51+
* This worker:
52+
* 1. Iterates through all connected accounts
53+
* 2. Discovers all spaces (personal + project) for each account
54+
* 3. Recursively scans all folders to find all files
55+
* 4. Enqueues a download for each file that is not yet available locally
56+
* 5. Shows a notification with progress information
4457
*/
4558
class DownloadEverythingWorker(
4659
private val appContext: Context,
@@ -54,77 +67,234 @@ class DownloadEverythingWorker(
5467
private val refreshSpacesFromServerAsyncUseCase: RefreshSpacesFromServerAsyncUseCase by inject()
5568
private val getPersonalAndProjectSpacesForAccountUseCase: GetPersonalAndProjectSpacesForAccountUseCase by inject()
5669
private val getFileByRemotePathUseCase: GetFileByRemotePathUseCase by inject()
57-
private val synchronizeFolderUseCase: SynchronizeFolderUseCase by inject()
70+
private val fileRepository: FileRepository by inject()
71+
private val downloadFileUseCase: DownloadFileUseCase by inject()
72+
73+
private var totalFilesFound = 0
74+
private var filesDownloaded = 0
75+
private var filesAlreadyLocal = 0
76+
private var filesSkipped = 0
77+
private var foldersProcessed = 0
5878

5979
override suspend fun doWork(): Result {
6080
Timber.i("DownloadEverythingWorker started")
81+
82+
// Create notification channel and show initial notification
83+
createNotificationChannel()
84+
updateNotification("Starting download of all files...")
6185

6286
return try {
6387
val accountManager = AccountManager.get(appContext)
6488
val accounts = accountManager.getAccountsByType(MainApp.accountType)
6589

66-
Timber.i("Found ${accounts.size} accounts to sync")
90+
Timber.i("Found ${accounts.size} accounts to process")
91+
updateNotification("Found ${accounts.size} accounts")
6792

68-
accounts.forEach { account ->
93+
accounts.forEachIndexed { accountIndex, account ->
6994
val accountName = account.name
70-
Timber.i("Syncing all files for account: $accountName")
71-
72-
// Get capabilities for account
73-
val capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))
74-
val spacesAvailableForAccount = AccountUtils.isSpacesFeatureAllowedForAccount(appContext, account, capabilities)
75-
76-
if (!spacesAvailableForAccount) {
77-
// Account does not support spaces - sync legacy root
78-
val rootLegacyFolder = getFileByRemotePathUseCase(
79-
GetFileByRemotePathUseCase.Params(accountName, ROOT_PATH, null)
80-
).getDataOrNull()
81-
rootLegacyFolder?.let {
82-
syncFolderRecursively(it)
83-
}
84-
} else {
85-
// Account supports spaces - sync all spaces
86-
refreshSpacesFromServerAsyncUseCase(RefreshSpacesFromServerAsyncUseCase.Params(accountName))
87-
val spaces = getPersonalAndProjectSpacesForAccountUseCase(
88-
GetPersonalAndProjectSpacesForAccountUseCase.Params(accountName)
89-
)
90-
91-
Timber.i("Found ${spaces.size} spaces for account $accountName")
92-
93-
spaces.forEach { space ->
94-
val rootFolderForSpace = getFileByRemotePathUseCase(
95-
GetFileByRemotePathUseCase.Params(accountName, ROOT_PATH, space.root.id)
96-
).getDataOrNull()
97-
98-
rootFolderForSpace?.let {
99-
Timber.i("Syncing space: ${space.name}")
100-
syncFolderRecursively(it)
95+
Timber.i("Processing account ${accountIndex + 1}/${accounts.size}: $accountName")
96+
updateNotification("Account ${accountIndex + 1}/${accounts.size}: $accountName")
97+
98+
try {
99+
// Get capabilities for account
100+
val capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))
101+
val spacesAvailableForAccount = AccountUtils.isSpacesFeatureAllowedForAccount(appContext, account, capabilities)
102+
103+
if (!spacesAvailableForAccount) {
104+
// Account does not support spaces - process legacy root
105+
Timber.i("Account $accountName uses legacy mode (no spaces)")
106+
processSpaceRoot(accountName, ROOT_PATH, null)
107+
} else {
108+
// Account supports spaces - process all spaces
109+
refreshSpacesFromServerAsyncUseCase(RefreshSpacesFromServerAsyncUseCase.Params(accountName))
110+
val spaces = getPersonalAndProjectSpacesForAccountUseCase(
111+
GetPersonalAndProjectSpacesForAccountUseCase.Params(accountName)
112+
)
113+
114+
Timber.i("Account $accountName has ${spaces.size} spaces")
115+
116+
spaces.forEachIndexed { spaceIndex, space ->
117+
Timber.i("Processing space ${spaceIndex + 1}/${spaces.size}: ${space.name}")
118+
updateNotification("Space ${spaceIndex + 1}/${spaces.size}: ${space.name}")
119+
120+
processSpaceRoot(accountName, ROOT_PATH, space.root.id)
101121
}
102122
}
123+
} catch (e: Exception) {
124+
Timber.e(e, "Error processing account $accountName")
103125
}
104126
}
105127

106-
Timber.i("DownloadEverythingWorker completed successfully")
128+
val summary = "Done! Files: $totalFilesFound, Downloaded: $filesDownloaded, Already local: $filesAlreadyLocal, Skipped: $filesSkipped, Folders: $foldersProcessed"
129+
Timber.i("DownloadEverythingWorker completed: $summary")
130+
updateNotification(summary)
131+
107132
Result.success()
108133
} catch (exception: Exception) {
109134
Timber.e(exception, "DownloadEverythingWorker failed")
135+
updateNotification("Failed: ${exception.message}")
110136
Result.failure()
111137
}
112138
}
113139

114-
private fun syncFolderRecursively(folder: OCFile) {
115-
synchronizeFolderUseCase(
116-
SynchronizeFolderUseCase.Params(
117-
accountName = folder.owner,
118-
remotePath = folder.remotePath,
119-
spaceId = folder.spaceId,
120-
syncMode = SynchronizeFolderUseCase.SyncFolderMode.SYNC_FOLDER_RECURSIVELY
140+
/**
141+
* Processes the root of a space by refreshing it and then recursively processing all content.
142+
*/
143+
private fun processSpaceRoot(accountName: String, remotePath: String, spaceId: String?) {
144+
try {
145+
Timber.i("Processing space root: remotePath=$remotePath, spaceId=$spaceId")
146+
147+
// First refresh the root folder from server to ensure DB has latest data
148+
fileRepository.refreshFolder(
149+
remotePath = remotePath,
150+
accountName = accountName,
151+
spaceId = spaceId,
152+
isActionSetFolderAvailableOfflineOrSynchronize = false
121153
)
122-
)
154+
155+
// Now get the root folder from local database
156+
val rootFolder = getFileByRemotePathUseCase(
157+
GetFileByRemotePathUseCase.Params(accountName, remotePath, spaceId)
158+
).getDataOrNull()
159+
160+
if (rootFolder == null) {
161+
Timber.w("Root folder not found after refresh for spaceId=$spaceId")
162+
return
163+
}
164+
165+
Timber.i("Got root folder with id=${rootFolder.id}, remotePath=${rootFolder.remotePath}")
166+
167+
// Process the root folder recursively
168+
processFolderRecursively(accountName, rootFolder, spaceId)
169+
170+
} catch (e: Exception) {
171+
Timber.e(e, "Error processing space root: spaceId=$spaceId")
172+
}
173+
}
174+
175+
/**
176+
* Recursively processes a folder: gets content from database,
177+
* enqueues downloads for files, and recurses into subfolders.
178+
*/
179+
private fun processFolderRecursively(accountName: String, folder: OCFile, spaceId: String?) {
180+
try {
181+
val folderId = folder.id
182+
if (folderId == null) {
183+
Timber.w("Folder ${folder.remotePath} has no id, skipping")
184+
return
185+
}
186+
187+
foldersProcessed++
188+
Timber.d("Processing folder: ${folder.remotePath} (id=$folderId)")
189+
190+
// First refresh this folder from server
191+
try {
192+
fileRepository.refreshFolder(
193+
remotePath = folder.remotePath,
194+
accountName = accountName,
195+
spaceId = spaceId,
196+
isActionSetFolderAvailableOfflineOrSynchronize = false
197+
)
198+
} catch (e: Exception) {
199+
Timber.e(e, "Error refreshing folder ${folder.remotePath}")
200+
}
201+
202+
// Now get ALL content from local database (this returns everything, not just changes)
203+
val folderContent = fileRepository.getFolderContent(folderId)
204+
205+
Timber.d("Folder ${folder.remotePath} contains ${folderContent.size} items")
206+
207+
folderContent.forEach { item ->
208+
if (item.isFolder) {
209+
// Recursively process subfolders
210+
processFolderRecursively(accountName, item, spaceId)
211+
} else {
212+
// Process file
213+
processFile(accountName, item)
214+
}
215+
}
216+
217+
// Update notification periodically
218+
if (foldersProcessed % 5 == 0) {
219+
updateNotification("Scanning: $foldersProcessed folders, $totalFilesFound files found")
220+
}
221+
} catch (e: Exception) {
222+
Timber.e(e, "Error processing folder ${folder.remotePath}")
223+
}
224+
}
225+
226+
/**
227+
* Processes a single file: checks if it's already local,
228+
* and if not, enqueues a download.
229+
*/
230+
private fun processFile(accountName: String, file: OCFile) {
231+
totalFilesFound++
232+
233+
try {
234+
if (file.isAvailableLocally) {
235+
// File is already downloaded
236+
filesAlreadyLocal++
237+
Timber.d("File already local: ${file.fileName}")
238+
} else {
239+
// Enqueue download
240+
val downloadId = downloadFileUseCase(DownloadFileUseCase.Params(accountName, file))
241+
if (downloadId != null) {
242+
filesDownloaded++
243+
Timber.i("Enqueued download for: ${file.fileName}")
244+
} else {
245+
filesSkipped++
246+
Timber.d("Download already enqueued or skipped: ${file.fileName}")
247+
}
248+
}
249+
250+
// Update notification periodically (every 20 files)
251+
if (totalFilesFound % 20 == 0) {
252+
updateNotification("Found: $totalFilesFound files, $filesDownloaded queued for download")
253+
}
254+
} catch (e: Exception) {
255+
filesSkipped++
256+
Timber.e(e, "Error processing file ${file.fileName}")
257+
}
258+
}
259+
260+
private fun createNotificationChannel() {
261+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
262+
val channel = NotificationChannel(
263+
NOTIFICATION_CHANNEL_ID,
264+
"Download Everything",
265+
NotificationManager.IMPORTANCE_LOW
266+
).apply {
267+
description = "Shows progress when downloading all files"
268+
}
269+
val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
270+
notificationManager.createNotificationChannel(channel)
271+
}
272+
}
273+
274+
private fun updateNotification(contentText: String) {
275+
try {
276+
val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
277+
val notification = NotificationCompat.Builder(appContext, NOTIFICATION_CHANNEL_ID)
278+
.setContentTitle("Download Everything")
279+
.setContentText(contentText)
280+
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
281+
.setSmallIcon(R.drawable.notification_icon)
282+
.setOngoing(true)
283+
.setProgress(0, 0, true)
284+
.build()
285+
286+
notificationManager.notify(NOTIFICATION_ID, notification)
287+
} catch (e: Exception) {
288+
Timber.e(e, "Error updating notification")
289+
}
123290
}
124291

125292
companion object {
126293
const val DOWNLOAD_EVERYTHING_WORKER = "DOWNLOAD_EVERYTHING_WORKER"
127294
const val repeatInterval: Long = 6L
128295
val repeatIntervalTimeUnit: TimeUnit = TimeUnit.HOURS
296+
297+
private const val NOTIFICATION_CHANNEL_ID = "download_everything_channel"
298+
private const val NOTIFICATION_ID = 9001
129299
}
130300
}

0 commit comments

Comments
 (0)