main branch updated
This commit is contained in:
parent
3d33d3fe49
commit
9a05dc1657
353 changed files with 16802 additions and 2995 deletions
|
|
@ -17,14 +17,17 @@ import androidx.work.WorkerFactory
|
|||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.core.Clock
|
||||
import com.nextcloud.client.device.DeviceInfo
|
||||
import com.nextcloud.client.database.NextcloudDatabase
|
||||
import com.nextcloud.client.device.PowerManagementService
|
||||
import com.nextcloud.client.documentscan.GeneratePDFUseCase
|
||||
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
|
||||
import com.nextcloud.client.integrations.deck.DeckApi
|
||||
import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker
|
||||
import com.nextcloud.client.jobs.autoUpload.FileSystemRepository
|
||||
import com.nextcloud.client.jobs.download.FileDownloadWorker
|
||||
import com.nextcloud.client.jobs.metadata.MetadataWorker
|
||||
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
|
||||
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
|
|
@ -50,7 +53,6 @@ class BackgroundJobFactory @Inject constructor(
|
|||
private val clock: Clock,
|
||||
private val powerManagementService: PowerManagementService,
|
||||
private val backgroundJobManager: Provider<BackgroundJobManager>,
|
||||
private val deviceInfo: DeviceInfo,
|
||||
private val accountManager: UserAccountManager,
|
||||
private val resources: Resources,
|
||||
private val arbitraryDataProvider: ArbitraryDataProvider,
|
||||
|
|
@ -62,7 +64,8 @@ class BackgroundJobFactory @Inject constructor(
|
|||
private val viewThemeUtils: Provider<ViewThemeUtils>,
|
||||
private val localBroadcastManager: Provider<LocalBroadcastManager>,
|
||||
private val generatePdfUseCase: GeneratePDFUseCase,
|
||||
private val syncedFolderProvider: SyncedFolderProvider
|
||||
private val syncedFolderProvider: SyncedFolderProvider,
|
||||
private val database: NextcloudDatabase
|
||||
) : WorkerFactory() {
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
|
|
@ -84,7 +87,7 @@ class BackgroundJobFactory @Inject constructor(
|
|||
when (workerClass) {
|
||||
ContactsBackupWork::class -> createContactsBackupWork(context, workerParameters)
|
||||
ContactsImportWork::class -> createContactsImportWork(context, workerParameters)
|
||||
FilesSyncWork::class -> createFilesSyncWork(context, workerParameters)
|
||||
AutoUploadWorker::class -> createFilesSyncWork(context, workerParameters)
|
||||
OfflineSyncWork::class -> createOfflineSyncWork(context, workerParameters)
|
||||
MediaFoldersDetectionWork::class -> createMediaFoldersDetectionWork(context, workerParameters)
|
||||
NotificationWork::class -> createNotificationWork(context, workerParameters)
|
||||
|
|
@ -100,6 +103,7 @@ class BackgroundJobFactory @Inject constructor(
|
|||
OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters)
|
||||
InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters)
|
||||
MetadataWorker::class -> createMetadataWorker(context, workerParameters)
|
||||
FolderDownloadWorker::class -> createFolderDownloadWorker(context, workerParameters)
|
||||
else -> null // caller falls back to default factory
|
||||
}
|
||||
}
|
||||
|
|
@ -166,16 +170,16 @@ class BackgroundJobFactory @Inject constructor(
|
|||
contentResolver
|
||||
)
|
||||
|
||||
private fun createFilesSyncWork(context: Context, params: WorkerParameters): FilesSyncWork = FilesSyncWork(
|
||||
private fun createFilesSyncWork(context: Context, params: WorkerParameters): AutoUploadWorker = AutoUploadWorker(
|
||||
context = context,
|
||||
params = params,
|
||||
contentResolver = contentResolver,
|
||||
userAccountManager = accountManager,
|
||||
uploadsStorageManager = uploadsStorageManager,
|
||||
connectivityService = connectivityService,
|
||||
powerManagementService = powerManagementService,
|
||||
syncedFolderProvider = syncedFolderProvider,
|
||||
backgroundJobManager = backgroundJobManager.get()
|
||||
backgroundJobManager = backgroundJobManager.get(),
|
||||
repository = FileSystemRepository(dao = database.fileSystemDao())
|
||||
)
|
||||
|
||||
private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork = OfflineSyncWork(
|
||||
|
|
@ -285,4 +289,12 @@ class BackgroundJobFactory @Inject constructor(
|
|||
params,
|
||||
accountManager.user
|
||||
)
|
||||
|
||||
private fun createFolderDownloadWorker(context: Context, params: WorkerParameters): FolderDownloadWorker =
|
||||
FolderDownloadWorker(
|
||||
accountManager,
|
||||
context,
|
||||
viewThemeUtils.get(),
|
||||
params
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.work.ListenableWorker
|
||||
import com.nextcloud.client.account.User
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.SyncedFolder
|
||||
import com.owncloud.android.operations.DownloadType
|
||||
|
||||
/**
|
||||
|
|
@ -119,15 +120,12 @@ interface BackgroundJobManager {
|
|||
|
||||
fun startImmediateFilesExportJob(files: Collection<OCFile>): LiveData<JobInfo?>
|
||||
|
||||
fun schedulePeriodicFilesSyncJob(syncedFolderID: Long)
|
||||
fun schedulePeriodicFilesSyncJob(syncedFolder: SyncedFolder)
|
||||
|
||||
/**
|
||||
* Immediately start File Sync job for given syncFolderID.
|
||||
*/
|
||||
fun startImmediateFilesSyncJob(
|
||||
syncedFolderID: Long,
|
||||
fun startAutoUploadImmediately(
|
||||
syncedFolder: SyncedFolder,
|
||||
overridePowerSaving: Boolean = false,
|
||||
changedFiles: Array<String?> = arrayOf<String?>()
|
||||
contentUris: Array<String?> = arrayOf()
|
||||
)
|
||||
|
||||
fun cancelTwoWaySyncJob()
|
||||
|
|
@ -142,12 +140,10 @@ interface BackgroundJobManager {
|
|||
fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean)
|
||||
fun getFileUploads(user: User): LiveData<List<JobInfo>>
|
||||
fun cancelFilesUploadJob(user: User)
|
||||
fun isStartFileUploadJobScheduled(user: User): Boolean
|
||||
fun isStartFileUploadJobScheduled(accountName: String): Boolean
|
||||
|
||||
fun cancelFilesDownloadJob(user: User, fileId: Long)
|
||||
|
||||
fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun startFileDownloadJob(
|
||||
user: User,
|
||||
|
|
@ -175,4 +171,6 @@ interface BackgroundJobManager {
|
|||
fun scheduleInternal2WaySync(intervalMinutes: Long)
|
||||
fun cancelAllFilesDownloadJobs()
|
||||
fun startMetadataSyncJob(currentDirPath: String)
|
||||
fun downloadFolder(folder: OCFile, accountName: String)
|
||||
fun cancelFolderDownload()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ import com.nextcloud.client.account.User
|
|||
import com.nextcloud.client.core.Clock
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
|
||||
import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker
|
||||
import com.nextcloud.client.jobs.download.FileDownloadWorker
|
||||
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
|
||||
import com.nextcloud.client.jobs.metadata.MetadataWorker
|
||||
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper
|
||||
|
|
@ -35,6 +37,7 @@ import com.nextcloud.client.preferences.AppPreferences
|
|||
import com.nextcloud.utils.extensions.isWorkRunning
|
||||
import com.nextcloud.utils.extensions.isWorkScheduled
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.SyncedFolder
|
||||
import com.owncloud.android.operations.DownloadType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -91,6 +94,7 @@ internal class BackgroundJobManagerImpl(
|
|||
const val JOB_PERIODIC_OFFLINE_OPERATIONS = "periodic_offline_operations"
|
||||
const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
|
||||
const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"
|
||||
const val JOB_DOWNLOAD_FOLDER = "download_folder"
|
||||
const val JOB_METADATA_SYNC = "metadata_sync"
|
||||
const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync"
|
||||
|
||||
|
|
@ -472,41 +476,68 @@ internal class BackgroundJobManagerImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun schedulePeriodicFilesSyncJob(syncedFolderID: Long) {
|
||||
override fun schedulePeriodicFilesSyncJob(syncedFolder: SyncedFolder) {
|
||||
val syncedFolderID = syncedFolder.id
|
||||
|
||||
val arguments = Data.Builder()
|
||||
.putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
.putLong(AutoUploadWorker.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
.build()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresCharging(syncedFolder.isChargingOnly)
|
||||
.build()
|
||||
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = FilesSyncWork::class,
|
||||
jobClass = AutoUploadWorker::class,
|
||||
jobName = JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID,
|
||||
intervalMins = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES
|
||||
intervalMins = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
|
||||
constraints = constraints
|
||||
)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.LINEAR,
|
||||
DEFAULT_BACKOFF_CRITERIA_DELAY_SEC,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
.setInputData(arguments)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun startImmediateFilesSyncJob(
|
||||
syncedFolderID: Long,
|
||||
override fun startAutoUploadImmediately(
|
||||
syncedFolder: SyncedFolder,
|
||||
overridePowerSaving: Boolean,
|
||||
changedFiles: Array<String?>
|
||||
contentUris: Array<String?>
|
||||
) {
|
||||
val syncedFolderID = syncedFolder.id
|
||||
|
||||
val arguments = Data.Builder()
|
||||
.putBoolean(FilesSyncWork.OVERRIDE_POWER_SAVING, overridePowerSaving)
|
||||
.putStringArray(FilesSyncWork.CHANGED_FILES, changedFiles)
|
||||
.putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
.putBoolean(AutoUploadWorker.OVERRIDE_POWER_SAVING, overridePowerSaving)
|
||||
.putStringArray(AutoUploadWorker.CONTENT_URIS, contentUris)
|
||||
.putLong(AutoUploadWorker.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
.build()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresCharging(syncedFolder.isChargingOnly)
|
||||
.build()
|
||||
|
||||
val request = oneTimeRequestBuilder(
|
||||
jobClass = FilesSyncWork::class,
|
||||
jobClass = AutoUploadWorker::class,
|
||||
jobName = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID
|
||||
)
|
||||
.setInputData(arguments)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.LINEAR,
|
||||
DEFAULT_BACKOFF_CRITERIA_DELAY_SEC,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
|
|
@ -606,10 +637,10 @@ internal class BackgroundJobManagerImpl(
|
|||
workManager.enqueue(request)
|
||||
}
|
||||
|
||||
private fun startFileUploadJobTag(user: User): String = JOB_FILES_UPLOAD + user.accountName
|
||||
private fun startFileUploadJobTag(accountName: String): String = JOB_FILES_UPLOAD + accountName
|
||||
|
||||
override fun isStartFileUploadJobScheduled(user: User): Boolean =
|
||||
workManager.isWorkScheduled(startFileUploadJobTag(user))
|
||||
override fun isStartFileUploadJobScheduled(accountName: String): Boolean =
|
||||
workManager.isWorkScheduled(startFileUploadJobTag(accountName))
|
||||
|
||||
/**
|
||||
* This method supports initiating uploads for various scenarios, including:
|
||||
|
|
@ -627,7 +658,7 @@ internal class BackgroundJobManagerImpl(
|
|||
defaultDispatcherScope.launch {
|
||||
val batchSize = FileUploadHelper.MAX_FILE_COUNT
|
||||
val batches = uploadIds.toList().chunked(batchSize)
|
||||
val tag = startFileUploadJobTag(user)
|
||||
val tag = startFileUploadJobTag(user.accountName)
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
|
|
@ -673,9 +704,6 @@ internal class BackgroundJobManagerImpl(
|
|||
private fun startFileDownloadJobTag(user: User, fileId: Long): String =
|
||||
JOB_FOLDER_DOWNLOAD + user.accountName + fileId
|
||||
|
||||
override fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean =
|
||||
workManager.isWorkScheduled(startFileDownloadJobTag(user, fileId))
|
||||
|
||||
override fun startFileDownloadJob(
|
||||
user: User,
|
||||
file: OCFile,
|
||||
|
|
@ -795,4 +823,28 @@ internal class BackgroundJobManagerImpl(
|
|||
|
||||
workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
}
|
||||
|
||||
override fun downloadFolder(folder: OCFile, accountName: String) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresStorageNotLow(true)
|
||||
.build()
|
||||
|
||||
val data = Data.Builder()
|
||||
.putLong(FolderDownloadWorker.FOLDER_ID, folder.fileId)
|
||||
.putString(FolderDownloadWorker.ACCOUNT_NAME, accountName)
|
||||
.build()
|
||||
|
||||
val request = oneTimeRequestBuilder(FolderDownloadWorker::class, JOB_DOWNLOAD_FOLDER)
|
||||
.addTag(JOB_DOWNLOAD_FOLDER)
|
||||
.setInputData(data)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(JOB_DOWNLOAD_FOLDER, ExistingWorkPolicy.APPEND_OR_REPLACE, request)
|
||||
}
|
||||
|
||||
override fun cancelFolderDownload() {
|
||||
workManager.cancelAllWorkByTag(JOB_DOWNLOAD_FOLDER)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.device.PowerManagementService
|
||||
import com.nextcloud.utils.ForegroundServiceHelper
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.ForegroundServiceType
|
||||
import com.owncloud.android.datamodel.SyncedFolderProvider
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.FilesSyncHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* This work is triggered when OS detects change in media folders.
|
||||
|
|
@ -21,53 +30,113 @@ import com.owncloud.android.utils.FilesSyncHelper
|
|||
*
|
||||
* This job must not be started on API < 24.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
class ContentObserverWork(
|
||||
appContext: Context,
|
||||
private val context: Context,
|
||||
private val params: WorkerParameters,
|
||||
private val syncedFolderProvider: SyncedFolderProvider,
|
||||
private val powerManagementService: PowerManagementService,
|
||||
private val backgroundJobManager: BackgroundJobManager
|
||||
) : Worker(appContext, params) {
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
|
||||
|
||||
if (params.triggeredContentUris.isNotEmpty()) {
|
||||
Log_OC.d(TAG, "File-sync Content Observer detected files change")
|
||||
checkAndStartFileSyncJob()
|
||||
backgroundJobManager.startMediaFoldersDetectionJob()
|
||||
} else {
|
||||
Log_OC.d(TAG, "triggeredContentUris empty")
|
||||
}
|
||||
recheduleSelf()
|
||||
|
||||
val result = Result.success()
|
||||
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
|
||||
return result
|
||||
companion object {
|
||||
private const val TAG = "🔍" + "ContentObserverWork"
|
||||
private const val CHANNEL_ID = NotificationUtils.NOTIFICATION_CHANNEL_CONTENT_OBSERVER
|
||||
private const val NOTIFICATION_ID = 774
|
||||
}
|
||||
|
||||
private fun recheduleSelf() {
|
||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||
val workerName = BackgroundJobManagerImpl.formatClassTag(this@ContentObserverWork::class)
|
||||
backgroundJobManager.logStartOfWorker(workerName)
|
||||
Log_OC.d(TAG, "started")
|
||||
|
||||
try {
|
||||
if (params.triggeredContentUris.isNotEmpty()) {
|
||||
Log_OC.d(TAG, "📸 content observer detected file changes.")
|
||||
|
||||
val notificationTitle = context.getString(R.string.content_observer_work_notification_title)
|
||||
val notification = createNotification(notificationTitle)
|
||||
updateForegroundInfo(notification)
|
||||
checkAndTriggerAutoUpload()
|
||||
|
||||
// prevent worker fail because of another worker
|
||||
try {
|
||||
backgroundJobManager.startMediaFoldersDetectionJob()
|
||||
} catch (e: Exception) {
|
||||
Log_OC.d(TAG, "⚠️ media folder detection job failed :$e")
|
||||
}
|
||||
} else {
|
||||
Log_OC.d(TAG, "⚠️ triggeredContentUris is empty — nothing to sync.")
|
||||
}
|
||||
|
||||
rescheduleSelf()
|
||||
|
||||
val result = Result.success()
|
||||
backgroundJobManager.logEndOfWorker(workerName, result)
|
||||
Log_OC.d(TAG, "finished")
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "❌ Exception in ContentObserverWork: ${e.message}", e)
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateForegroundInfo(notification: Notification) {
|
||||
val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
ForegroundServiceType.DataSync
|
||||
)
|
||||
setForeground(foregroundInfo)
|
||||
}
|
||||
|
||||
private fun createNotification(title: String): Notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setSmallIcon(R.drawable.ic_find_in_page)
|
||||
.setOngoing(true)
|
||||
.setSound(null)
|
||||
.setVibrate(null)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setSilent(true)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Re-schedules this observer to ensure continuous monitoring of media changes.
|
||||
*/
|
||||
private fun rescheduleSelf() {
|
||||
Log_OC.d(TAG, "🔁 Rescheduling ContentObserverWork for continued observation.")
|
||||
backgroundJobManager.scheduleContentObserverJob()
|
||||
}
|
||||
|
||||
private fun checkAndStartFileSyncJob() {
|
||||
if (!powerManagementService.isPowerSavingEnabled && syncedFolderProvider.countEnabledSyncedFolders() > 0) {
|
||||
val changedFiles = mutableListOf<String>()
|
||||
for (uri in params.triggeredContentUris) {
|
||||
changedFiles.add(uri.toString())
|
||||
}
|
||||
FilesSyncHelper.startFilesSyncForAllFolders(
|
||||
private suspend fun checkAndTriggerAutoUpload() = withContext(Dispatchers.IO) {
|
||||
if (powerManagementService.isPowerSavingEnabled) {
|
||||
Log_OC.w(TAG, "⚡ Power saving mode active — skipping file sync.")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val enabledFoldersCount = syncedFolderProvider.countEnabledSyncedFolders()
|
||||
if (enabledFoldersCount <= 0) {
|
||||
Log_OC.w(TAG, "🚫 No enabled synced folders found — skipping file sync.")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val contentUris = params.triggeredContentUris.map { uri ->
|
||||
// adds uri strings e.g. content://media/external/images/media/2281
|
||||
uri.toString()
|
||||
}.toTypedArray()
|
||||
Log_OC.d(TAG, "📄 Content uris detected")
|
||||
|
||||
try {
|
||||
FilesSyncHelper.startAutoUploadImmediatelyWithContentUris(
|
||||
syncedFolderProvider,
|
||||
backgroundJobManager,
|
||||
false,
|
||||
changedFiles.toTypedArray()
|
||||
contentUris
|
||||
)
|
||||
} else {
|
||||
Log_OC.w(TAG, "cant startFilesSyncForAllFolders")
|
||||
Log_OC.d(TAG, "✅ auto upload triggered successfully for ${contentUris.size} file(s).")
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "❌ Failed to start auto upload for changed files: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = ContentObserverWork::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.autoUpload
|
||||
|
||||
import com.nextcloud.utils.extensions.shouldSkipFile
|
||||
import com.nextcloud.utils.extensions.toLocalPath
|
||||
import com.owncloud.android.datamodel.FilesystemDataProvider
|
||||
import com.owncloud.android.datamodel.SyncedFolder
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import java.io.IOException
|
||||
import java.nio.file.AccessDeniedException
|
||||
import java.nio.file.FileVisitOption
|
||||
import java.nio.file.FileVisitResult
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.SimpleFileVisitor
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "MagicNumber", "ReturnCount")
|
||||
class AutoUploadHelper {
|
||||
companion object {
|
||||
private const val TAG = "AutoUploadHelper"
|
||||
private const val MAX_DEPTH = 100
|
||||
}
|
||||
|
||||
fun insertCustomFolderIntoDB(folder: SyncedFolder, filesystemDataProvider: FilesystemDataProvider?): Int {
|
||||
val path = Paths.get(folder.localPath)
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
Log_OC.w(TAG, "Folder does not exist: ${folder.localPath}")
|
||||
return 0
|
||||
}
|
||||
|
||||
if (!Files.isReadable(path)) {
|
||||
Log_OC.w(TAG, "Folder is not readable: ${folder.localPath}")
|
||||
return 0
|
||||
}
|
||||
|
||||
val excludeHidden = folder.isExcludeHidden
|
||||
|
||||
var fileCount = 0
|
||||
var skipCount = 0
|
||||
var errorCount = 0
|
||||
|
||||
try {
|
||||
Files.walkFileTree(
|
||||
path,
|
||||
setOf(FileVisitOption.FOLLOW_LINKS),
|
||||
MAX_DEPTH,
|
||||
object : SimpleFileVisitor<Path>() {
|
||||
|
||||
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes?): FileVisitResult {
|
||||
if (excludeHidden && dir != path && dir.toFile().isHidden) {
|
||||
Log_OC.d(TAG, "Skipping hidden directory: ${dir.fileName}")
|
||||
skipCount++
|
||||
return FileVisitResult.SKIP_SUBTREE
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFile(file: Path, attrs: BasicFileAttributes?): FileVisitResult {
|
||||
try {
|
||||
val javaFile = file.toFile()
|
||||
val lastModified = attrs?.lastModifiedTime()?.toMillis() ?: javaFile.lastModified()
|
||||
val creationTime = attrs?.creationTime()?.toMillis()
|
||||
|
||||
if (folder.shouldSkipFile(javaFile, lastModified, creationTime)) {
|
||||
skipCount++
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
val localPath = file.toLocalPath()
|
||||
|
||||
filesystemDataProvider?.storeOrUpdateFileValue(
|
||||
localPath,
|
||||
lastModified,
|
||||
javaFile.isDirectory,
|
||||
folder
|
||||
)
|
||||
|
||||
fileCount++
|
||||
|
||||
if (fileCount % 100 == 0) {
|
||||
Log_OC.d(TAG, "Processed $fileCount files so far...")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Error processing file: $file", e)
|
||||
errorCount++
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFileFailed(file: Path, exc: IOException?): FileVisitResult {
|
||||
when (exc) {
|
||||
is AccessDeniedException -> {
|
||||
Log_OC.w(TAG, "Access denied: $file")
|
||||
}
|
||||
else -> {
|
||||
Log_OC.e(TAG, "Failed to visit file: $file", exc)
|
||||
}
|
||||
}
|
||||
errorCount++
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
|
||||
if (exc != null) {
|
||||
Log_OC.e(TAG, "Error after visiting directory: $dir", exc)
|
||||
errorCount++
|
||||
}
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Log_OC.d(
|
||||
TAG,
|
||||
"Scan complete for ${folder.localPath}: " +
|
||||
"$fileCount files processed, $skipCount skipped, $errorCount errors"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Error walking file tree: ${folder.localPath}", e)
|
||||
}
|
||||
|
||||
return fileCount
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,480 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.autoUpload
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.database.entity.UploadEntity
|
||||
import com.nextcloud.client.database.entity.toOCUpload
|
||||
import com.nextcloud.client.database.entity.toUploadEntity
|
||||
import com.nextcloud.client.device.PowerManagementService
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
import com.nextcloud.client.preferences.SubFolderRule
|
||||
import com.nextcloud.utils.ForegroundServiceHelper
|
||||
import com.nextcloud.utils.extensions.updateStatus
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.ForegroundServiceType
|
||||
import com.owncloud.android.datamodel.MediaFolderType
|
||||
import com.owncloud.android.datamodel.SyncedFolder
|
||||
import com.owncloud.android.datamodel.SyncedFolderProvider
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager
|
||||
import com.owncloud.android.db.OCUpload
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount
|
||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.UploadFileOperation
|
||||
import com.owncloud.android.ui.activity.SettingsActivity
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.FileStorageUtils
|
||||
import com.owncloud.android.utils.FilesSyncHelper
|
||||
import com.owncloud.android.utils.MimeType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.ParsePosition
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
class AutoUploadWorker(
|
||||
private val context: Context,
|
||||
params: WorkerParameters,
|
||||
private val userAccountManager: UserAccountManager,
|
||||
private val uploadsStorageManager: UploadsStorageManager,
|
||||
private val connectivityService: ConnectivityService,
|
||||
private val powerManagementService: PowerManagementService,
|
||||
private val syncedFolderProvider: SyncedFolderProvider,
|
||||
private val backgroundJobManager: BackgroundJobManager,
|
||||
private val repository: FileSystemRepository
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "🔄📤" + "AutoUpload"
|
||||
const val OVERRIDE_POWER_SAVING = "overridePowerSaving"
|
||||
const val CONTENT_URIS = "content_uris"
|
||||
const val SYNCED_FOLDER_ID = "syncedFolderId"
|
||||
private const val CHANNEL_ID = NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD
|
||||
|
||||
private const val NOTIFICATION_ID = 266
|
||||
}
|
||||
|
||||
private val helper = AutoUploadHelper()
|
||||
private lateinit var syncedFolder: SyncedFolder
|
||||
private val notificationManager by lazy {
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "ReturnCount")
|
||||
override suspend fun doWork(): Result {
|
||||
return try {
|
||||
val syncFolderId = inputData.getLong(SYNCED_FOLDER_ID, -1)
|
||||
syncedFolder = syncedFolderProvider.getSyncedFolderByID(syncFolderId)
|
||||
?.takeIf { it.isEnabled } ?: return Result.failure()
|
||||
|
||||
// initial notification
|
||||
val notification = createNotification(context.getString(R.string.upload_files))
|
||||
updateForegroundInfo(notification)
|
||||
|
||||
/**
|
||||
* Receives from [com.nextcloud.client.jobs.ContentObserverWork.checkAndTriggerAutoUpload]
|
||||
*/
|
||||
val contentUris = inputData.getStringArray(CONTENT_URIS)
|
||||
|
||||
if (canExitEarly(contentUris, syncFolderId)) {
|
||||
return Result.retry()
|
||||
}
|
||||
|
||||
collectFileChangesFromContentObserverWork(contentUris)
|
||||
updateNotification()
|
||||
uploadFiles(syncedFolder)
|
||||
|
||||
Log_OC.d(TAG, "✅ ${syncedFolder.remotePath} finished checking files.")
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "❌ failed: ${e.message}")
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
getStartNotificationTitle()?.let { (localFolderName, remoteFolderName) ->
|
||||
val startNotification = createNotification(
|
||||
context.getString(
|
||||
R.string.auto_upload_worker_start_text,
|
||||
localFolderName,
|
||||
remoteFolderName
|
||||
)
|
||||
)
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID, startNotification)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateForegroundInfo(notification: Notification) {
|
||||
val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
ForegroundServiceType.DataSync
|
||||
)
|
||||
setForeground(foregroundInfo)
|
||||
}
|
||||
|
||||
private fun createNotification(title: String): Notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setSmallIcon(R.drawable.uploads)
|
||||
.setOngoing(true)
|
||||
.setSound(null)
|
||||
.setVibrate(null)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSilent(true)
|
||||
.build()
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun getStartNotificationTitle(): Pair<String, String>? = try {
|
||||
val localPath = syncedFolder.localPath
|
||||
val remotePath = syncedFolder.remotePath
|
||||
if (localPath.isBlank() || remotePath.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
File(localPath).name to File(remotePath).name
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private fun canExitEarly(contentUris: Array<String>?, syncedFolderID: Long): Boolean {
|
||||
val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false)
|
||||
if ((powerManagementService.isPowerSavingEnabled && !overridePowerSaving)) {
|
||||
Log_OC.w(TAG, "⚡ Skipping: device is in power saving mode")
|
||||
return true
|
||||
}
|
||||
|
||||
if (syncedFolderID < 0) {
|
||||
Log_OC.e(TAG, "invalid sync folder id")
|
||||
return true
|
||||
}
|
||||
|
||||
if (backgroundJobManager.bothFilesSyncJobsRunning(syncedFolderID)) {
|
||||
Log_OC.w(TAG, "🚧 another worker is already running for $syncedFolderID")
|
||||
return true
|
||||
}
|
||||
|
||||
val totalScanInterval = syncedFolder.getTotalScanInterval(connectivityService, powerManagementService)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val passedScanInterval = totalScanInterval <= currentTime
|
||||
|
||||
Log_OC.d(TAG, "lastScanTimestampMs: " + syncedFolder.lastScanTimestampMs)
|
||||
Log_OC.d(TAG, "totalScanInterval: $totalScanInterval")
|
||||
Log_OC.d(TAG, "currentTime: $currentTime")
|
||||
Log_OC.d(TAG, "passedScanInterval: $passedScanInterval")
|
||||
|
||||
if (!passedScanInterval && contentUris.isNullOrEmpty() && !overridePowerSaving) {
|
||||
Log_OC.w(
|
||||
TAG,
|
||||
"skipped since started before scan interval and nothing todo: " + syncedFolder.localPath
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Instead of scanning the entire local folder, optional content URIs can be passed to the worker
|
||||
* to detect only the relevant changes.
|
||||
*/
|
||||
@Suppress("MagicNumber", "TooGenericExceptionCaught")
|
||||
private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array<String>?) = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (contentUris.isNullOrEmpty()) {
|
||||
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
|
||||
} else {
|
||||
val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris)
|
||||
if (!isContentUrisStored) {
|
||||
Log_OC.w(
|
||||
TAG,
|
||||
"changed content uris not stored, fallback to insert all db entries to not lose files"
|
||||
)
|
||||
|
||||
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
|
||||
}
|
||||
}
|
||||
syncedFolder.lastScanTimestampMs = System.currentTimeMillis()
|
||||
syncedFolderProvider.updateSyncFolder(syncedFolder)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.d(TAG, "Exception collectFileChangesFromContentObserverWork: $e")
|
||||
}
|
||||
|
||||
private fun prepareDateFormat(): SimpleDateFormat {
|
||||
val currentLocale = context.resources.configuration.locales[0]
|
||||
return SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale).apply {
|
||||
timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUserOrReturn(syncedFolder: SyncedFolder): User? {
|
||||
val optionalUser = userAccountManager.getUser(syncedFolder.account)
|
||||
if (!optionalUser.isPresent) {
|
||||
Log_OC.w(TAG, "user not present")
|
||||
return null
|
||||
}
|
||||
return optionalUser.get()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getUploadSettings(syncedFolder: SyncedFolder): Triple<Boolean, Boolean, Int> {
|
||||
val lightVersion = context.resources.getBoolean(R.bool.syncedFolder_light)
|
||||
val accountName = syncedFolder.account
|
||||
|
||||
return if (lightVersion) {
|
||||
Log_OC.d(TAG, "light version is used")
|
||||
val arbitraryDataProvider = ArbitraryDataProviderImpl(context)
|
||||
val needsCharging = context.resources.getBoolean(R.bool.syncedFolder_light_on_charging)
|
||||
val needsWifi = arbitraryDataProvider.getBooleanValue(
|
||||
accountName,
|
||||
SettingsActivity.SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI
|
||||
)
|
||||
val uploadActionString = context.resources.getString(R.string.syncedFolder_light_upload_behaviour)
|
||||
val uploadAction = getUploadAction(uploadActionString)
|
||||
Log_OC.d(TAG, "upload action is: $uploadAction")
|
||||
Triple(needsCharging, needsWifi, uploadAction)
|
||||
} else {
|
||||
Log_OC.d(TAG, "not light version is used")
|
||||
Triple(syncedFolder.isChargingOnly, syncedFolder.isWifiOnly, syncedFolder.uploadAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "DEPRECATION", "TooGenericExceptionCaught")
|
||||
private suspend fun uploadFiles(syncedFolder: SyncedFolder) = withContext(Dispatchers.IO) {
|
||||
val dateFormat = prepareDateFormat()
|
||||
val user = getUserOrReturn(syncedFolder) ?: return@withContext
|
||||
val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context)
|
||||
val client = OwnCloudClientManagerFactory.getDefaultSingleton()
|
||||
.getClientFor(ocAccount, context)
|
||||
val lightVersion = context.resources.getBoolean(R.bool.syncedFolder_light)
|
||||
val currentLocale = context.resources.configuration.locales[0]
|
||||
|
||||
var lastId = 0
|
||||
while (true) {
|
||||
val filePathsWithIds = repository.getFilePathsWithIds(syncedFolder, lastId)
|
||||
|
||||
if (filePathsWithIds.isEmpty()) {
|
||||
Log_OC.w(TAG, "no more files to upload at lastId: $lastId")
|
||||
break
|
||||
}
|
||||
Log_OC.d(TAG, "Processing batch: lastId=$lastId, count=${filePathsWithIds.size}")
|
||||
|
||||
filePathsWithIds.forEach { (path, id) ->
|
||||
val file = File(path)
|
||||
val localPath = file.absolutePath
|
||||
val remotePath = getRemotePath(
|
||||
file,
|
||||
syncedFolder,
|
||||
dateFormat,
|
||||
lightVersion,
|
||||
context.resources,
|
||||
currentLocale
|
||||
)
|
||||
|
||||
try {
|
||||
var (uploadEntity, upload) = createEntityAndUpload(user, localPath, remotePath)
|
||||
try {
|
||||
// Insert/update to IN_PROGRESS state before starting upload
|
||||
val generatedId = uploadsStorageManager.uploadDao.insertOrReplace(uploadEntity)
|
||||
uploadEntity = uploadEntity.copy(id = generatedId.toInt())
|
||||
upload.uploadId = generatedId
|
||||
|
||||
val operation = createUploadFileOperation(upload, user)
|
||||
Log_OC.d(TAG, "🕒 uploading: $localPath, id: $generatedId")
|
||||
|
||||
val result = operation.execute(client)
|
||||
uploadsStorageManager.updateStatus(uploadEntity, result.isSuccess)
|
||||
|
||||
if (result.isSuccess) {
|
||||
repository.markFileAsUploaded(localPath, syncedFolder)
|
||||
Log_OC.d(TAG, "✅ upload completed: $localPath")
|
||||
} else {
|
||||
Log_OC.e(
|
||||
TAG,
|
||||
"❌ upload failed $localPath (${upload.accountName}): ${result.logMessage}"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
uploadsStorageManager.updateStatus(
|
||||
uploadEntity,
|
||||
UploadsStorageManager.UploadStatus.UPLOAD_FAILED
|
||||
)
|
||||
Log_OC.e(
|
||||
TAG,
|
||||
"Exception during upload file, localPath: $localPath, remotePath: $remotePath," +
|
||||
" exception: $e"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(
|
||||
TAG,
|
||||
"Exception uploadFiles during creating entity and upload, localPath: $localPath, " +
|
||||
"remotePath: $remotePath, exception: $e"
|
||||
)
|
||||
}
|
||||
|
||||
// update last id so upload can continue where it left
|
||||
lastId = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEntityAndUpload(user: User, localPath: String, remotePath: String): Pair<UploadEntity, OCUpload> {
|
||||
val (needsCharging, needsWifi, uploadAction) = getUploadSettings(syncedFolder)
|
||||
Log_OC.d(TAG, "creating oc upload for ${user.accountName}")
|
||||
|
||||
// Get existing upload or create new one
|
||||
val uploadEntity = uploadsStorageManager.uploadDao.getUploadByAccountAndPaths(
|
||||
localPath = localPath,
|
||||
remotePath = remotePath,
|
||||
accountName = user.accountName
|
||||
)
|
||||
|
||||
val upload = (
|
||||
uploadEntity?.toOCUpload(null) ?: OCUpload(
|
||||
localPath,
|
||||
remotePath,
|
||||
user.accountName
|
||||
)
|
||||
).apply {
|
||||
uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS
|
||||
nameCollisionPolicy = syncedFolder.nameCollisionPolicy
|
||||
isUseWifiOnly = needsWifi
|
||||
isWhileChargingOnly = needsCharging
|
||||
localAction = uploadAction
|
||||
|
||||
// Only set these for new uploads
|
||||
if (uploadEntity == null) {
|
||||
createdBy = UploadFileOperation.CREATED_AS_INSTANT_PICTURE
|
||||
isCreateRemoteFolder = true
|
||||
}
|
||||
}
|
||||
|
||||
return upload.toUploadEntity() to upload
|
||||
}
|
||||
|
||||
private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation = UploadFileOperation(
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
powerManagementService,
|
||||
user,
|
||||
null,
|
||||
upload,
|
||||
upload.nameCollisionPolicy,
|
||||
upload.localAction,
|
||||
context,
|
||||
upload.isUseWifiOnly,
|
||||
upload.isWhileChargingOnly,
|
||||
true,
|
||||
FileDataStorageManager(user, context.contentResolver)
|
||||
)
|
||||
|
||||
private fun getRemotePath(
|
||||
file: File,
|
||||
syncedFolder: SyncedFolder,
|
||||
sFormatter: SimpleDateFormat,
|
||||
lightVersion: Boolean,
|
||||
resources: Resources,
|
||||
currentLocale: Locale
|
||||
): String {
|
||||
val lastModificationTime = calculateLastModificationTime(file, syncedFolder, sFormatter)
|
||||
|
||||
val (remoteFolder, useSubfolders, subFolderRule) = if (lightVersion) {
|
||||
Triple(
|
||||
resources.getString(R.string.syncedFolder_remote_folder),
|
||||
resources.getBoolean(R.bool.syncedFolder_light_use_subfolders),
|
||||
SubFolderRule.YEAR_MONTH
|
||||
)
|
||||
} else {
|
||||
Triple(
|
||||
syncedFolder.remotePath,
|
||||
syncedFolder.isSubfolderByDate,
|
||||
syncedFolder.subfolderRule
|
||||
)
|
||||
}
|
||||
|
||||
return FileStorageUtils.getInstantUploadFilePath(
|
||||
file,
|
||||
currentLocale,
|
||||
remoteFolder,
|
||||
syncedFolder.localPath,
|
||||
lastModificationTime,
|
||||
useSubfolders,
|
||||
subFolderRule
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasExif(file: File): Boolean {
|
||||
val mimeType = FileStorageUtils.getMimeTypeFromName(file.absolutePath)
|
||||
return MimeType.JPEG.equals(mimeType, ignoreCase = true) || MimeType.TIFF.equals(mimeType, ignoreCase = true)
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
private fun calculateLastModificationTime(
|
||||
file: File,
|
||||
syncedFolder: SyncedFolder,
|
||||
formatter: SimpleDateFormat
|
||||
): Long {
|
||||
var lastModificationTime = file.lastModified()
|
||||
if (MediaFolderType.IMAGE == syncedFolder.type && hasExif(file)) {
|
||||
Log_OC.d(TAG, "calculateLastModificationTime exif found")
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val exifInterface = ExifInterface(file.absolutePath)
|
||||
val exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME)
|
||||
if (!exifDate.isNullOrBlank()) {
|
||||
val pos = ParsePosition(0)
|
||||
val dateTime = formatter.parse(exifDate, pos)
|
||||
if (dateTime != null) {
|
||||
lastModificationTime = dateTime.time
|
||||
Log_OC.w(TAG, "calculateLastModificationTime calculatedTime is: $lastModificationTime")
|
||||
} else {
|
||||
Log_OC.w(TAG, "calculateLastModificationTime dateTime is empty")
|
||||
}
|
||||
} else {
|
||||
Log_OC.w(TAG, "calculateLastModificationTime exifDate is empty")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.d(TAG, "Failed to get the proper time " + e.localizedMessage)
|
||||
}
|
||||
}
|
||||
return lastModificationTime
|
||||
}
|
||||
|
||||
private fun getUploadAction(action: String): Int = when (action) {
|
||||
"LOCAL_BEHAVIOUR_FORGET" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_FORGET
|
||||
"LOCAL_BEHAVIOUR_MOVE" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_MOVE
|
||||
"LOCAL_BEHAVIOUR_DELETE" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_DELETE
|
||||
else -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_FORGET
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.autoUpload
|
||||
|
||||
import com.nextcloud.client.database.dao.FileSystemDao
|
||||
import com.owncloud.android.datamodel.SyncedFolder
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.utils.SyncedFolderUtils
|
||||
import java.io.File
|
||||
|
||||
class FileSystemRepository(private val dao: FileSystemDao) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FilesystemRepository"
|
||||
const val BATCH_SIZE = 50
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
suspend fun getFilePathsWithIds(syncedFolder: SyncedFolder, lastId: Int): List<Pair<String, Int>> {
|
||||
val syncedFolderId = syncedFolder.id.toString()
|
||||
Log_OC.d(TAG, "Fetching candidate files for syncedFolderId = $syncedFolderId")
|
||||
|
||||
val entities = dao.getAutoUploadFilesEntities(syncedFolderId, BATCH_SIZE, lastId)
|
||||
val filtered = mutableListOf<Pair<String, Int>>()
|
||||
|
||||
entities.forEach {
|
||||
it.localPath?.let { path ->
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
Log_OC.w(TAG, "Ignoring file for upload (doesn't exist): $path")
|
||||
} else if (!SyncedFolderUtils.isQualifiedFolder(file.parent)) {
|
||||
Log_OC.w(TAG, "Ignoring file for upload (unqualified folder): $path")
|
||||
} else if (!SyncedFolderUtils.isFileNameQualifiedForAutoUpload(file.name)) {
|
||||
Log_OC.w(TAG, "Ignoring file for upload (unqualified file): $path")
|
||||
} else {
|
||||
Log_OC.d(TAG, "Adding path to upload: $path")
|
||||
|
||||
if (it.id != null) {
|
||||
filtered.add(path to it.id)
|
||||
} else {
|
||||
Log_OC.w(TAG, "cant adding path to upload, id is null")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
suspend fun markFileAsUploaded(localPath: String, syncedFolder: SyncedFolder) {
|
||||
val syncedFolderIdStr = syncedFolder.id.toString()
|
||||
|
||||
try {
|
||||
dao.markFileAsUploaded(localPath, syncedFolderIdStr)
|
||||
Log_OC.d(TAG, "Marked file as uploaded: $localPath for syncedFolderId=$syncedFolderIdStr")
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Error marking file as uploaded: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,10 +9,12 @@ package com.nextcloud.client.jobs.download
|
|||
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
import com.owncloud.android.operations.DownloadType
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
|
|
@ -29,6 +31,7 @@ class FileDownloadHelper {
|
|||
|
||||
companion object {
|
||||
private var instance: FileDownloadHelper? = null
|
||||
private const val TAG = "FileDownloadHelper"
|
||||
|
||||
fun instance(): FileDownloadHelper = instance ?: synchronized(this) {
|
||||
instance ?: FileDownloadHelper().also { instance = it }
|
||||
|
|
@ -44,17 +47,11 @@ class FileDownloadHelper {
|
|||
return false
|
||||
}
|
||||
|
||||
val fileStorageManager = FileDataStorageManager(user, MainApp.getAppContext().contentResolver)
|
||||
val topParentId = fileStorageManager.getTopParentId(file)
|
||||
|
||||
val isJobScheduled = backgroundJobManager.isStartFileDownloadJobScheduled(user, file.fileId)
|
||||
return isJobScheduled ||
|
||||
if (file.isFolder) {
|
||||
FileDownloadWorker.isDownloadingFolder(file.fileId) ||
|
||||
backgroundJobManager.isStartFileDownloadJobScheduled(user, topParentId)
|
||||
} else {
|
||||
FileDownloadWorker.isDownloading(user.accountName, file.fileId)
|
||||
}
|
||||
return if (file.isFolder) {
|
||||
FolderDownloadWorker.isDownloading(file.fileId)
|
||||
} else {
|
||||
FileDownloadWorker.isDownloading(user.accountName, file.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelPendingOrCurrentDownloads(user: User?, files: List<OCFile>?) {
|
||||
|
|
@ -141,4 +138,14 @@ class FileDownloadHelper {
|
|||
conflictUploadId
|
||||
)
|
||||
}
|
||||
|
||||
fun downloadFolder(folder: OCFile?, accountName: String) {
|
||||
if (folder == null) {
|
||||
Log_OC.e(TAG, "folder cannot be null, cant sync")
|
||||
return
|
||||
}
|
||||
backgroundJobManager.downloadFolder(folder, accountName)
|
||||
}
|
||||
|
||||
fun cancelFolderDownload() = backgroundJobManager.cancelFolderDownload()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import com.nextcloud.client.account.UserAccountManager
|
|||
import com.nextcloud.model.WorkerState
|
||||
import com.nextcloud.model.WorkerStateLiveData
|
||||
import com.nextcloud.utils.ForegroundServiceHelper
|
||||
import com.nextcloud.utils.extensions.getParentIdsOfSubfiles
|
||||
import com.nextcloud.utils.extensions.getPercent
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
|
|
@ -45,7 +44,6 @@ import com.owncloud.android.utils.theme.ViewThemeUtils
|
|||
import java.util.AbstractList
|
||||
import java.util.Optional
|
||||
import java.util.Vector
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.random.Random
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
|
|
@ -63,7 +61,6 @@ class FileDownloadWorker(
|
|||
private val TAG = FileDownloadWorker::class.java.simpleName
|
||||
|
||||
private val pendingDownloads = IndexedForest<DownloadFileOperation>()
|
||||
private val pendingFolderDownloads: MutableSet<Long> = ConcurrentHashMap.newKeySet<Long>()
|
||||
|
||||
fun cancelOperation(accountName: String, fileId: Long) {
|
||||
pendingDownloads.all.forEach {
|
||||
|
|
@ -75,8 +72,6 @@ class FileDownloadWorker(
|
|||
it.value?.payload?.isMatching(accountName, fileId) == true
|
||||
}
|
||||
|
||||
fun isDownloadingFolder(id: Long): Boolean = pendingFolderDownloads.contains(id)
|
||||
|
||||
const val FILE_REMOTE_PATH = "FILE_REMOTE_PATH"
|
||||
const val ACCOUNT_NAME = "ACCOUNT_NAME"
|
||||
const val BEHAVIOUR = "BEHAVIOUR"
|
||||
|
|
@ -170,10 +165,6 @@ class FileDownloadWorker(
|
|||
|
||||
private fun getRequestDownloads(ocFile: OCFile): AbstractList<String> {
|
||||
val files = getFiles(ocFile)
|
||||
val filesPaths = files.map { it.remotePath }
|
||||
val parentIdsOfSubFiles = fileDataStorageManager?.getParentIdsOfSubfiles(filesPaths) ?: listOf()
|
||||
pendingFolderDownloads.addAll(parentIdsOfSubFiles)
|
||||
|
||||
val downloadType = getDownloadType()
|
||||
|
||||
conflictUploadId = inputData.keyValueMap[CONFLICT_UPLOAD_ID] as Long?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.folderDownload
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.jobs.download.FileDownloadHelper
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
import com.owncloud.android.operations.DownloadType
|
||||
import com.owncloud.android.ui.helpers.FileOperationsHelper
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Suppress("LongMethod")
|
||||
class FolderDownloadWorker(
|
||||
private val accountManager: UserAccountManager,
|
||||
private val context: Context,
|
||||
private val viewThemeUtils: ViewThemeUtils,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "📂" + "FolderDownloadWorker"
|
||||
const val FOLDER_ID = "FOLDER_ID"
|
||||
const val ACCOUNT_NAME = "ACCOUNT_NAME"
|
||||
|
||||
private val pendingDownloads: MutableSet<Long> = ConcurrentHashMap.newKeySet<Long>()
|
||||
|
||||
fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id)
|
||||
}
|
||||
|
||||
private var notificationManager: FolderDownloadWorkerNotificationManager? = null
|
||||
private lateinit var storageManager: FileDataStorageManager
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "ReturnCount", "DEPRECATION")
|
||||
override suspend fun doWork(): Result {
|
||||
val folderID = inputData.getLong(FOLDER_ID, -1)
|
||||
if (folderID == -1L) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val accountName = inputData.getString(ACCOUNT_NAME)
|
||||
if (accountName == null) {
|
||||
Log_OC.e(TAG, "failed accountName cannot be null")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val optionalUser = accountManager.getUser(accountName)
|
||||
if (optionalUser.isEmpty) {
|
||||
Log_OC.e(TAG, "failed user is not present")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val user = optionalUser.get()
|
||||
storageManager = FileDataStorageManager(user, context.contentResolver)
|
||||
val folder = storageManager.getFileById(folderID)
|
||||
if (folder == null) {
|
||||
Log_OC.e(TAG, "failed folder cannot be nul")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
notificationManager = FolderDownloadWorkerNotificationManager(context, viewThemeUtils)
|
||||
|
||||
Log_OC.d(TAG, "🕒 started for ${user.accountName} downloading ${folder.fileName}")
|
||||
|
||||
val foregroundInfo = notificationManager?.getForegroundInfo(folder) ?: return Result.failure()
|
||||
setForeground(foregroundInfo)
|
||||
|
||||
pendingDownloads.add(folder.fileId)
|
||||
|
||||
val downloadHelper = FileDownloadHelper.instance()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val files = getFiles(folder, storageManager)
|
||||
val account = user.toOwnCloudAccount()
|
||||
val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(account, context)
|
||||
|
||||
var result = true
|
||||
files.forEachIndexed { index, file ->
|
||||
if (!checkDiskSize(file)) {
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager?.showProgressNotification(
|
||||
folder.fileName,
|
||||
file.fileName,
|
||||
index,
|
||||
files.size
|
||||
)
|
||||
}
|
||||
|
||||
val operation = DownloadFileOperation(user, file, context)
|
||||
val operationResult = operation.execute(client)
|
||||
if (operationResult?.isSuccess == true && operation.downloadType === DownloadType.DOWNLOAD) {
|
||||
getOCFile(operation)?.let { ocFile ->
|
||||
downloadHelper.saveFile(ocFile, operation, storageManager)
|
||||
}
|
||||
}
|
||||
|
||||
if (!operationResult.isSuccess) {
|
||||
result = false
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager?.showCompletionMessage(folder.fileName, result)
|
||||
}
|
||||
|
||||
if (result) {
|
||||
Log_OC.d(TAG, "✅ completed")
|
||||
Result.success()
|
||||
} else {
|
||||
Log_OC.d(TAG, "❌ failed")
|
||||
Result.failure()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.d(TAG, "❌ failed reason: $e")
|
||||
Result.failure()
|
||||
} finally {
|
||||
pendingDownloads.remove(folder.fileId)
|
||||
notificationManager?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOCFile(operation: DownloadFileOperation): OCFile? {
|
||||
val file = operation.file?.fileId?.let { storageManager.getFileById(it) }
|
||||
?: storageManager.getFileByDecryptedRemotePath(operation.file?.remotePath)
|
||||
?: run {
|
||||
Log_OC.e(TAG, "could not save ${operation.file?.remotePath}")
|
||||
return null
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
private fun getFiles(folder: OCFile, storageManager: FileDataStorageManager): List<OCFile> =
|
||||
storageManager.getFolderContent(folder, false)
|
||||
.filter { !it.isFolder && !it.isDown }
|
||||
|
||||
private suspend fun checkDiskSize(file: OCFile): Boolean {
|
||||
val fileSizeInByte = file.fileLength
|
||||
val availableDiskSpace = FileOperationsHelper.getAvailableSpaceOnDevice()
|
||||
|
||||
return if (availableDiskSpace < fileSizeInByte) {
|
||||
notificationManager?.showNotAvailableDiskSpace()
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.folderDownload
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.work.ForegroundInfo
|
||||
import com.nextcloud.client.jobs.notification.WorkerNotificationManager
|
||||
import com.nextcloud.utils.ForegroundServiceHelper
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.ForegroundServiceType
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.random.Random
|
||||
|
||||
class FolderDownloadWorkerNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) :
|
||||
WorkerNotificationManager(
|
||||
id = NOTIFICATION_ID,
|
||||
context,
|
||||
viewThemeUtils,
|
||||
tickerId = R.string.folder_download_worker_ticker_id,
|
||||
channelId = NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 391
|
||||
private const val MAX_PROGRESS = 100
|
||||
private const val DELAY = 1000L
|
||||
}
|
||||
|
||||
private fun getNotification(title: String, description: String? = null, progress: Int? = null): Notification =
|
||||
notificationBuilder.apply {
|
||||
setSmallIcon(R.drawable.ic_sync)
|
||||
setContentTitle(title)
|
||||
clearActions()
|
||||
|
||||
description?.let {
|
||||
setContentText(description)
|
||||
}
|
||||
|
||||
progress?.let {
|
||||
setProgress(MAX_PROGRESS, progress, false)
|
||||
addAction(
|
||||
android.R.drawable.ic_menu_close_clear_cancel,
|
||||
context.getString(R.string.common_cancel),
|
||||
getCancelPendingIntent()
|
||||
)
|
||||
}
|
||||
|
||||
setAutoCancel(true)
|
||||
}.build()
|
||||
|
||||
private fun getCancelPendingIntent(): PendingIntent {
|
||||
val intent = Intent(context, FolderDownloadWorkerReceiver::class.java)
|
||||
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
Random.nextInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
fun showProgressNotification(folderName: String, filename: String, currentIndex: Int, totalFileSize: Int) {
|
||||
val currentFileIndex = (currentIndex + 1)
|
||||
val description = context.getString(R.string.folder_download_counter, currentFileIndex, totalFileSize, filename)
|
||||
val progress = (currentFileIndex * MAX_PROGRESS) / totalFileSize
|
||||
val notification = getNotification(title = folderName, description = description, progress = progress)
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
suspend fun showCompletionMessage(folderName: String, success: Boolean) {
|
||||
val title = if (success) {
|
||||
context.getString(R.string.folder_download_success_notification_title, folderName)
|
||||
} else {
|
||||
context.getString(R.string.folder_download_error_notification_title, folderName)
|
||||
}
|
||||
|
||||
val notification = getNotification(title = title)
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
|
||||
delay(DELAY)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
fun getForegroundInfo(folder: OCFile): ForegroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
|
||||
NOTIFICATION_ID,
|
||||
getNotification(folder.fileName, progress = 0),
|
||||
ForegroundServiceType.DataSync
|
||||
)
|
||||
|
||||
suspend fun showNotAvailableDiskSpace() {
|
||||
val title = context.getString(R.string.folder_download_insufficient_disk_space_notification_title)
|
||||
val notification = getNotification(title)
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
|
||||
delay(DELAY)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs.folderDownload
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.owncloud.android.MainApp
|
||||
import javax.inject.Inject
|
||||
|
||||
class FolderDownloadWorkerReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var backgroundJobManager: BackgroundJobManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
MainApp.getAppComponent().inject(this)
|
||||
backgroundJobManager.cancelFolderDownload()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,11 +7,14 @@
|
|||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.database.entity.toOCUpload
|
||||
import com.nextcloud.client.database.entity.toUploadEntity
|
||||
import com.nextcloud.client.device.BatteryStatus
|
||||
import com.nextcloud.client.device.PowerManagementService
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
|
|
@ -20,6 +23,7 @@ import com.nextcloud.client.network.Connectivity
|
|||
import com.nextcloud.client.network.ConnectivityService
|
||||
import com.nextcloud.utils.extensions.getUploadIds
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager
|
||||
|
|
@ -35,13 +39,12 @@ import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
|
|||
import com.owncloud.android.lib.resources.files.model.RemoteFile
|
||||
import com.owncloud.android.operations.RemoveFileOperation
|
||||
import com.owncloud.android.operations.UploadFileOperation
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.FileUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Semaphore
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -85,18 +88,42 @@ class FileUploadHelper {
|
|||
fun buildRemoteName(accountName: String, remotePath: String): String = accountName + remotePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries all failed uploads across all user accounts.
|
||||
*
|
||||
* This function retrieves all uploads with the status [UploadStatus.UPLOAD_FAILED], including both
|
||||
* manual uploads and auto uploads. It runs in a background thread (Dispatcher.IO) and ensures
|
||||
* that only one retry operation runs at a time by using a semaphore to prevent concurrent execution.
|
||||
*
|
||||
* Once the failed uploads are retrieved, it calls [retryUploads], which triggers the corresponding
|
||||
* upload workers for each failed upload.
|
||||
*
|
||||
* The function returns `true` if there were any failed uploads to retry and the retry process was
|
||||
* started, or `false` if no uploads were retried.
|
||||
*
|
||||
* @param uploadsStorageManager Provides access to upload data and persistence.
|
||||
* @param connectivityService Checks the current network connectivity state.
|
||||
* @param accountManager Handles user account authentication and selection.
|
||||
* @param powerManagementService Ensures uploads respect power constraints.
|
||||
* @return `true` if any failed uploads were found and retried; `false` otherwise.
|
||||
*/
|
||||
fun retryFailedUploads(
|
||||
uploadsStorageManager: UploadsStorageManager,
|
||||
connectivityService: ConnectivityService,
|
||||
accountManager: UserAccountManager,
|
||||
powerManagementService: PowerManagementService
|
||||
) {
|
||||
if (retryFailedUploadsSemaphore.tryAcquire()) {
|
||||
try {
|
||||
val failedUploads = uploadsStorageManager.failedUploads
|
||||
if (failedUploads == null || failedUploads.isEmpty()) {
|
||||
Log_OC.d(TAG, "Failed uploads are empty or null")
|
||||
return
|
||||
): Boolean {
|
||||
if (!retryFailedUploadsSemaphore.tryAcquire()) {
|
||||
Log_OC.d(TAG, "skipping retryFailedUploads, already running")
|
||||
return true
|
||||
}
|
||||
|
||||
var isUploadStarted = false
|
||||
|
||||
try {
|
||||
getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED) {
|
||||
if (it.isNotEmpty()) {
|
||||
isUploadStarted = true
|
||||
}
|
||||
|
||||
retryUploads(
|
||||
|
|
@ -104,14 +131,14 @@ class FileUploadHelper {
|
|||
connectivityService,
|
||||
accountManager,
|
||||
powerManagementService,
|
||||
failedUploads
|
||||
uploads = it
|
||||
)
|
||||
} finally {
|
||||
retryFailedUploadsSemaphore.release()
|
||||
}
|
||||
} else {
|
||||
Log_OC.d(TAG, "Skip retryFailedUploads since it is already running")
|
||||
} finally {
|
||||
retryFailedUploadsSemaphore.release()
|
||||
}
|
||||
|
||||
return isUploadStarted
|
||||
}
|
||||
|
||||
fun retryCancelledUploads(
|
||||
|
|
@ -120,18 +147,18 @@ class FileUploadHelper {
|
|||
accountManager: UserAccountManager,
|
||||
powerManagementService: PowerManagementService
|
||||
): Boolean {
|
||||
val cancelledUploads = uploadsStorageManager.cancelledUploadsForCurrentAccount
|
||||
if (cancelledUploads == null || cancelledUploads.isEmpty()) {
|
||||
return false
|
||||
var result = false
|
||||
getUploadsByStatus(accountManager.user.accountName, UploadStatus.UPLOAD_CANCELLED) {
|
||||
result = retryUploads(
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
accountManager,
|
||||
powerManagementService,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
return retryUploads(
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
accountManager,
|
||||
powerManagementService,
|
||||
cancelledUploads
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
|
|
@ -140,35 +167,32 @@ class FileUploadHelper {
|
|||
connectivityService: ConnectivityService,
|
||||
accountManager: UserAccountManager,
|
||||
powerManagementService: PowerManagementService,
|
||||
failedUploads: Array<OCUpload>
|
||||
uploads: Array<OCUpload>
|
||||
): Boolean {
|
||||
var showNotExistMessage = false
|
||||
val isOnline = checkConnectivity(connectivityService)
|
||||
val connectivity = connectivityService.connectivity
|
||||
val batteryStatus = powerManagementService.battery
|
||||
val accountNames = accountManager.accounts.filter { account ->
|
||||
accountManager.getUser(account.name).isPresent
|
||||
}.map { account ->
|
||||
account.name
|
||||
}.toHashSet()
|
||||
|
||||
for (failedUpload in failedUploads) {
|
||||
if (!accountNames.contains(failedUpload.accountName)) {
|
||||
uploadsStorageManager.removeUpload(failedUpload)
|
||||
continue
|
||||
}
|
||||
val uploadsToRetry = mutableListOf<Long>()
|
||||
|
||||
val uploadResult =
|
||||
checkUploadConditions(failedUpload, connectivity, batteryStatus, powerManagementService, isOnline)
|
||||
for (upload in uploads) {
|
||||
val uploadResult = checkUploadConditions(
|
||||
upload,
|
||||
connectivity,
|
||||
batteryStatus,
|
||||
powerManagementService,
|
||||
isOnline
|
||||
)
|
||||
|
||||
if (uploadResult != UploadResult.UPLOADED) {
|
||||
if (failedUpload.lastResult != uploadResult) {
|
||||
if (upload.lastResult != uploadResult) {
|
||||
// Setting Upload status else cancelled uploads will behave wrong, when retrying
|
||||
// Needs to happen first since lastResult wil be overwritten by setter
|
||||
failedUpload.uploadStatus = UploadStatus.UPLOAD_FAILED
|
||||
upload.uploadStatus = UploadStatus.UPLOAD_FAILED
|
||||
|
||||
failedUpload.lastResult = uploadResult
|
||||
uploadsStorageManager.updateUpload(failedUpload)
|
||||
upload.lastResult = uploadResult
|
||||
uploadsStorageManager.updateUpload(upload)
|
||||
}
|
||||
if (uploadResult == UploadResult.FILE_NOT_FOUND) {
|
||||
showNotExistMessage = true
|
||||
|
|
@ -176,15 +200,18 @@ class FileUploadHelper {
|
|||
continue
|
||||
}
|
||||
|
||||
failedUpload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
|
||||
uploadsStorageManager.updateUpload(failedUpload)
|
||||
// Only uploads that passed checks get marked in progress and are collected for scheduling
|
||||
upload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
|
||||
uploadsStorageManager.updateUpload(upload)
|
||||
uploadsToRetry.add(upload.uploadId)
|
||||
}
|
||||
|
||||
accountNames.forEach { accountName ->
|
||||
val user = accountManager.getUser(accountName)
|
||||
if (user.isPresent) {
|
||||
backgroundJobManager.startFilesUploadJob(user.get(), failedUploads.getUploadIds(), false)
|
||||
}
|
||||
if (uploadsToRetry.isNotEmpty()) {
|
||||
backgroundJobManager.startFilesUploadJob(
|
||||
accountManager.user,
|
||||
uploadsToRetry.toLongArray(),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
return showNotExistMessage
|
||||
|
|
@ -205,7 +232,7 @@ class FileUploadHelper {
|
|||
showSameFileAlreadyExistsNotification: Boolean = true
|
||||
) {
|
||||
val uploads = localPaths.mapIndexed { index, localPath ->
|
||||
OCUpload(localPath, remotePaths[index], user.accountName).apply {
|
||||
val result = OCUpload(localPath, remotePaths[index], user.accountName).apply {
|
||||
this.nameCollisionPolicy = nameCollisionPolicy
|
||||
isUseWifiOnly = requiresWifi
|
||||
isWhileChargingOnly = requiresCharging
|
||||
|
|
@ -214,47 +241,54 @@ class FileUploadHelper {
|
|||
isCreateRemoteFolder = createRemoteFolder
|
||||
localAction = localBehavior
|
||||
}
|
||||
|
||||
val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity())
|
||||
result.uploadId = id
|
||||
result
|
||||
}
|
||||
uploadsStorageManager.storeUploads(uploads)
|
||||
backgroundJobManager.startFilesUploadJob(user, uploads.getUploadIds(), showSameFileAlreadyExistsNotification)
|
||||
}
|
||||
|
||||
fun removeFileUpload(remotePath: String, accountName: String) {
|
||||
try {
|
||||
val user = accountManager.getUser(accountName).get()
|
||||
|
||||
// need to update now table in mUploadsStorageManager,
|
||||
// since the operation will not get to be run by FileUploader#uploadFile
|
||||
uploadsStorageManager.removeUpload(accountName, remotePath)
|
||||
val uploadIds = uploadsStorageManager.getCurrentUploadIds(user.accountName)
|
||||
cancelAndRestartUploadJob(user, uploadIds)
|
||||
} catch (e: NoSuchElementException) {
|
||||
Log_OC.e(TAG, "Error cancelling current upload because user does not exist!: " + e.message)
|
||||
}
|
||||
uploadsStorageManager.uploadDao.deleteByAccountAndRemotePath(accountName, remotePath)
|
||||
}
|
||||
|
||||
fun cancelFileUpload(remotePath: String, accountName: String) {
|
||||
fun updateUploadStatus(remotePath: String, accountName: String, status: UploadStatus) {
|
||||
ioScope.launch {
|
||||
val upload = uploadsStorageManager.getUploadByRemotePath(remotePath)
|
||||
if (upload != null) {
|
||||
cancelFileUploads(listOf(upload), accountName)
|
||||
} else {
|
||||
Log_OC.e(TAG, "Error cancelling current upload because upload does not exist!")
|
||||
}
|
||||
uploadsStorageManager.uploadDao.updateStatus(remotePath, accountName, status.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFileUploads(uploads: List<OCUpload>, accountName: String) {
|
||||
for (upload in uploads) {
|
||||
upload.uploadStatus = UploadStatus.UPLOAD_CANCELLED
|
||||
uploadsStorageManager.updateUpload(upload)
|
||||
}
|
||||
|
||||
try {
|
||||
val user = accountManager.getUser(accountName).get()
|
||||
cancelAndRestartUploadJob(user, uploads.getUploadIds())
|
||||
} catch (e: NoSuchElementException) {
|
||||
Log_OC.e(TAG, "Error restarting upload job because user does not exist!: " + e.message)
|
||||
/**
|
||||
* Retrieves uploads filtered by their status, optionally for a specific account.
|
||||
*
|
||||
* This function queries the uploads database asynchronously to obtain a list of uploads
|
||||
* that match the specified [status]. If an [accountName] is provided, only uploads
|
||||
* belonging to that account are retrieved. If [accountName] is `null`, uploads with the
|
||||
* given [status] from **all user accounts** are returned.
|
||||
*
|
||||
* Once the uploads are fetched, the [onCompleted] callback is invoked with the resulting array.
|
||||
*
|
||||
* @param accountName The name of the account to filter uploads by.
|
||||
* If `null`, uploads matching the given [status] from all accounts are returned.
|
||||
* @param status The [UploadStatus] to filter uploads by (e.g., `UPLOAD_FAILED`).
|
||||
* @param nameCollisionPolicy The [NameCollisionPolicy] to filter uploads by (e.g., `SKIP`).
|
||||
* @param onCompleted A callback invoked with the resulting array of [OCUpload] objects.
|
||||
*/
|
||||
fun getUploadsByStatus(
|
||||
accountName: String?,
|
||||
status: UploadStatus,
|
||||
nameCollisionPolicy: NameCollisionPolicy? = null,
|
||||
onCompleted: (Array<OCUpload>) -> Unit
|
||||
) {
|
||||
ioScope.launch {
|
||||
val dao = uploadsStorageManager.uploadDao
|
||||
val result = if (accountName != null) {
|
||||
dao.getUploadsByAccountNameAndStatus(accountName, status.value, nameCollisionPolicy?.serialize())
|
||||
} else {
|
||||
dao.getUploadsByStatus(status.value, nameCollisionPolicy?.serialize())
|
||||
}.map { it.toOCUpload(null) }.toTypedArray()
|
||||
onCompleted(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -266,26 +300,16 @@ class FileUploadHelper {
|
|||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
fun isUploading(user: User?, file: OCFile?): Boolean {
|
||||
if (user == null || file == null || !backgroundJobManager.isStartFileUploadJobScheduled(user)) {
|
||||
fun isUploading(remotePath: String?, accountName: String?): Boolean {
|
||||
accountName ?: return false
|
||||
if (!backgroundJobManager.isStartFileUploadJobScheduled(accountName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val uploadCompletableFuture = CompletableFuture.supplyAsync {
|
||||
uploadsStorageManager.getUploadByRemotePath(file.remotePath)
|
||||
}
|
||||
return try {
|
||||
val upload = uploadCompletableFuture.get()
|
||||
if (upload != null) {
|
||||
upload.uploadStatus == UploadStatus.UPLOAD_IN_PROGRESS
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
false
|
||||
} catch (e: InterruptedException) {
|
||||
false
|
||||
}
|
||||
remotePath ?: return false
|
||||
val upload = uploadsStorageManager.uploadDao.getByRemotePath(remotePath)
|
||||
return upload?.status == UploadStatus.UPLOAD_IN_PROGRESS.value ||
|
||||
FileUploadWorker.isUploading(remotePath, accountName)
|
||||
}
|
||||
|
||||
private fun checkConnectivity(connectivityService: ConnectivityService): Boolean {
|
||||
|
|
@ -364,7 +388,7 @@ class FileUploadHelper {
|
|||
|
||||
val uploads = existingFiles.map { file ->
|
||||
file?.let {
|
||||
OCUpload(file, user).apply {
|
||||
val result = OCUpload(file, user).apply {
|
||||
fileSize = file.fileLength
|
||||
this.nameCollisionPolicy = nameCollisionPolicy
|
||||
isCreateRemoteFolder = true
|
||||
|
|
@ -373,9 +397,12 @@ class FileUploadHelper {
|
|||
isWhileChargingOnly = false
|
||||
uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
|
||||
}
|
||||
|
||||
val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity())
|
||||
result.uploadId = id
|
||||
result
|
||||
}
|
||||
}
|
||||
uploadsStorageManager.storeUploads(uploads)
|
||||
val uploadIds: LongArray = uploads.filterNotNull().map { it.uploadId }.toLongArray()
|
||||
backgroundJobManager.startFilesUploadJob(user, uploadIds, true)
|
||||
}
|
||||
|
|
@ -459,6 +486,14 @@ class FileUploadHelper {
|
|||
return false
|
||||
}
|
||||
|
||||
fun showFileUploadLimitMessage(activity: Activity) {
|
||||
val message = activity.resources.getQuantityString(
|
||||
R.plurals.file_upload_limit_message,
|
||||
MAX_FILE_COUNT
|
||||
)
|
||||
DisplayUtils.showSnackMessage(activity, message)
|
||||
}
|
||||
|
||||
class UploadNotificationActionReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val accountName = intent.getStringExtra(FileUploadWorker.EXTRA_ACCOUNT_NAME)
|
||||
|
|
@ -474,7 +509,9 @@ class FileUploadHelper {
|
|||
return
|
||||
}
|
||||
|
||||
instance().cancelFileUpload(remotePath, accountName)
|
||||
FileUploadWorker.cancelCurrentUpload(remotePath, accountName, onCompleted = {
|
||||
instance().updateUploadStatus(remotePath, accountName, UploadStatus.UPLOAD_CANCELLED)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@
|
|||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
|
|
@ -21,8 +23,12 @@ import com.nextcloud.client.network.ConnectivityService
|
|||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import com.nextcloud.model.WorkerState
|
||||
import com.nextcloud.model.WorkerStateLiveData
|
||||
import com.nextcloud.utils.ForegroundServiceHelper
|
||||
import com.nextcloud.utils.extensions.getPercent
|
||||
import com.nextcloud.utils.extensions.updateStatus
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.ForegroundServiceType
|
||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager
|
||||
import com.owncloud.android.db.OCUpload
|
||||
|
|
@ -34,8 +40,12 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
|||
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.UploadFileOperation
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.ErrorMessageAdapter
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import kotlin.random.Random
|
||||
|
||||
|
|
@ -51,7 +61,7 @@ class FileUploadWorker(
|
|||
val preferences: AppPreferences,
|
||||
val context: Context,
|
||||
params: WorkerParameters
|
||||
) : Worker(context, params),
|
||||
) : CoroutineWorker(context, params),
|
||||
OnDatatransferProgressListener {
|
||||
|
||||
companion object {
|
||||
|
|
@ -91,19 +101,44 @@ class FileUploadWorker(
|
|||
fun getUploadStartMessage(): String = FileUploadWorker::class.java.name + UPLOAD_START_MESSAGE
|
||||
|
||||
fun getUploadFinishMessage(): String = FileUploadWorker::class.java.name + UPLOAD_FINISH_MESSAGE
|
||||
|
||||
fun cancelCurrentUpload(remotePath: String, accountName: String, onCompleted: () -> Unit) {
|
||||
currentUploadFileOperation?.let {
|
||||
if (it.remotePath == remotePath && it.user.accountName == accountName) {
|
||||
it.cancel(ResultCode.USER_CANCELLED)
|
||||
onCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isUploading(remotePath: String?, accountName: String?): Boolean {
|
||||
currentUploadFileOperation?.let {
|
||||
return it.remotePath == remotePath && it.user.accountName == accountName
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var lastPercent = 0
|
||||
private val notificationManager = UploadNotificationManager(context, viewThemeUtils, Random.nextInt())
|
||||
private val notificationId = Random.nextInt()
|
||||
private val notificationManager = UploadNotificationManager(context, viewThemeUtils, notificationId)
|
||||
private val intents = FileUploaderIntents(context)
|
||||
private val fileUploaderDelegate = FileUploaderDelegate()
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun doWork(): Result = try {
|
||||
override suspend fun doWork(): Result = try {
|
||||
Log_OC.d(TAG, "FileUploadWorker started")
|
||||
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
|
||||
val workerName = BackgroundJobManagerImpl.formatClassTag(this::class)
|
||||
backgroundJobManager.logStartOfWorker(workerName)
|
||||
|
||||
val notificationTitle = notificationManager.currentOperationTitle
|
||||
?: context.getString(R.string.foreground_service_upload)
|
||||
val notification = createNotification(notificationTitle)
|
||||
updateForegroundInfo(notification)
|
||||
|
||||
val result = uploadFiles()
|
||||
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
|
||||
backgroundJobManager.logEndOfWorker(workerName, result)
|
||||
notificationManager.dismissNotification()
|
||||
if (result == Result.success()) {
|
||||
setIdleWorkerState()
|
||||
|
|
@ -111,17 +146,37 @@ class FileUploadWorker(
|
|||
result
|
||||
} catch (t: Throwable) {
|
||||
Log_OC.e(TAG, "Error caught at FileUploadWorker $t")
|
||||
cleanup()
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
private suspend fun updateForegroundInfo(notification: Notification) {
|
||||
val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
|
||||
notificationId,
|
||||
notification,
|
||||
ForegroundServiceType.DataSync
|
||||
)
|
||||
setForeground(foregroundInfo)
|
||||
}
|
||||
|
||||
private fun createNotification(title: String): Notification =
|
||||
NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD)
|
||||
.setContentTitle(title)
|
||||
.setSmallIcon(R.drawable.uploads)
|
||||
.setOngoing(true)
|
||||
.setSound(null)
|
||||
.setVibrate(null)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setSilent(true)
|
||||
.build()
|
||||
|
||||
private fun cleanup() {
|
||||
Log_OC.e(TAG, "FileUploadWorker stopped")
|
||||
|
||||
setIdleWorkerState()
|
||||
currentUploadFileOperation?.cancel(null)
|
||||
notificationManager.dismissNotification()
|
||||
|
||||
super.onStopped()
|
||||
}
|
||||
|
||||
private fun setWorkerState(user: User?) {
|
||||
|
|
@ -133,36 +188,36 @@ class FileUploadWorker(
|
|||
}
|
||||
|
||||
@Suppress("ReturnCount", "LongMethod")
|
||||
private fun uploadFiles(): Result {
|
||||
private suspend fun uploadFiles(): Result = withContext(Dispatchers.IO) {
|
||||
val accountName = inputData.getString(ACCOUNT)
|
||||
if (accountName == null) {
|
||||
Log_OC.e(TAG, "accountName is null")
|
||||
return Result.failure()
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
|
||||
val uploadIds = inputData.getLongArray(UPLOAD_IDS)
|
||||
if (uploadIds == null) {
|
||||
Log_OC.e(TAG, "uploadIds is null")
|
||||
return Result.failure()
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
|
||||
val currentBatchIndex = inputData.getInt(CURRENT_BATCH_INDEX, -1)
|
||||
if (currentBatchIndex == -1) {
|
||||
Log_OC.e(TAG, "currentBatchIndex is -1, cancelling")
|
||||
return Result.failure()
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
|
||||
val totalUploadSize = inputData.getInt(TOTAL_UPLOAD_SIZE, -1)
|
||||
if (totalUploadSize == -1) {
|
||||
Log_OC.e(TAG, "totalUploadSize is -1, cancelling")
|
||||
return Result.failure()
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
|
||||
// since worker's policy is append or replace and account name comes from there no need check in the loop
|
||||
val optionalUser = userAccountManager.getUser(accountName)
|
||||
if (!optionalUser.isPresent) {
|
||||
Log_OC.e(TAG, "User not found for account: $accountName")
|
||||
return Result.failure()
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
|
||||
val user = optionalUser.get()
|
||||
|
|
@ -172,21 +227,19 @@ class FileUploadWorker(
|
|||
val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context)
|
||||
|
||||
for ((index, upload) in uploads.withIndex()) {
|
||||
ensureActive()
|
||||
|
||||
if (preferences.isGlobalUploadPaused) {
|
||||
Log_OC.d(TAG, "Upload is paused, skip uploading files!")
|
||||
notificationManager.notifyPaused(
|
||||
intents.notificationStartIntent(null)
|
||||
)
|
||||
return Result.success()
|
||||
return@withContext Result.success()
|
||||
}
|
||||
|
||||
if (canExitEarly()) {
|
||||
notificationManager.showConnectionErrorNotification()
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
if (isStopped) {
|
||||
continue
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
|
||||
setWorkerState(user)
|
||||
|
|
@ -203,12 +256,16 @@ class FileUploadWorker(
|
|||
totalUploadSize = totalUploadSize
|
||||
)
|
||||
|
||||
val result = upload(operation, user, client)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
upload(operation, user, client)
|
||||
}
|
||||
val entity = uploadsStorageManager.uploadDao.getUploadById(upload.uploadId, accountName)
|
||||
uploadsStorageManager.updateStatus(entity, result.isSuccess)
|
||||
currentUploadFileOperation = null
|
||||
sendUploadFinishEvent(totalUploadSize, currentUploadIndex, operation, result)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
return@withContext Result.success()
|
||||
}
|
||||
|
||||
private fun sendUploadFinishEvent(
|
||||
|
|
@ -346,6 +403,10 @@ class FileUploadWorker(
|
|||
return
|
||||
}
|
||||
|
||||
if (uploadResult.code == ResultCode.USER_CANCELLED) {
|
||||
return
|
||||
}
|
||||
|
||||
notificationManager.run {
|
||||
val errorMessage = ErrorMessageAdapter.getErrorCauseMessage(
|
||||
uploadResult,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue