2121package eu.opencloud.android.workers
2222
2323import android.accounts.AccountManager
24+ import android.app.NotificationChannel
25+ import android.app.NotificationManager
2426import android.content.Context
27+ import android.os.Build
28+ import androidx.core.app.NotificationCompat
2529import androidx.work.CoroutineWorker
2630import androidx.work.WorkerParameters
2731import eu.opencloud.android.MainApp
32+ import eu.opencloud.android.R
2833import eu.opencloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
34+ import eu.opencloud.android.domain.files.FileRepository
2935import eu.opencloud.android.domain.files.model.OCFile
3036import eu.opencloud.android.domain.files.model.OCFile.Companion.ROOT_PATH
3137import eu.opencloud.android.domain.files.usecases.GetFileByRemotePathUseCase
3238import eu.opencloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase
3339import eu.opencloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase
3440import eu.opencloud.android.presentation.authentication.AccountUtils
35- import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase
41+ import eu.opencloud.android.usecases.transfers.downloads.DownloadFileUseCase
3642import org.koin.core.component.KoinComponent
3743import org.koin.core.component.inject
3844import timber.log.Timber
3945import 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 */
4558class 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