Skip to content

Commit 9b760aa

Browse files
Merge pull request #16744 from nextcloud/feat/sync-all
feat(sync): download folder including all nested files
2 parents 10f006e + b7a3fd0 commit 9b760aa

26 files changed

Lines changed: 650 additions & 642 deletions

app/src/androidTest/java/com/owncloud/android/FileIT.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public void testRenameFolder() throws IOException {
107107
folderPath,
108108
user,
109109
fileDataStorageManager,
110+
false,
110111
false)
111112
.execute(targetContext)
112113
.isSuccess());

app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,15 @@ class InternalTwoWaySyncWork(
6767
}
6868

6969
Log_OC.d(TAG, "Folder ${folder.remotePath}: started!")
70-
operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager, true)
70+
operation =
71+
SynchronizeFolderOperation(
72+
context,
73+
folder.remotePath,
74+
user,
75+
fileDataStorageManager,
76+
false,
77+
false
78+
)
7179
val operationResult = operation?.execute(context)
7280

7381
if (operationResult?.isSuccess == true) {

app/src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class OfflineSyncWork(
8484
true,
8585
context,
8686
storageManager,
87-
true,
87+
false,
8888
false
8989
)
9090
synchronizeFileOperation.execute(context)

app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt

Lines changed: 60 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
2020
import com.owncloud.android.lib.common.utils.Log_OC
2121
import com.owncloud.android.operations.DownloadFileOperation
2222
import com.owncloud.android.operations.DownloadType
23-
import com.owncloud.android.ui.helpers.FileOperationsHelper
23+
import com.owncloud.android.utils.FileStorageUtils
2424
import com.owncloud.android.utils.theme.ViewThemeUtils
2525
import kotlinx.coroutines.Dispatchers
26+
import kotlinx.coroutines.delay
2627
import kotlinx.coroutines.withContext
2728
import java.util.concurrent.ConcurrentHashMap
2829

29-
@Suppress("LongMethod", "TooGenericExceptionCaught")
30+
/**
31+
* Only used for downloading files in first level of the folder, not subfolders.
32+
*/
33+
@Suppress("LongMethod", "TooGenericExceptionCaught", "MagicNumber")
3034
class FolderDownloadWorker(
3135
private val accountManager: UserAccountManager,
3236
private val context: Context,
@@ -39,13 +43,11 @@ class FolderDownloadWorker(
3943
private const val TAG = "📂" + "FolderDownloadWorker"
4044
const val FOLDER_ID = "FOLDER_ID"
4145
const val ACCOUNT_NAME = "ACCOUNT_NAME"
42-
43-
private val pendingDownloads: MutableSet<Long> = ConcurrentHashMap.newKeySet<Long>()
44-
46+
private val pendingDownloads: MutableSet<Long> = ConcurrentHashMap.newKeySet()
4547
fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id)
4648
}
4749

48-
private val notificationManager = FolderDownloadWorkerNotificationManager(context, viewThemeUtils)
50+
private val notificationManager = FolderDownloadWorkerNotificationManager(context, true, viewThemeUtils)
4951
private val folderDownloadEventBroadcaster = FolderDownloadEventBroadcaster(context, localBroadcastManager)
5052
private lateinit var storageManager: FileDataStorageManager
5153

@@ -58,88 +60,99 @@ class FolderDownloadWorker(
5860

5961
val accountName = inputData.getString(ACCOUNT_NAME)
6062
if (accountName == null) {
61-
Log_OC.e(TAG, "failed accountName cannot be null")
63+
Log_OC.e(TAG, "failed: accountName cannot be null")
6264
return Result.failure()
6365
}
6466

6567
val optionalUser = accountManager.getUser(accountName)
6668
if (optionalUser.isEmpty) {
67-
Log_OC.e(TAG, "failed user is not present")
69+
Log_OC.e(TAG, "failed: user is not present")
6870
return Result.failure()
6971
}
7072

7173
val user = optionalUser.get()
7274
storageManager = FileDataStorageManager(user, context.contentResolver)
75+
7376
val folder = storageManager.getFileById(folderID)
7477
if (folder == null) {
75-
Log_OC.e(TAG, "failed folder cannot be nul")
78+
Log_OC.e(TAG, "failed: folder cannot be null")
7679
return Result.failure()
7780
}
7881

79-
Log_OC.d(TAG, "🕒 started for ${user.accountName} downloading ${folder.fileName}")
82+
Log_OC.d(TAG, "🕒 started for ${user.accountName} | folder=${folder.fileName}")
8083

8184
trySetForeground(folder)
82-
8385
folderDownloadEventBroadcaster.sendDownloadEnqueued(folder.fileId)
8486
pendingDownloads.add(folder.fileId)
8587

8688
val downloadHelper = FileDownloadHelper.instance()
8789

8890
return withContext(Dispatchers.IO) {
8991
try {
90-
val files = getFiles(folder, storageManager)
9192
val account = user.toOwnCloudAccount()
92-
val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(account, context)
93+
val client = OwnCloudClientManagerFactory.getDefaultSingleton()
94+
.getClientFor(account, context)
95+
val files = getFiles(folder, storageManager)
96+
if (files.isEmpty()) {
97+
Log_OC.d(TAG, "✅ no files need downloading")
98+
notificationManager.showCompletionNotification(folder.fileName, true)
99+
return@withContext Result.success()
100+
}
101+
102+
var overallSuccess = true
93103

94-
var result = true
95104
files.forEachIndexed { index, file ->
96-
if (!checkDiskSize(file)) {
105+
if (isStopped) {
106+
Log_OC.d(TAG, "⚠️ worker stopped mid-download, aborting remaining files")
97107
return@withContext Result.failure()
98108
}
99109

100-
withContext(Dispatchers.Main) {
101-
val notification = notificationManager.getProgressNotification(
102-
folder.fileName,
103-
file.fileName,
104-
index,
105-
files.size
106-
)
107-
notificationManager.showNotification(notification)
108-
109-
val foregroundInfo = notificationManager.getForegroundInfo(notification)
110-
setForeground(foregroundInfo)
110+
if (!FileStorageUtils.checkIfEnoughSpace(folder)) {
111+
notificationManager.showNotAvailableDiskSpace()
112+
return@withContext Result.failure()
111113
}
112114

115+
notificationManager.showProgressNotification(
116+
folder.fileName,
117+
file.fileName,
118+
index,
119+
files.size
120+
)
121+
113122
val operation = DownloadFileOperation(user, file, context)
114123
val operationResult = operation.execute(client)
124+
115125
if (operationResult?.isSuccess == true && operation.downloadType === DownloadType.DOWNLOAD) {
116126
getOCFile(operation)?.let { ocFile ->
117127
downloadHelper.saveFile(ocFile, operation, storageManager)
118128
}
119129
}
120130

121-
if (!operationResult.isSuccess) {
122-
result = false
131+
if (operationResult?.isSuccess != true) {
132+
Log_OC.w(TAG, "⚠️ download failed for ${file.remotePath}: ${operationResult?.logMessage}")
133+
overallSuccess = false
123134
}
124135
}
125136

126-
withContext(Dispatchers.Main) {
127-
notificationManager.showCompletionNotification(folder.fileName, result)
128-
}
137+
notificationManager.showCompletionNotification(folder.fileName, overallSuccess)
129138

130-
if (result) {
131-
Log_OC.d(TAG, "✅ completed")
139+
if (overallSuccess) {
140+
Log_OC.d(TAG, "✅ completed successfully")
132141
Result.success()
133142
} else {
134-
Log_OC.d(TAG, "failed")
143+
Log_OC.d(TAG, "completed with failures")
135144
Result.failure()
136145
}
137146
} catch (e: Exception) {
138-
Log_OC.d(TAG, "❌ failed reason: $e")
147+
Log_OC.e(TAG, "❌ unexpected failure: $e")
148+
notificationManager.showCompletionNotification(folder.fileName, false)
139149
Result.failure()
140150
} finally {
141151
folderDownloadEventBroadcaster.sendDownloadCompleted(folder.fileId)
142152
pendingDownloads.remove(folder.fileId)
153+
154+
// delay so that user can see the error or success notification
155+
delay(2000)
143156
notificationManager.dismiss()
144157
}
145158
}
@@ -155,11 +168,12 @@ class FolderDownloadWorker(
155168
return notificationManager.getForegroundInfo(null)
156169
}
157170

158-
val folder = storageManager.getFileById(folderID) ?: return notificationManager.getForegroundInfo(null)
171+
val folder = storageManager.getFileById(folderID)
172+
?: return notificationManager.getForegroundInfo(null)
159173

160-
return notificationManager.getForegroundInfo(folder)
174+
notificationManager.getForegroundInfo(folder)
161175
} catch (e: Exception) {
162-
Log_OC.w(TAG, "⚠️ Error getting foreground info: ${e.message}")
176+
Log_OC.w(TAG, "⚠️ error getting foreground info: ${e.message}")
163177
notificationManager.getForegroundInfo(null)
164178
}
165179
}
@@ -169,34 +183,20 @@ class FolderDownloadWorker(
169183
val foregroundInfo = notificationManager.getForegroundInfo(folder)
170184
setForeground(foregroundInfo)
171185
} catch (e: Exception) {
172-
Log_OC.w(TAG, "⚠️ Could not set foreground service: ${e.message}")
186+
Log_OC.w(TAG, "⚠️ could not set foreground service: ${e.message}")
173187
}
174188
}
175189

176-
private fun getOCFile(operation: DownloadFileOperation): OCFile? {
177-
val file = operation.file?.fileId?.let { storageManager.getFileById(it) }
178-
?: storageManager.getFileByDecryptedRemotePath(operation.file?.remotePath)
179-
?: run {
180-
Log_OC.e(TAG, "could not save ${operation.file?.remotePath}")
181-
return null
182-
}
183-
184-
return file
190+
private fun getOCFile(operation: DownloadFileOperation): OCFile? = operation.file?.fileId?.let {
191+
storageManager.getFileById(it)
185192
}
193+
?: storageManager.getFileByDecryptedRemotePath(operation.file?.remotePath)
194+
?: run {
195+
Log_OC.e(TAG, "could not resolve OCFile for save: ${operation.file?.remotePath}")
196+
null
197+
}
186198

187199
private fun getFiles(folder: OCFile, storageManager: FileDataStorageManager): List<OCFile> =
188200
storageManager.getFolderContent(folder, false)
189201
.filter { !it.isFolder && !it.isDown }
190-
191-
private fun checkDiskSize(file: OCFile): Boolean {
192-
val fileSizeInByte = file.fileLength
193-
val availableDiskSpace = FileOperationsHelper.getAvailableSpaceOnDevice()
194-
195-
return if (availableDiskSpace < fileSizeInByte) {
196-
notificationManager.showNotAvailableDiskSpace()
197-
false
198-
} else {
199-
true
200-
}
201-
}
202202
}

app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerNotificationManager.kt

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,26 @@ import android.content.Context
1313
import android.content.Intent
1414
import androidx.work.ForegroundInfo
1515
import com.nextcloud.client.jobs.notification.WorkerNotificationManager
16+
import com.nextcloud.utils.extensions.mainThread
1617
import com.owncloud.android.R
1718
import com.owncloud.android.datamodel.OCFile
1819
import com.owncloud.android.ui.notifications.NotificationUtils
1920
import com.owncloud.android.utils.theme.ViewThemeUtils
21+
import kotlinx.coroutines.Dispatchers
22+
import kotlinx.coroutines.withContext
2023
import kotlin.random.Random
2124

22-
class FolderDownloadWorkerNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) :
23-
WorkerNotificationManager(
24-
id = NOTIFICATION_ID,
25-
context,
26-
viewThemeUtils,
27-
tickerId = R.string.folder_download_worker_ticker_id,
28-
channelId = NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD
29-
) {
25+
class FolderDownloadWorkerNotificationManager(
26+
private val context: Context,
27+
private val showCancel: Boolean = true,
28+
viewThemeUtils: ViewThemeUtils?
29+
) : WorkerNotificationManager(
30+
id = NOTIFICATION_ID,
31+
context,
32+
viewThemeUtils,
33+
tickerId = R.string.folder_download_worker_ticker_id,
34+
channelId = NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD
35+
) {
3036

3137
companion object {
3238
private const val NOTIFICATION_ID = 391
@@ -42,11 +48,13 @@ class FolderDownloadWorkerNotificationManager(private val context: Context, view
4248

4349
if (progress != null) {
4450
setProgress(MAX_PROGRESS, progress, false)
45-
addAction(
46-
R.drawable.ic_cancel,
47-
context.getString(R.string.common_cancel),
48-
getCancelPendingIntent()
49-
)
51+
if (showCancel) {
52+
addAction(
53+
R.drawable.ic_cancel,
54+
context.getString(R.string.common_cancel),
55+
getCancelPendingIntent()
56+
)
57+
}
5058
} else {
5159
setProgress(0, 0, false)
5260
}
@@ -66,35 +74,36 @@ class FolderDownloadWorkerNotificationManager(private val context: Context, view
6674
}
6775

6876
@Suppress("MagicNumber")
69-
fun getProgressNotification(
70-
folderName: String,
71-
filename: String,
72-
currentIndex: Int,
73-
totalFileSize: Int
74-
): Notification {
75-
val currentFileIndex = (currentIndex + 1)
76-
val description = context.getString(R.string.folder_download_counter, currentFileIndex, totalFileSize, filename)
77-
val progress = (currentFileIndex * MAX_PROGRESS) / totalFileSize
78-
return getNotification(folderName, description, progress)
77+
fun showProgressNotification(folderName: String, filename: String, currentIndex: Int, totalFileSize: Int) {
78+
mainThread(delay = 0) {
79+
val currentFileIndex = (currentIndex + 1)
80+
val description =
81+
context.getString(R.string.folder_download_counter, currentFileIndex, totalFileSize, filename)
82+
val progress = (currentFileIndex * MAX_PROGRESS) / totalFileSize
83+
val notification = getNotification(folderName, description, progress)
84+
showNotification(notification)
85+
}
7986
}
8087

8188
fun showCompletionNotification(folderName: String, success: Boolean) {
82-
val titleId = if (success) {
83-
R.string.folder_download_success_notification_title
84-
} else {
85-
R.string.folder_download_error_notification_title
86-
}
89+
mainThread(delay = 0) {
90+
val titleId = if (success) {
91+
R.string.folder_download_success_notification_title
92+
} else {
93+
R.string.folder_download_error_notification_title
94+
}
8795

88-
val title = context.getString(titleId, folderName)
96+
val title = context.getString(titleId, folderName)
8997

90-
val notification = getNotification(title)
91-
notificationManager.notify(NOTIFICATION_ID, notification)
98+
val notification = getNotification(title)
99+
notificationManager.notify(NOTIFICATION_ID, notification)
100+
}
92101
}
93102

94-
fun showNotAvailableDiskSpace() {
103+
suspend fun showNotAvailableDiskSpace() = withContext(Dispatchers.Main) {
95104
val title = context.getString(R.string.folder_download_insufficient_disk_space_notification_title)
96105
val notification = getNotification(title)
97-
notificationManager.notify(NOTIFICATION_ID, notification)
106+
notificationManager.notify(Random.nextInt(), notification)
98107
}
99108

100109
fun getForegroundInfo(folder: OCFile?): ForegroundInfo {

app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import com.owncloud.android.utils.theme.ViewThemeUtils
2323
open class WorkerNotificationManager(
2424
private val id: Int,
2525
private val context: Context,
26-
viewThemeUtils: ViewThemeUtils,
26+
viewThemeUtils: ViewThemeUtils?,
2727
private val tickerId: Int,
2828
channelId: String
2929
) {
@@ -42,7 +42,7 @@ open class WorkerNotificationManager(
4242
setVibrate(null)
4343
setOnlyAlertOnce(true)
4444
setSilent(true)
45-
viewThemeUtils.androidx.themeNotificationCompatBuilder(context, this)
45+
viewThemeUtils?.androidx?.themeNotificationCompatBuilder(context, this)
4646
}
4747

4848
fun showNotification() {

0 commit comments

Comments
 (0)