added DEV version to repo
This commit is contained in:
parent
1ef725ef20
commit
23e673bfdf
2135 changed files with 97033 additions and 21206 deletions
|
|
@ -8,7 +8,7 @@
|
|||
* Copyright (C) 2017 Nextcloud GmbH.
|
||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -23,6 +23,8 @@ import com.nextcloud.client.documentscan.GeneratePDFUseCase
|
|||
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
|
||||
import com.nextcloud.client.integrations.deck.DeckApi
|
||||
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.upload.FileUploadWorker
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
|
|
@ -95,39 +97,42 @@ class BackgroundJobFactory @Inject constructor(
|
|||
GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
|
||||
HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
|
||||
TestJob::class -> createTestJob(context, workerParameters)
|
||||
OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters)
|
||||
InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters)
|
||||
MetadataWorker::class -> createMetadataWorker(context, workerParameters)
|
||||
else -> null // caller falls back to default factory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFilesExportWork(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
): ListenableWorker {
|
||||
return FilesExportWork(
|
||||
context,
|
||||
private fun createOfflineOperationsWorker(context: Context, params: WorkerParameters): ListenableWorker =
|
||||
OfflineOperationsWorker(
|
||||
accountManager.user,
|
||||
contentResolver,
|
||||
context,
|
||||
connectivityService,
|
||||
viewThemeUtils.get(),
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
private fun createContentObserverJob(
|
||||
context: Context,
|
||||
workerParameters: WorkerParameters
|
||||
): ListenableWorker {
|
||||
return ContentObserverWork(
|
||||
private fun createFilesExportWork(context: Context, params: WorkerParameters): ListenableWorker = FilesExportWork(
|
||||
context,
|
||||
accountManager.user,
|
||||
contentResolver,
|
||||
viewThemeUtils.get(),
|
||||
params
|
||||
)
|
||||
|
||||
private fun createContentObserverJob(context: Context, workerParameters: WorkerParameters): ListenableWorker =
|
||||
ContentObserverWork(
|
||||
context,
|
||||
workerParameters,
|
||||
SyncedFolderProvider(contentResolver, preferences, clock),
|
||||
powerManagementService,
|
||||
backgroundJobManager.get()
|
||||
)
|
||||
}
|
||||
|
||||
private fun createContactsBackupWork(context: Context, params: WorkerParameters): ContactsBackupWork {
|
||||
return ContactsBackupWork(
|
||||
private fun createContactsBackupWork(context: Context, params: WorkerParameters): ContactsBackupWork =
|
||||
ContactsBackupWork(
|
||||
context,
|
||||
params,
|
||||
resources,
|
||||
|
|
@ -135,63 +140,55 @@ class BackgroundJobFactory @Inject constructor(
|
|||
contentResolver,
|
||||
accountManager
|
||||
)
|
||||
}
|
||||
|
||||
private fun createContactsImportWork(context: Context, params: WorkerParameters): ContactsImportWork {
|
||||
return ContactsImportWork(
|
||||
private fun createContactsImportWork(context: Context, params: WorkerParameters): ContactsImportWork =
|
||||
ContactsImportWork(
|
||||
context,
|
||||
params,
|
||||
logger,
|
||||
contentResolver
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCalendarBackupWork(context: Context, params: WorkerParameters): CalendarBackupWork {
|
||||
return CalendarBackupWork(
|
||||
private fun createCalendarBackupWork(context: Context, params: WorkerParameters): CalendarBackupWork =
|
||||
CalendarBackupWork(
|
||||
context,
|
||||
params,
|
||||
contentResolver,
|
||||
accountManager,
|
||||
preferences
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCalendarImportWork(context: Context, params: WorkerParameters): CalendarImportWork {
|
||||
return CalendarImportWork(
|
||||
private fun createCalendarImportWork(context: Context, params: WorkerParameters): CalendarImportWork =
|
||||
CalendarImportWork(
|
||||
context,
|
||||
params,
|
||||
logger,
|
||||
contentResolver
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFilesSyncWork(context: Context, params: WorkerParameters): FilesSyncWork {
|
||||
return FilesSyncWork(
|
||||
context = context,
|
||||
params = params,
|
||||
contentResolver = contentResolver,
|
||||
userAccountManager = accountManager,
|
||||
uploadsStorageManager = uploadsStorageManager,
|
||||
connectivityService = connectivityService,
|
||||
powerManagementService = powerManagementService,
|
||||
syncedFolderProvider = syncedFolderProvider,
|
||||
backgroundJobManager = backgroundJobManager.get()
|
||||
)
|
||||
}
|
||||
private fun createFilesSyncWork(context: Context, params: WorkerParameters): FilesSyncWork = FilesSyncWork(
|
||||
context = context,
|
||||
params = params,
|
||||
contentResolver = contentResolver,
|
||||
userAccountManager = accountManager,
|
||||
uploadsStorageManager = uploadsStorageManager,
|
||||
connectivityService = connectivityService,
|
||||
powerManagementService = powerManagementService,
|
||||
syncedFolderProvider = syncedFolderProvider,
|
||||
backgroundJobManager = backgroundJobManager.get()
|
||||
)
|
||||
|
||||
private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork {
|
||||
return OfflineSyncWork(
|
||||
context = context,
|
||||
params = params,
|
||||
contentResolver = contentResolver,
|
||||
userAccountManager = accountManager,
|
||||
connectivityService = connectivityService,
|
||||
powerManagementService = powerManagementService
|
||||
)
|
||||
}
|
||||
private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork = OfflineSyncWork(
|
||||
context = context,
|
||||
params = params,
|
||||
contentResolver = contentResolver,
|
||||
userAccountManager = accountManager,
|
||||
connectivityService = connectivityService,
|
||||
powerManagementService = powerManagementService
|
||||
)
|
||||
|
||||
private fun createMediaFoldersDetectionWork(context: Context, params: WorkerParameters): MediaFoldersDetectionWork {
|
||||
return MediaFoldersDetectionWork(
|
||||
private fun createMediaFoldersDetectionWork(context: Context, params: WorkerParameters): MediaFoldersDetectionWork =
|
||||
MediaFoldersDetectionWork(
|
||||
context,
|
||||
params,
|
||||
resources,
|
||||
|
|
@ -202,21 +199,18 @@ class BackgroundJobFactory @Inject constructor(
|
|||
viewThemeUtils.get(),
|
||||
syncedFolderProvider
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNotificationWork(context: Context, params: WorkerParameters): NotificationWork {
|
||||
return NotificationWork(
|
||||
context,
|
||||
params,
|
||||
notificationManager,
|
||||
accountManager,
|
||||
deckApi,
|
||||
viewThemeUtils.get()
|
||||
)
|
||||
}
|
||||
private fun createNotificationWork(context: Context, params: WorkerParameters): NotificationWork = NotificationWork(
|
||||
context,
|
||||
params,
|
||||
notificationManager,
|
||||
accountManager,
|
||||
deckApi,
|
||||
viewThemeUtils.get()
|
||||
)
|
||||
|
||||
private fun createAccountRemovalWork(context: Context, params: WorkerParameters): AccountRemovalWork {
|
||||
return AccountRemovalWork(
|
||||
private fun createAccountRemovalWork(context: Context, params: WorkerParameters): AccountRemovalWork =
|
||||
AccountRemovalWork(
|
||||
context,
|
||||
params,
|
||||
uploadsStorageManager,
|
||||
|
|
@ -227,10 +221,9 @@ class BackgroundJobFactory @Inject constructor(
|
|||
preferences,
|
||||
syncedFolderProvider
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFilesUploadWorker(context: Context, params: WorkerParameters): FileUploadWorker {
|
||||
return FileUploadWorker(
|
||||
private fun createFilesUploadWorker(context: Context, params: WorkerParameters): FileUploadWorker =
|
||||
FileUploadWorker(
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
powerManagementService,
|
||||
|
|
@ -242,20 +235,18 @@ class BackgroundJobFactory @Inject constructor(
|
|||
context,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFilesDownloadWorker(context: Context, params: WorkerParameters): FileDownloadWorker {
|
||||
return FileDownloadWorker(
|
||||
private fun createFilesDownloadWorker(context: Context, params: WorkerParameters): FileDownloadWorker =
|
||||
FileDownloadWorker(
|
||||
viewThemeUtils.get(),
|
||||
accountManager,
|
||||
localBroadcastManager.get(),
|
||||
context,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPDFGenerateWork(context: Context, params: WorkerParameters): GeneratePdfFromImagesWork {
|
||||
return GeneratePdfFromImagesWork(
|
||||
private fun createPDFGenerateWork(context: Context, params: WorkerParameters): GeneratePdfFromImagesWork =
|
||||
GeneratePdfFromImagesWork(
|
||||
appContext = context,
|
||||
generatePdfUseCase = generatePdfUseCase,
|
||||
viewThemeUtils = viewThemeUtils.get(),
|
||||
|
|
@ -264,23 +255,34 @@ class BackgroundJobFactory @Inject constructor(
|
|||
logger = logger,
|
||||
params = params
|
||||
)
|
||||
}
|
||||
|
||||
private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork {
|
||||
return HealthStatusWork(
|
||||
private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork = HealthStatusWork(
|
||||
context,
|
||||
params,
|
||||
accountManager,
|
||||
arbitraryDataProvider,
|
||||
backgroundJobManager.get()
|
||||
)
|
||||
|
||||
private fun createTestJob(context: Context, params: WorkerParameters): TestJob = TestJob(
|
||||
context,
|
||||
params,
|
||||
backgroundJobManager.get()
|
||||
)
|
||||
|
||||
private fun createInternalTwoWaySyncWork(context: Context, params: WorkerParameters): InternalTwoWaySyncWork =
|
||||
InternalTwoWaySyncWork(
|
||||
context,
|
||||
params,
|
||||
accountManager,
|
||||
arbitraryDataProvider,
|
||||
backgroundJobManager.get()
|
||||
powerManagementService,
|
||||
connectivityService,
|
||||
preferences
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestJob(context: Context, params: WorkerParameters): TestJob {
|
||||
return TestJob(
|
||||
context,
|
||||
params,
|
||||
backgroundJobManager.get()
|
||||
)
|
||||
}
|
||||
private fun createMetadataWorker(context: Context, params: WorkerParameters): MetadataWorker = MetadataWorker(
|
||||
context,
|
||||
params,
|
||||
accountManager.user
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ interface BackgroundJobManager {
|
|||
* @param contactsAccountName Target contacts account name; null for local contacts
|
||||
* @param contactsAccountType Target contacts account type; null for local contacts
|
||||
* @param vCardFilePath Path to file containing all contact entries
|
||||
* @param selectedContacts List of contact indices to import from [vCardFilePath] file
|
||||
* @param selectedContactsFilePath File path of list of contact indices to import from [vCardFilePath] file
|
||||
*
|
||||
* @return Job info with current status; status is null if job does not exist
|
||||
*/
|
||||
|
|
@ -103,7 +103,7 @@ interface BackgroundJobManager {
|
|||
contactsAccountName: String?,
|
||||
contactsAccountType: String?,
|
||||
vCardFilePath: String,
|
||||
selectedContacts: IntArray
|
||||
selectedContactsFilePath: String
|
||||
): LiveData<JobInfo?>
|
||||
|
||||
/**
|
||||
|
|
@ -119,13 +119,19 @@ interface BackgroundJobManager {
|
|||
|
||||
fun startImmediateFilesExportJob(files: Collection<OCFile>): LiveData<JobInfo?>
|
||||
|
||||
fun schedulePeriodicFilesSyncJob()
|
||||
fun schedulePeriodicFilesSyncJob(syncedFolderID: Long)
|
||||
|
||||
/**
|
||||
* Immediately start File Sync job for given syncFolderID.
|
||||
*/
|
||||
fun startImmediateFilesSyncJob(
|
||||
syncedFolderID: Long,
|
||||
overridePowerSaving: Boolean = false,
|
||||
changedFiles: Array<String> = arrayOf<String>()
|
||||
changedFiles: Array<String?> = arrayOf<String?>()
|
||||
)
|
||||
|
||||
fun cancelTwoWaySyncJob()
|
||||
|
||||
fun scheduleOfflineSync()
|
||||
|
||||
fun scheduleMediaFoldersDetectionJob()
|
||||
|
|
@ -133,7 +139,7 @@ interface BackgroundJobManager {
|
|||
|
||||
fun startNotificationJob(subject: String, signature: String)
|
||||
fun startAccountRemovalJob(accountName: String, remoteWipe: Boolean)
|
||||
fun startFilesUploadJob(user: User)
|
||||
fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean)
|
||||
fun getFileUploads(user: User): LiveData<List<JobInfo>>
|
||||
fun cancelFilesUploadJob(user: User)
|
||||
fun isStartFileUploadJobScheduled(user: User): Boolean
|
||||
|
|
@ -163,4 +169,10 @@ interface BackgroundJobManager {
|
|||
fun cancelAllJobs()
|
||||
fun schedulePeriodicHealthStatus()
|
||||
fun startHealthStatus()
|
||||
fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean
|
||||
fun startOfflineOperations()
|
||||
fun startPeriodicallyOfflineOperation()
|
||||
fun scheduleInternal2WaySync(intervalMinutes: Long)
|
||||
fun cancelAllFilesDownloadJobs()
|
||||
fun startMetadataSyncJob(currentDirPath: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
import android.provider.MediaStore
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
|
|
@ -26,11 +27,18 @@ import com.nextcloud.client.core.Clock
|
|||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
|
||||
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.upload.FileUploadHelper
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
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.operations.DownloadType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
@ -55,10 +63,10 @@ internal class BackgroundJobManagerImpl(
|
|||
private val workManager: WorkManager,
|
||||
private val clock: Clock,
|
||||
private val preferences: AppPreferences
|
||||
) : BackgroundJobManager, Injectable {
|
||||
) : BackgroundJobManager,
|
||||
Injectable {
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_ALL = "*" // This tag allows us to retrieve list of all jobs run by Nextcloud client
|
||||
const val JOB_CONTENT_OBSERVER = "content_observer"
|
||||
const val JOB_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup"
|
||||
|
|
@ -79,9 +87,12 @@ internal class BackgroundJobManagerImpl(
|
|||
const val JOB_PDF_GENERATION = "pdf_generation"
|
||||
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
|
||||
const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
|
||||
|
||||
const val JOB_OFFLINE_OPERATIONS = "offline_operations"
|
||||
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_METADATA_SYNC = "metadata_sync"
|
||||
const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync"
|
||||
|
||||
const val JOB_TEST = "test_job"
|
||||
|
||||
|
|
@ -95,16 +106,16 @@ internal class BackgroundJobManagerImpl(
|
|||
const val NOT_SET_VALUE = "not set"
|
||||
const val PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L
|
||||
const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L
|
||||
const val OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES = 5L
|
||||
const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L
|
||||
const val DEFAULT_BACKOFF_CRITERIA_DELAY_SEC = 300L
|
||||
|
||||
private const val KEEP_LOG_MILLIS = 1000 * 60 * 60 * 24 * 3L
|
||||
|
||||
fun formatNameTag(name: String, user: User? = null): String {
|
||||
return if (user == null) {
|
||||
"$TAG_PREFIX_NAME:$name"
|
||||
} else {
|
||||
"$TAG_PREFIX_NAME:$name ${user.accountName}"
|
||||
}
|
||||
fun formatNameTag(name: String, user: User? = null): String = if (user == null) {
|
||||
"$TAG_PREFIX_NAME:$name"
|
||||
} else {
|
||||
"$TAG_PREFIX_NAME:$name ${user.accountName}"
|
||||
}
|
||||
|
||||
fun formatUserTag(user: User): String = "$TAG_PREFIX_USER:${user.accountName}"
|
||||
|
|
@ -121,36 +132,32 @@ internal class BackgroundJobManagerImpl(
|
|||
}
|
||||
}
|
||||
|
||||
fun parseTimestamp(timestamp: String): Date {
|
||||
return try {
|
||||
val ms = timestamp.toLong()
|
||||
Date(ms)
|
||||
} catch (ex: NumberFormatException) {
|
||||
Date(0)
|
||||
}
|
||||
fun parseTimestamp(timestamp: String): Date = try {
|
||||
val ms = timestamp.toLong()
|
||||
Date(ms)
|
||||
} catch (ex: NumberFormatException) {
|
||||
Date(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert platform [androidx.work.WorkInfo] object into application-specific [JobInfo] model.
|
||||
* Conversion extracts work metadata from tags.
|
||||
*/
|
||||
fun fromWorkInfo(info: WorkInfo?): JobInfo? {
|
||||
return if (info != null) {
|
||||
val metadata = mutableMapOf<String, String>()
|
||||
info.tags.forEach { parseTag(it)?.let { metadata[it.first] = it.second } }
|
||||
val timestamp = parseTimestamp(metadata.get(TAG_PREFIX_START_TIMESTAMP) ?: "0")
|
||||
JobInfo(
|
||||
id = info.id,
|
||||
state = info.state.toString(),
|
||||
name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE,
|
||||
user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE,
|
||||
started = timestamp,
|
||||
progress = info.progress.getInt("progress", -1),
|
||||
workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
fun fromWorkInfo(info: WorkInfo?): JobInfo? = if (info != null) {
|
||||
val metadata = mutableMapOf<String, String>()
|
||||
info.tags.forEach { parseTag(it)?.let { metadata[it.first] = it.second } }
|
||||
val timestamp = parseTimestamp(metadata.get(TAG_PREFIX_START_TIMESTAMP) ?: "0")
|
||||
JobInfo(
|
||||
id = info.id,
|
||||
state = info.state.toString(),
|
||||
name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE,
|
||||
user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE,
|
||||
started = timestamp,
|
||||
progress = info.progress.getInt("progress", -1),
|
||||
workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun deleteOldLogs(logEntries: MutableList<LogEntry>): MutableList<LogEntry> {
|
||||
|
|
@ -168,6 +175,8 @@ internal class BackgroundJobManagerImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private val defaultDispatcherScope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override fun logStartOfWorker(workerName: String?) {
|
||||
val logs = deleteOldLogs(preferences.readLogEntry().toMutableList())
|
||||
|
||||
|
|
@ -195,13 +204,15 @@ internal class BackgroundJobManagerImpl(
|
|||
private fun oneTimeRequestBuilder(
|
||||
jobClass: KClass<out ListenableWorker>,
|
||||
jobName: String,
|
||||
user: User? = null
|
||||
user: User? = null,
|
||||
constraints: Constraints = Constraints.Builder().build()
|
||||
): OneTimeWorkRequest.Builder {
|
||||
val builder = OneTimeWorkRequest.Builder(jobClass.java)
|
||||
.addTag(TAG_ALL)
|
||||
.addTag(formatNameTag(jobName, user))
|
||||
.addTag(formatTimeTag(clock.currentTime))
|
||||
.addTag(formatClassTag(jobClass))
|
||||
.setConstraints(constraints)
|
||||
user?.let { builder.addTag(formatUserTag(it)) }
|
||||
return builder
|
||||
}
|
||||
|
|
@ -214,7 +225,8 @@ internal class BackgroundJobManagerImpl(
|
|||
jobName: String,
|
||||
intervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
|
||||
flexIntervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
|
||||
user: User? = null
|
||||
user: User? = null,
|
||||
constraints: Constraints = Constraints.Builder().build()
|
||||
): PeriodicWorkRequest.Builder {
|
||||
val builder = PeriodicWorkRequest.Builder(
|
||||
jobClass.java,
|
||||
|
|
@ -227,6 +239,7 @@ internal class BackgroundJobManagerImpl(
|
|||
.addTag(formatNameTag(jobName, user))
|
||||
.addTag(formatTimeTag(clock.currentTime))
|
||||
.addTag(formatClassTag(jobClass))
|
||||
.setConstraints(constraints)
|
||||
user?.let { builder.addTag(formatUserTag(it)) }
|
||||
return builder
|
||||
}
|
||||
|
|
@ -298,13 +311,13 @@ internal class BackgroundJobManagerImpl(
|
|||
contactsAccountName: String?,
|
||||
contactsAccountType: String?,
|
||||
vCardFilePath: String,
|
||||
selectedContacts: IntArray
|
||||
selectedContactsFilePath: String
|
||||
): LiveData<JobInfo?> {
|
||||
val data = Data.Builder()
|
||||
.putString(ContactsImportWork.ACCOUNT_NAME, contactsAccountName)
|
||||
.putString(ContactsImportWork.ACCOUNT_TYPE, contactsAccountType)
|
||||
.putString(ContactsImportWork.VCARD_FILE_PATH, vCardFilePath)
|
||||
.putIntArray(ContactsImportWork.SELECTED_CONTACTS_INDICES, selectedContacts)
|
||||
.putString(ContactsImportWork.SELECTED_CONTACTS_FILE_PATH, selectedContactsFilePath)
|
||||
.build()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
|
|
@ -403,32 +416,134 @@ internal class BackgroundJobManagerImpl(
|
|||
workManager.cancelJob(JOB_PERIODIC_CALENDAR_BACKUP, user)
|
||||
}
|
||||
|
||||
override fun schedulePeriodicFilesSyncJob() {
|
||||
override fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean =
|
||||
workManager.isWorkRunning(JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID) &&
|
||||
workManager.isWorkRunning(JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID)
|
||||
|
||||
override fun startPeriodicallyOfflineOperation() {
|
||||
val inputData = Data.Builder()
|
||||
.putString(OfflineOperationsWorker.JOB_NAME, JOB_PERIODIC_OFFLINE_OPERATIONS)
|
||||
.build()
|
||||
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = OfflineOperationsWorker::class,
|
||||
jobName = JOB_PERIODIC_OFFLINE_OPERATIONS,
|
||||
intervalMins = OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES
|
||||
)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
JOB_PERIODIC_OFFLINE_OPERATIONS,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun startOfflineOperations() {
|
||||
val inputData = Data.Builder()
|
||||
.putString(OfflineOperationsWorker.JOB_NAME, JOB_OFFLINE_OPERATIONS)
|
||||
.build()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
// Backoff criteria define how the system should retry the task if it fails.
|
||||
// LINEAR means each retry will be delayed linearly (e.g., 10s, 20s, 30s...)
|
||||
// DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES is used as the initial delay duration.
|
||||
val backoffCriteriaPolicy = BackoffPolicy.LINEAR
|
||||
val backoffCriteriaDelay = DEFAULT_BACKOFF_CRITERIA_DELAY_SEC
|
||||
|
||||
val request =
|
||||
oneTimeRequestBuilder(OfflineOperationsWorker::class, JOB_OFFLINE_OPERATIONS, constraints = constraints)
|
||||
.setBackoffCriteria(
|
||||
backoffCriteriaPolicy,
|
||||
backoffCriteriaDelay,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
JOB_OFFLINE_OPERATIONS,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun schedulePeriodicFilesSyncJob(syncedFolderID: Long) {
|
||||
val arguments = Data.Builder()
|
||||
.putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
.build()
|
||||
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = FilesSyncWork::class,
|
||||
jobName = JOB_PERIODIC_FILES_SYNC,
|
||||
jobName = JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID,
|
||||
intervalMins = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES
|
||||
).build()
|
||||
workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||
)
|
||||
.setInputData(arguments)
|
||||
.build()
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun startImmediateFilesSyncJob(
|
||||
syncedFolderID: Long,
|
||||
overridePowerSaving: Boolean,
|
||||
changedFiles: Array<String>
|
||||
changedFiles: Array<String?>
|
||||
) {
|
||||
val arguments = Data.Builder()
|
||||
.putBoolean(FilesSyncWork.OVERRIDE_POWER_SAVING, overridePowerSaving)
|
||||
.putStringArray(FilesSyncWork.CHANGED_FILES, changedFiles)
|
||||
.putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
.build()
|
||||
|
||||
val request = oneTimeRequestBuilder(
|
||||
jobClass = FilesSyncWork::class,
|
||||
jobName = JOB_IMMEDIATE_FILES_SYNC
|
||||
jobName = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID
|
||||
)
|
||||
.setInputData(arguments)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(JOB_IMMEDIATE_FILES_SYNC, ExistingWorkPolicy.APPEND, request)
|
||||
workManager.enqueueUniqueWork(
|
||||
JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID,
|
||||
ExistingWorkPolicy.APPEND,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun cancelTwoWaySyncJob() {
|
||||
workManager.cancelJob(JOB_INTERNAL_TWO_WAY_SYNC)
|
||||
}
|
||||
|
||||
override fun cancelAllFilesDownloadJobs() {
|
||||
workManager.cancelAllWorkByTag(formatClassTag(FileDownloadWorker::class))
|
||||
}
|
||||
|
||||
override fun startMetadataSyncJob(currentDirPath: String) {
|
||||
val inputData = Data.Builder()
|
||||
.putString(MetadataWorker.FILE_PATH, currentDirPath)
|
||||
.build()
|
||||
|
||||
val constrains = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
||||
val request = oneTimeRequestBuilder(MetadataWorker::class, JOB_METADATA_SYNC)
|
||||
.setConstraints(constrains)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
JOB_METADATA_SYNC,
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun scheduleOfflineSync() {
|
||||
|
|
@ -491,34 +606,75 @@ internal class BackgroundJobManagerImpl(
|
|||
workManager.enqueue(request)
|
||||
}
|
||||
|
||||
private fun startFileUploadJobTag(user: User): String {
|
||||
return JOB_FILES_UPLOAD + user.accountName
|
||||
private fun startFileUploadJobTag(user: User): String = JOB_FILES_UPLOAD + user.accountName
|
||||
|
||||
override fun isStartFileUploadJobScheduled(user: User): Boolean =
|
||||
workManager.isWorkScheduled(startFileUploadJobTag(user))
|
||||
|
||||
/**
|
||||
* This method supports initiating uploads for various scenarios, including:
|
||||
* - New upload batches
|
||||
* - Failed uploads
|
||||
* - FilesSyncWork
|
||||
* - ...
|
||||
*
|
||||
* @param user The user for whom the upload job is being created.
|
||||
* @param uploadIds Array of upload IDs to be processed. These IDs originate from multiple sources
|
||||
* and cannot be determined directly from the account name or a single function
|
||||
* within the worker.
|
||||
*/
|
||||
override fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean) {
|
||||
defaultDispatcherScope.launch {
|
||||
val batchSize = FileUploadHelper.MAX_FILE_COUNT
|
||||
val batches = uploadIds.toList().chunked(batchSize)
|
||||
val tag = startFileUploadJobTag(user)
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val dataBuilder = Data.Builder()
|
||||
.putBoolean(
|
||||
FileUploadWorker.SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION,
|
||||
showSameFileAlreadyExistsNotification
|
||||
)
|
||||
.putString(FileUploadWorker.ACCOUNT, user.accountName)
|
||||
.putInt(FileUploadWorker.TOTAL_UPLOAD_SIZE, uploadIds.size)
|
||||
|
||||
val workRequests = batches.mapIndexed { index, batch ->
|
||||
dataBuilder
|
||||
.putLongArray(FileUploadWorker.UPLOAD_IDS, batch.toLongArray())
|
||||
.putInt(FileUploadWorker.CURRENT_BATCH_INDEX, index)
|
||||
|
||||
oneTimeRequestBuilder(FileUploadWorker::class, JOB_FILES_UPLOAD, user)
|
||||
.addTag(tag)
|
||||
.setInputData(dataBuilder.build())
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
}
|
||||
|
||||
// Chain the work requests sequentially
|
||||
if (workRequests.isNotEmpty()) {
|
||||
var workChain = workManager.beginUniqueWork(
|
||||
tag,
|
||||
ExistingWorkPolicy.APPEND_OR_REPLACE,
|
||||
workRequests.first()
|
||||
)
|
||||
|
||||
workRequests.drop(1).forEach { request ->
|
||||
workChain = workChain.then(request)
|
||||
}
|
||||
|
||||
workChain.enqueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isStartFileUploadJobScheduled(user: User): Boolean {
|
||||
return workManager.isWorkScheduled(startFileUploadJobTag(user))
|
||||
}
|
||||
private fun startFileDownloadJobTag(user: User, fileId: Long): String =
|
||||
JOB_FOLDER_DOWNLOAD + user.accountName + fileId
|
||||
|
||||
override fun startFilesUploadJob(user: User) {
|
||||
val data = workDataOf(FileUploadWorker.ACCOUNT to user.accountName)
|
||||
|
||||
val tag = startFileUploadJobTag(user)
|
||||
|
||||
val request = oneTimeRequestBuilder(FileUploadWorker::class, JOB_FILES_UPLOAD, user)
|
||||
.addTag(tag)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
private fun startFileDownloadJobTag(user: User, fileId: Long): String {
|
||||
return JOB_FOLDER_DOWNLOAD + user.accountName + fileId
|
||||
}
|
||||
|
||||
override fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean {
|
||||
return workManager.isWorkScheduled(startFileDownloadJobTag(user, fileId))
|
||||
}
|
||||
override fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean =
|
||||
workManager.isWorkScheduled(startFileDownloadJobTag(user, fileId))
|
||||
|
||||
override fun startFileDownloadJob(
|
||||
user: User,
|
||||
|
|
@ -546,7 +702,9 @@ internal class BackgroundJobManagerImpl(
|
|||
.setInputData(data)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.REPLACE, request)
|
||||
// Since for each file new FileDownloadWorker going to be scheduled,
|
||||
// better to use ExistingWorkPolicy.KEEP policy.
|
||||
workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
override fun getFileUploads(user: User): LiveData<List<JobInfo>> {
|
||||
|
|
@ -625,4 +783,16 @@ internal class BackgroundJobManagerImpl(
|
|||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun scheduleInternal2WaySync(intervalMinutes: Long) {
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = InternalTwoWaySyncWork::class,
|
||||
jobName = JOB_INTERNAL_TWO_WAY_SYNC,
|
||||
intervalMins = intervalMinutes
|
||||
)
|
||||
.setInitialDelay(intervalMinutes, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2017 Tobias Kaminsky
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ import android.content.Context
|
|||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import net.fortuna.ical4j.data.CalendarBuilder
|
||||
import third_parties.sufficientlysecure.AndroidCalendar
|
||||
import third_parties.sufficientlysecure.CalendarSource
|
||||
|
|
@ -28,37 +30,56 @@ class CalendarImportWork(
|
|||
|
||||
companion object {
|
||||
const val TAG = "CalendarImportWork"
|
||||
const val SELECTED_CALENDARS = "selected_contacts_indices"
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun doWork(): Result {
|
||||
val calendarPaths = inputData.getStringArray(SELECTED_CALENDARS) ?: arrayOf<String>()
|
||||
val calendars = inputData.keyValueMap as Map<String, AndroidCalendar>
|
||||
val calendars = inputData.keyValueMap as? Map<*, *>
|
||||
if (calendars == null) {
|
||||
logger.d(TAG, "CalendarImportWork cancelled due to null empty input data")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val calendarBuilder = CalendarBuilder()
|
||||
|
||||
for ((path, selectedCalendar) in calendars) {
|
||||
logger.d(TAG, "Import calendar from $path")
|
||||
for ((path, selectedCalendarIndex) in calendars) {
|
||||
try {
|
||||
if (path !is String || selectedCalendarIndex !is Int) {
|
||||
logger.d(TAG, "Skipping wrong input data types: $path - $selectedCalendarIndex")
|
||||
continue
|
||||
}
|
||||
|
||||
val file = File(path)
|
||||
val calendarSource = CalendarSource(
|
||||
file.toURI().toURL().toString(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
appContext
|
||||
)
|
||||
logger.d(TAG, "Import calendar from $path")
|
||||
|
||||
val calendars = AndroidCalendar.loadAll(contentResolver)[0]
|
||||
val file = File(path)
|
||||
val calendarSource = CalendarSource(
|
||||
file.toURI().toURL().toString(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
appContext
|
||||
)
|
||||
|
||||
ProcessVEvent(
|
||||
appContext,
|
||||
calendarBuilder.build(calendarSource.stream),
|
||||
selectedCalendar,
|
||||
true
|
||||
).run()
|
||||
val calendarList = AndroidCalendar.loadAll(contentResolver)
|
||||
if (selectedCalendarIndex >= calendarList.size) {
|
||||
logger.d(TAG, "Skipping selectedCalendarIndex out of bound")
|
||||
continue
|
||||
}
|
||||
|
||||
val selectedCalendar = calendarList[selectedCalendarIndex]
|
||||
|
||||
ProcessVEvent(
|
||||
appContext,
|
||||
calendarBuilder.build(calendarSource.stream),
|
||||
selectedCalendar,
|
||||
true
|
||||
).run()
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "skipping calendarIndex: $selectedCalendarIndex due to: $e")
|
||||
}
|
||||
}
|
||||
|
||||
logger.d(TAG, "CalendarImportWork successfully completed")
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2017 Tobias Kaminsky
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -16,12 +16,16 @@ import android.provider.ContactsContract
|
|||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.logger.Logger
|
||||
import com.nextcloud.utils.extensions.toIntArray
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment
|
||||
import com.owncloud.android.ui.fragment.contactsbackup.VCardComparator
|
||||
import ezvcard.Ezvcard
|
||||
import ezvcard.VCard
|
||||
import org.apache.commons.io.FileUtils
|
||||
import third_parties.ezvcard_android.ContactOperations
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.util.Collections
|
||||
|
|
@ -39,15 +43,27 @@ class ContactsImportWork(
|
|||
const val ACCOUNT_TYPE = "account_type"
|
||||
const val ACCOUNT_NAME = "account_name"
|
||||
const val VCARD_FILE_PATH = "vcard_file_path"
|
||||
const val SELECTED_CONTACTS_INDICES = "selected_contacts_indices"
|
||||
const val SELECTED_CONTACTS_FILE_PATH = "selected_contacts_file_path"
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod", "NestedBlockDepth") // legacy code
|
||||
@Suppress("ComplexMethod", "NestedBlockDepth", "LongMethod", "ReturnCount") // legacy code
|
||||
override fun doWork(): Result {
|
||||
val vCardFilePath = inputData.getString(VCARD_FILE_PATH) ?: ""
|
||||
val contactsAccountName = inputData.getString(ACCOUNT_NAME)
|
||||
val contactsAccountType = inputData.getString(ACCOUNT_TYPE)
|
||||
val selectedContactsIndices = inputData.getIntArray(SELECTED_CONTACTS_INDICES) ?: IntArray(0)
|
||||
val selectedContactsFilePath = inputData.getString(SELECTED_CONTACTS_FILE_PATH)
|
||||
if (selectedContactsFilePath == null) {
|
||||
Log_OC.d(TAG, "selectedContactsFilePath is null")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val selectedContactsFile = File(selectedContactsFilePath)
|
||||
if (!selectedContactsFile.exists()) {
|
||||
Log_OC.d(TAG, "selectedContactsFile not exists")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val selectedContactsIndices = readCheckedContractsFromFile(selectedContactsFile)
|
||||
|
||||
val inputStream = BufferedInputStream(FileInputStream(vCardFilePath))
|
||||
val vCards = ArrayList<VCard>()
|
||||
|
|
@ -79,16 +95,21 @@ class ContactsImportWork(
|
|||
cursor.moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
for (contactIndex in selectedContactsIndices) {
|
||||
val vCard = vCards[contactIndex]
|
||||
if (BackupListFragment.getDisplayName(vCard).isEmpty()) {
|
||||
if (!ownContactMap.containsKey(vCard)) {
|
||||
operations.insertContact(vCard)
|
||||
try {
|
||||
val vCard = vCards[contactIndex]
|
||||
if (BackupListFragment.getDisplayName(vCard).isEmpty()) {
|
||||
if (!ownContactMap.containsKey(vCard)) {
|
||||
operations.insertContact(vCard)
|
||||
} else {
|
||||
operations.updateContact(vCard, ownContactMap[vCard])
|
||||
}
|
||||
} else {
|
||||
operations.updateContact(vCard, ownContactMap[vCard])
|
||||
operations.insertContact(vCard) // Insert All the contacts without name
|
||||
}
|
||||
} else {
|
||||
operations.insertContact(vCard) // Insert All the contacts without name
|
||||
} catch (t: Throwable) {
|
||||
Log_OC.e(TAG, "skipping contactIndex: $contactIndex due to: $t")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -103,9 +124,20 @@ class ContactsImportWork(
|
|||
logger.e(TAG, "Error closing vCard stream", e)
|
||||
}
|
||||
|
||||
Log_OC.d(TAG, "ContractsImportWork successfully completed")
|
||||
selectedContactsFile.delete()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
fun readCheckedContractsFromFile(file: File): IntArray = try {
|
||||
val fileData = FileUtils.readFileToByteArray(file)
|
||||
fileData.toIntArray()
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Exception readCheckedContractsFromFile: $e")
|
||||
intArrayOf()
|
||||
}
|
||||
|
||||
private fun getContactFromCursor(cursor: Cursor): VCard? {
|
||||
val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
|
||||
val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ import androidx.work.WorkerParameters
|
|||
import com.nextcloud.client.device.PowerManagementService
|
||||
import com.owncloud.android.datamodel.SyncedFolderProvider
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.utils.FilesSyncHelper
|
||||
|
||||
/**
|
||||
* This work is triggered when OS detects change in media folders.
|
||||
|
|
@ -23,7 +24,7 @@ import com.owncloud.android.lib.common.utils.Log_OC
|
|||
class ContentObserverWork(
|
||||
appContext: Context,
|
||||
private val params: WorkerParameters,
|
||||
private val syncerFolderProvider: SyncedFolderProvider,
|
||||
private val syncedFolderProvider: SyncedFolderProvider,
|
||||
private val powerManagementService: PowerManagementService,
|
||||
private val backgroundJobManager: BackgroundJobManager
|
||||
) : Worker(appContext, params) {
|
||||
|
|
@ -31,10 +32,12 @@ class ContentObserverWork(
|
|||
override fun doWork(): Result {
|
||||
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
|
||||
|
||||
if (params.triggeredContentUris.size > 0) {
|
||||
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()
|
||||
|
||||
|
|
@ -48,13 +51,19 @@ class ContentObserverWork(
|
|||
}
|
||||
|
||||
private fun checkAndStartFileSyncJob() {
|
||||
val syncFolders = syncerFolderProvider.countEnabledSyncedFolders() > 0
|
||||
if (!powerManagementService.isPowerSavingEnabled && syncFolders) {
|
||||
if (!powerManagementService.isPowerSavingEnabled && syncedFolderProvider.countEnabledSyncedFolders() > 0) {
|
||||
val changedFiles = mutableListOf<String>()
|
||||
for (uri in params.triggeredContentUris) {
|
||||
changedFiles.add(uri.toString())
|
||||
}
|
||||
backgroundJobManager.startImmediateFilesSyncJob(false, changedFiles.toTypedArray())
|
||||
FilesSyncHelper.startFilesSyncForAllFolders(
|
||||
syncedFolderProvider,
|
||||
backgroundJobManager,
|
||||
false,
|
||||
changedFiles.toTypedArray()
|
||||
)
|
||||
} else {
|
||||
Log_OC.w(TAG, "cant startFilesSyncForAllFolders")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-FileCopyrightText: 2022 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -57,6 +57,8 @@ class FilesExportWork(
|
|||
}
|
||||
|
||||
private fun exportFiles(fileIDs: LongArray): Int {
|
||||
val fileDownloadHelper = FileDownloadHelper.instance()
|
||||
|
||||
var successfulExports = 0
|
||||
fileIDs
|
||||
.asSequence()
|
||||
|
|
@ -76,7 +78,11 @@ class FilesExportWork(
|
|||
showErrorNotification(successfulExports)
|
||||
}
|
||||
} else {
|
||||
downloadFile(ocFile)
|
||||
fileDownloadHelper.downloadFile(
|
||||
user,
|
||||
ocFile,
|
||||
downloadType = DownloadType.EXPORT
|
||||
)
|
||||
}
|
||||
|
||||
successfulExports++
|
||||
|
|
@ -95,14 +101,6 @@ class FilesExportWork(
|
|||
)
|
||||
}
|
||||
|
||||
private fun downloadFile(ocFile: OCFile) {
|
||||
FileDownloadHelper.instance().downloadFile(
|
||||
user,
|
||||
ocFile,
|
||||
downloadType = DownloadType.EXPORT
|
||||
)
|
||||
}
|
||||
|
||||
private fun showErrorNotification(successfulExports: Int) {
|
||||
val message = if (successfulExports == 0) {
|
||||
appContext.resources.getQuantityString(R.plurals.export_failed, successfulExports, successfulExports)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Jonas Mayer <jonas.mayer@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
|
|
@ -25,10 +23,8 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker
|
|||
import com.nextcloud.client.network.ConnectivityService
|
||||
import com.nextcloud.client.preferences.SubFolderRule
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
|
||||
import com.owncloud.android.datamodel.FilesystemDataProvider
|
||||
import com.owncloud.android.datamodel.ForegroundServiceType
|
||||
import com.owncloud.android.datamodel.MediaFolderType
|
||||
import com.owncloud.android.datamodel.SyncedFolder
|
||||
import com.owncloud.android.datamodel.SyncedFolderProvider
|
||||
|
|
@ -36,7 +32,6 @@ import com.owncloud.android.datamodel.UploadsStorageManager
|
|||
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
|
||||
|
|
@ -64,106 +59,172 @@ class FilesSyncWork(
|
|||
const val TAG = "FilesSyncJob"
|
||||
const val OVERRIDE_POWER_SAVING = "overridePowerSaving"
|
||||
const val CHANGED_FILES = "changedFiles"
|
||||
const val FOREGROUND_SERVICE_ID = 414
|
||||
const val SYNCED_FOLDER_ID = "syncedFolderId"
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun updateForegroundWorker(progressPercent: Int, useForegroundWorker: Boolean) {
|
||||
if (!useForegroundWorker) {
|
||||
return
|
||||
}
|
||||
private lateinit var syncedFolder: SyncedFolder
|
||||
|
||||
// update throughout worker execution to give use feedback how far worker is
|
||||
val notification = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_FILE_SYNC)
|
||||
.setTicker(context.getString(R.string.autoupload_worker_foreground_info))
|
||||
.setContentText(context.getString(R.string.autoupload_worker_foreground_info))
|
||||
.setSmallIcon(R.drawable.notification_icon)
|
||||
.setContentTitle(context.getString(R.string.autoupload_worker_foreground_info))
|
||||
.setOngoing(true)
|
||||
.setProgress(100, progressPercent, false)
|
||||
.build()
|
||||
val foregroundInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ForegroundInfo(FOREGROUND_SERVICE_ID, notification, ForegroundServiceType.DataSync.getId())
|
||||
} else {
|
||||
ForegroundInfo(FOREGROUND_SERVICE_ID, notification)
|
||||
}
|
||||
|
||||
setForegroundAsync(foregroundInfo)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Suppress("MagicNumber", "ReturnCount")
|
||||
override fun doWork(): Result {
|
||||
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
|
||||
Log_OC.d(TAG, "File-sync worker started")
|
||||
val syncFolderId = inputData.getLong(SYNCED_FOLDER_ID, -1)
|
||||
val changedFiles = inputData.getStringArray(CHANGED_FILES)
|
||||
|
||||
val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false)
|
||||
// If we are in power save mode, better to postpone upload
|
||||
if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) {
|
||||
val result = Result.success()
|
||||
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
|
||||
return result
|
||||
}
|
||||
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class) + "_" + syncFolderId)
|
||||
Log_OC.d(TAG, "AutoUpload started folder ID: $syncFolderId")
|
||||
|
||||
// Create all the providers we'll need
|
||||
val resources = context.resources
|
||||
val lightVersion = resources.getBoolean(R.bool.syncedFolder_light)
|
||||
FilesSyncHelper.restartJobsIfNeeded(
|
||||
val filesystemDataProvider = FilesystemDataProvider(contentResolver)
|
||||
val currentLocale = resources.configuration.locale
|
||||
val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id)
|
||||
|
||||
if (!setSyncedFolder(syncFolderId)) {
|
||||
Log_OC.w(TAG, "AutoUpload skipped since syncedFolder ($syncFolderId) is not enabled!")
|
||||
return logEndOfWorker(syncFolderId)
|
||||
}
|
||||
|
||||
// Always first try to schedule uploads to make sure files are uploaded even if worker was killed to early
|
||||
uploadFilesFromFolder(
|
||||
context,
|
||||
resources,
|
||||
lightVersion,
|
||||
filesystemDataProvider,
|
||||
currentLocale,
|
||||
dateFormat,
|
||||
syncedFolder
|
||||
)
|
||||
|
||||
if (canExitEarly(changedFiles, syncFolderId)) {
|
||||
Log_OC.w(TAG, "AutoUpload skipped canExit conditions are met")
|
||||
return logEndOfWorker(syncFolderId)
|
||||
}
|
||||
|
||||
val user = userAccountManager.getUser(syncedFolder.account)
|
||||
if (user.isPresent) {
|
||||
var uploadIds = uploadsStorageManager.getCurrentUploadIds(user.get().accountName)
|
||||
backgroundJobManager.startFilesUploadJob(user.get(), uploadIds, false)
|
||||
}
|
||||
|
||||
// Get changed files from ContentObserverWork (only images and videos) or by scanning filesystem
|
||||
Log_OC.d(
|
||||
TAG,
|
||||
"AutoUpload (${syncedFolder.remotePath}) changed files from observer: " +
|
||||
changedFiles.contentToString()
|
||||
)
|
||||
collectChangedFiles(changedFiles)
|
||||
Log_OC.d(TAG, "AutoUpload (${syncedFolder.remotePath}) finished checking files.")
|
||||
|
||||
uploadFilesFromFolder(
|
||||
context,
|
||||
resources,
|
||||
lightVersion,
|
||||
filesystemDataProvider,
|
||||
currentLocale,
|
||||
dateFormat,
|
||||
syncedFolder
|
||||
)
|
||||
|
||||
FilesSyncHelper.restartUploadsIfNeeded(
|
||||
uploadsStorageManager,
|
||||
userAccountManager,
|
||||
connectivityService,
|
||||
powerManagementService
|
||||
)
|
||||
|
||||
// Get changed files from ContentObserverWork (only images and videos) or by scanning filesystem
|
||||
val changedFiles = inputData.getStringArray(CHANGED_FILES)
|
||||
Log_OC.d(TAG, "File-sync worker changed files from observer: " + changedFiles.contentToString())
|
||||
collectChangedFiles(changedFiles)
|
||||
Log_OC.d(TAG, "File-sync worker finished checking files.")
|
||||
return logEndOfWorker(syncFolderId)
|
||||
}
|
||||
|
||||
// Create all the providers we'll need
|
||||
val filesystemDataProvider = FilesystemDataProvider(contentResolver)
|
||||
val currentLocale = resources.configuration.locale
|
||||
val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id)
|
||||
|
||||
// start upload of changed / new files
|
||||
val syncedFolders = syncedFolderProvider.syncedFolders
|
||||
for ((index, syncedFolder) in syncedFolders.withIndex()) {
|
||||
updateForegroundWorker(
|
||||
(50 + (index.toDouble() / syncedFolders.size.toDouble()) * 50).toInt(),
|
||||
changedFiles.isNullOrEmpty()
|
||||
)
|
||||
if (syncedFolder.isEnabled) {
|
||||
syncFolder(
|
||||
context,
|
||||
resources,
|
||||
lightVersion,
|
||||
filesystemDataProvider,
|
||||
currentLocale,
|
||||
dateFormat,
|
||||
syncedFolder
|
||||
)
|
||||
}
|
||||
}
|
||||
Log_OC.d(TAG, "File-sync worker finished")
|
||||
private fun logEndOfWorker(syncFolderId: Long): Result {
|
||||
Log_OC.d(TAG, "AutoUpload worker (${syncedFolder.remotePath}) finished")
|
||||
val result = Result.success()
|
||||
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
|
||||
backgroundJobManager.logEndOfWorker(
|
||||
BackgroundJobManagerImpl.formatClassTag(this::class) +
|
||||
"_" + syncFolderId,
|
||||
result
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun setSyncedFolder(syncedFolderID: Long): Boolean {
|
||||
val syncedFolderTmp = syncedFolderProvider.getSyncedFolderByID(syncedFolderID)
|
||||
if (syncedFolderTmp == null || !syncedFolderTmp.isEnabled) {
|
||||
return false
|
||||
}
|
||||
syncedFolder = syncedFolderTmp
|
||||
return true
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private fun canExitEarly(changedFiles: Array<String>?, syncedFolderID: Long): Boolean {
|
||||
// If we are in power save mode better to postpone scan and upload
|
||||
val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false)
|
||||
if ((powerManagementService.isPowerSavingEnabled && !overridePowerSaving)) {
|
||||
Log_OC.w(TAG, "AutoUpload skipped powerSaving is enabled!")
|
||||
return true
|
||||
}
|
||||
|
||||
if (syncedFolderID < 0) {
|
||||
Log_OC.w(TAG, "AutoUpload skipped no valid syncedFolderID provided")
|
||||
return true
|
||||
}
|
||||
|
||||
// or sync worker already running
|
||||
if (backgroundJobManager.bothFilesSyncJobsRunning(syncedFolderID)) {
|
||||
Log_OC.w(TAG, "AutoUpload skipped another worker instance is running for $syncedFolderID")
|
||||
return true
|
||||
}
|
||||
|
||||
val calculatedScanInterval =
|
||||
FilesSyncHelper.calculateScanInterval(syncedFolder, connectivityService, powerManagementService)
|
||||
val totalScanInterval = (syncedFolder.lastScanTimestampMs + calculatedScanInterval)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val passedScanInterval = totalScanInterval <= currentTime
|
||||
|
||||
Log_OC.d(TAG, "AutoUpload lastScanTimestampMs: " + syncedFolder.lastScanTimestampMs)
|
||||
Log_OC.d(TAG, "AutoUpload calculatedScanInterval: $calculatedScanInterval")
|
||||
Log_OC.d(TAG, "AutoUpload totalScanInterval: $totalScanInterval")
|
||||
Log_OC.d(TAG, "AutoUpload currentTime: $currentTime")
|
||||
Log_OC.d(TAG, "AutoUpload passedScanInterval: $passedScanInterval")
|
||||
|
||||
if (!passedScanInterval && changedFiles.isNullOrEmpty() && !overridePowerSaving) {
|
||||
Log_OC.w(
|
||||
TAG,
|
||||
"AutoUpload skipped since started before scan interval and nothing todo: " + syncedFolder.localPath
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (syncedFolder.isChargingOnly &&
|
||||
!powerManagementService.battery.isCharging &&
|
||||
!powerManagementService.battery.isFull
|
||||
) {
|
||||
Log_OC.w(
|
||||
TAG,
|
||||
"AutoUpload skipped since phone is not charging: " + syncedFolder.localPath
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun collectChangedFiles(changedFiles: Array<String>?) {
|
||||
if (!changedFiles.isNullOrEmpty()) {
|
||||
FilesSyncHelper.insertChangedEntries(syncedFolderProvider, changedFiles)
|
||||
FilesSyncHelper.insertChangedEntries(syncedFolder, changedFiles)
|
||||
} else {
|
||||
// Check every file in every synced folder for changes and update
|
||||
// filesystemDataProvider database (potentially needs a long time so use foreground worker)
|
||||
updateForegroundWorker(5, true)
|
||||
FilesSyncHelper.insertAllDBEntries(syncedFolderProvider)
|
||||
updateForegroundWorker(50, true)
|
||||
// Check every file in synced folder for changes and update
|
||||
// filesystemDataProvider database (potentially needs a long time)
|
||||
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder)
|
||||
}
|
||||
syncedFolder.lastScanTimestampMs = System.currentTimeMillis()
|
||||
syncedFolderProvider.updateSyncFolder(syncedFolder)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod") // legacy code
|
||||
private fun syncFolder(
|
||||
private fun uploadFilesFromFolder(
|
||||
context: Context,
|
||||
resources: Resources,
|
||||
lightVersion: Boolean,
|
||||
|
|
@ -175,66 +236,90 @@ class FilesSyncWork(
|
|||
val uploadAction: Int?
|
||||
val needsCharging: Boolean
|
||||
val needsWifi: Boolean
|
||||
var file: File
|
||||
val accountName = syncedFolder.account
|
||||
|
||||
val optionalUser = userAccountManager.getUser(accountName)
|
||||
if (!optionalUser.isPresent) {
|
||||
Log_OC.w(TAG, "AutoUpload:uploadFilesFromFolder skipped user not present")
|
||||
return
|
||||
}
|
||||
|
||||
val user = optionalUser.get()
|
||||
val arbitraryDataProvider: ArbitraryDataProvider? = if (lightVersion) {
|
||||
val arbitraryDataProvider = if (lightVersion) {
|
||||
ArbitraryDataProviderImpl(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Ensure only new files are processed for upload.
|
||||
// Files that have been previously uploaded cannot be re-uploaded,
|
||||
// even if they have been deleted or moved from the target folder,
|
||||
// as they are already marked as uploaded in the database.
|
||||
val paths = filesystemDataProvider.getFilesForUpload(
|
||||
syncedFolder.localPath,
|
||||
syncedFolder.id.toString()
|
||||
)
|
||||
|
||||
if (paths.size == 0) {
|
||||
if (paths.isEmpty()) {
|
||||
Log_OC.w(TAG, "AutoUpload:uploadFilesFromFolder skipped paths is empty")
|
||||
return
|
||||
}
|
||||
|
||||
val pathsAndMimes = paths.map { path ->
|
||||
file = File(path)
|
||||
val file = File(path)
|
||||
val localPath = file.absolutePath
|
||||
val remotePath = getRemotePath(file, syncedFolder, sFormatter, lightVersion, resources, currentLocale)
|
||||
val mimeType = MimeTypeUtil.getBestMimeTypeByFilename(localPath)
|
||||
|
||||
Log_OC.d(TAG, "AutoUpload:pathsAndMimes file.path: ${file.path}")
|
||||
Log_OC.d(TAG, "AutoUpload:pathsAndMimes localPath: $localPath")
|
||||
Log_OC.d(TAG, "AutoUpload:pathsAndMimes remotePath: $remotePath")
|
||||
Log_OC.d(TAG, "AutoUpload:pathsAndMimes mimeType: $mimeType")
|
||||
|
||||
Triple(
|
||||
localPath,
|
||||
getRemotePath(file, syncedFolder, sFormatter, lightVersion, resources, currentLocale),
|
||||
MimeTypeUtil.getBestMimeTypeByFilename(localPath)
|
||||
remotePath,
|
||||
mimeType
|
||||
)
|
||||
}
|
||||
|
||||
val localPaths = pathsAndMimes.map { it.first }.toTypedArray()
|
||||
val remotePaths = pathsAndMimes.map { it.second }.toTypedArray()
|
||||
|
||||
if (lightVersion) {
|
||||
Log_OC.d(TAG, "AutoUpload:uploadFilesFromFolder light version is used")
|
||||
|
||||
needsCharging = resources.getBoolean(R.bool.syncedFolder_light_on_charging)
|
||||
needsWifi = arbitraryDataProvider!!.getBooleanValue(
|
||||
needsWifi = arbitraryDataProvider?.getBooleanValue(
|
||||
accountName,
|
||||
SettingsActivity.SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI
|
||||
)
|
||||
) ?: true
|
||||
|
||||
val uploadActionString = resources.getString(R.string.syncedFolder_light_upload_behaviour)
|
||||
uploadAction = getUploadAction(uploadActionString)
|
||||
Log_OC.d(TAG, "AutoUpload upload action is: $uploadAction")
|
||||
} else {
|
||||
Log_OC.d(TAG, "AutoUpload:uploadFilesFromFolder not light version is used")
|
||||
|
||||
needsCharging = syncedFolder.isChargingOnly
|
||||
needsWifi = syncedFolder.isWifiOnly
|
||||
uploadAction = syncedFolder.uploadAction
|
||||
}
|
||||
|
||||
FileUploadHelper.instance().uploadNewFiles(
|
||||
user,
|
||||
localPaths,
|
||||
remotePaths,
|
||||
uploadAction!!,
|
||||
true, // create parent folder if not existent
|
||||
uploadAction,
|
||||
// create parent folder if not existent
|
||||
true,
|
||||
UploadFileOperation.CREATED_AS_INSTANT_PICTURE,
|
||||
needsWifi,
|
||||
needsCharging,
|
||||
syncedFolder.nameCollisionPolicy
|
||||
syncedFolder.nameCollisionPolicy,
|
||||
false
|
||||
)
|
||||
|
||||
for (path in paths) {
|
||||
// TODO batch update
|
||||
filesystemDataProvider.updateFilesystemFileAsSentForUpload(
|
||||
path,
|
||||
syncedFolder.id.toString()
|
||||
|
|
@ -255,10 +340,14 @@ class FilesSyncWork(
|
|||
val useSubfolders: Boolean
|
||||
val subFolderRule: SubFolderRule
|
||||
if (lightVersion) {
|
||||
Log_OC.d(TAG, "AutoUpload:getRemotePath light version is used")
|
||||
|
||||
useSubfolders = resources.getBoolean(R.bool.syncedFolder_light_use_subfolders)
|
||||
remoteFolder = resources.getString(R.string.syncedFolder_remote_folder)
|
||||
subFolderRule = SubFolderRule.YEAR_MONTH
|
||||
} else {
|
||||
Log_OC.d(TAG, "AutoUpload:getRemotePath not light version is used")
|
||||
|
||||
useSubfolders = syncedFolder.isSubfolderByDate
|
||||
remoteFolder = syncedFolder.remotePath
|
||||
subFolderRule = syncedFolder.subfolderRule
|
||||
|
|
@ -286,6 +375,8 @@ class FilesSyncWork(
|
|||
): Long {
|
||||
var lastModificationTime = file.lastModified()
|
||||
if (MediaFolderType.IMAGE == syncedFolder.type && hasExif(file)) {
|
||||
Log_OC.d(TAG, "AutoUpload:calculateLastModificationTime exif found")
|
||||
|
||||
@Suppress("TooGenericExceptionCaught") // legacy code
|
||||
try {
|
||||
val exifInterface = ExifInterface(file.absolutePath)
|
||||
|
|
@ -294,6 +385,9 @@ class FilesSyncWork(
|
|||
val pos = ParsePosition(0)
|
||||
val dateTime = formatter.parse(exifDate, pos)
|
||||
lastModificationTime = dateTime.time
|
||||
Log_OC.w(TAG, "AutoUpload:calculateLastModificationTime calculatedTime is: $lastModificationTime")
|
||||
} else {
|
||||
Log_OC.w(TAG, "AutoUpload:calculateLastModificationTime exifDate is empty")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.d(TAG, "Failed to get the proper time " + e.localizedMessage)
|
||||
|
|
@ -302,12 +396,10 @@ class FilesSyncWork(
|
|||
return lastModificationTime
|
||||
}
|
||||
|
||||
private fun getUploadAction(action: String): Int? {
|
||||
return when (action) {
|
||||
"LOCAL_BEHAVIOUR_FORGET" -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET
|
||||
"LOCAL_BEHAVIOUR_MOVE" -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE
|
||||
"LOCAL_BEHAVIOUR_DELETE" -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE
|
||||
else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET
|
||||
}
|
||||
private fun getUploadAction(action: String): Int = when (action) {
|
||||
"LOCAL_BEHAVIOUR_FORGET" -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET
|
||||
"LOCAL_BEHAVIOUR_MOVE" -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE
|
||||
"LOCAL_BEHAVIOUR_DELETE" -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE
|
||||
else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.device.PowerManagementService
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.SynchronizeFolderOperation
|
||||
import com.owncloud.android.utils.FileStorageUtils
|
||||
import java.io.File
|
||||
|
||||
@Suppress("Detekt.NestedBlockDepth", "ReturnCount", "LongParameterList")
|
||||
class InternalTwoWaySyncWork(
|
||||
private val context: Context,
|
||||
params: WorkerParameters,
|
||||
private val userAccountManager: UserAccountManager,
|
||||
private val powerManagementService: PowerManagementService,
|
||||
private val connectivityService: ConnectivityService,
|
||||
private val appPreferences: AppPreferences
|
||||
) : Worker(context, params) {
|
||||
private var shouldRun = true
|
||||
private var operation: SynchronizeFolderOperation? = null
|
||||
|
||||
override fun doWork(): Result {
|
||||
Log_OC.d(TAG, "Worker started!")
|
||||
|
||||
var result = true
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
if (!appPreferences.isTwoWaySyncEnabled ||
|
||||
powerManagementService.isPowerSavingEnabled ||
|
||||
!connectivityService.isConnected ||
|
||||
connectivityService.isInternetWalled ||
|
||||
!connectivityService.connectivity.isWifi
|
||||
) {
|
||||
Log_OC.d(TAG, "Not starting due to constraints!")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val users = userAccountManager.allUsers
|
||||
|
||||
for (user in users) {
|
||||
val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
|
||||
val folders = fileDataStorageManager.getInternalTwoWaySyncFolders(user)
|
||||
|
||||
for (folder in folders) {
|
||||
if (!shouldRun) {
|
||||
Log_OC.d(TAG, "Worker was stopped!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
checkFreeSpace(folder)?.let { checkFreeSpaceResult ->
|
||||
return checkFreeSpaceResult
|
||||
}
|
||||
|
||||
Log_OC.d(TAG, "Folder ${folder.remotePath}: started!")
|
||||
operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager, true)
|
||||
val operationResult = operation?.execute(context)
|
||||
|
||||
if (operationResult?.isSuccess == true) {
|
||||
Log_OC.d(TAG, "Folder ${folder.remotePath}: finished!")
|
||||
} else {
|
||||
Log_OC.d(TAG, "Folder ${folder.remotePath} failed!")
|
||||
result = false
|
||||
}
|
||||
|
||||
folder.apply {
|
||||
operationResult?.let {
|
||||
internalFolderSyncResult = it.code.toString()
|
||||
}
|
||||
|
||||
internalFolderSyncTimestamp = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fileDataStorageManager.saveFile(folder)
|
||||
}
|
||||
}
|
||||
|
||||
return if (result) {
|
||||
Log_OC.d(TAG, "Worker finished with success!")
|
||||
Result.success()
|
||||
} else {
|
||||
Log_OC.d(TAG, "Worker finished with failure!")
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
Log_OC.d(TAG, "OnStopped of worker called!")
|
||||
operation?.cancel()
|
||||
shouldRun = false
|
||||
super.onStopped()
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun checkFreeSpace(folder: OCFile): Result? {
|
||||
val storagePath = folder.storagePath ?: MainApp.getStoragePath()
|
||||
val file = File(storagePath)
|
||||
|
||||
if (!file.exists()) return null
|
||||
|
||||
return try {
|
||||
val freeSpaceLeft = file.freeSpace
|
||||
val localFolder = File(storagePath, MainApp.getDataFolder())
|
||||
val localFolderSize = FileStorageUtils.getFolderSize(localFolder)
|
||||
val remoteFolderSize = folder.fileLength
|
||||
|
||||
if (freeSpaceLeft < (remoteFolderSize - localFolderSize)) {
|
||||
Log_OC.d(TAG, "Not enough space left!")
|
||||
Result.failure()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.d(TAG, "Error caught at checkFreeSpace: $e")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "InternalTwoWaySyncWork"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -27,9 +27,7 @@ class JobsModule {
|
|||
.build()
|
||||
|
||||
val contextWrapper = object : ContextWrapper(context) {
|
||||
override fun getApplicationContext(): Context {
|
||||
return this
|
||||
}
|
||||
override fun getApplicationContext(): Context = this
|
||||
}
|
||||
|
||||
WorkManager.initialize(contextWrapper, configuration)
|
||||
|
|
@ -42,7 +40,5 @@ class JobsModule {
|
|||
workManager: WorkManager,
|
||||
clock: Clock,
|
||||
preferences: AppPreferences
|
||||
): BackgroundJobManager {
|
||||
return BackgroundJobManagerImpl(workManager, clock, preferences)
|
||||
}
|
||||
): BackgroundJobManager = BackgroundJobManagerImpl(workManager, clock, preferences)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* Copyright (C) 2018 Andy Scherzinger
|
||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ import com.owncloud.android.datamodel.MediaFoldersModel
|
|||
import com.owncloud.android.datamodel.MediaProvider
|
||||
import com.owncloud.android.datamodel.SyncedFolderProvider
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.ui.activity.ManageAccountsActivity.PENDING_FOR_REMOVAL
|
||||
import com.owncloud.android.ui.activity.ManageAccountsActivity
|
||||
import com.owncloud.android.ui.activity.SyncedFoldersActivity
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.SyncedFolderUtils
|
||||
|
|
@ -73,7 +73,7 @@ class MediaFoldersDetectionWork constructor(
|
|||
|
||||
private val randomIdGenerator = Random(clock.currentTime)
|
||||
|
||||
@Suppress("LongMethod", "ComplexMethod", "NestedBlockDepth") // legacy code
|
||||
@Suppress("LongMethod", "ComplexMethod", "NestedBlockDepth", "ReturnCount") // legacy code
|
||||
override fun doWork(): Result {
|
||||
val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(context)
|
||||
val gson = Gson()
|
||||
|
|
@ -134,7 +134,7 @@ class MediaFoldersDetectionWork constructor(
|
|||
val allUsers = userAccountManager.allUsers
|
||||
val activeUsers: MutableList<User> = ArrayList()
|
||||
for (user in allUsers) {
|
||||
if (!arbitraryDataProvider.getBooleanValue(user, PENDING_FOR_REMOVAL)) {
|
||||
if (!arbitraryDataProvider.getBooleanValue(user, ManageAccountsActivity.PENDING_FOR_REMOVAL)) {
|
||||
activeUsers.add(user)
|
||||
}
|
||||
}
|
||||
|
|
@ -190,6 +190,7 @@ class MediaFoldersDetectionWork constructor(
|
|||
gson.toJson(mediaFoldersModel)
|
||||
)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.AuthenticatorException
|
||||
import android.accounts.OperationCanceledException
|
||||
import android.app.Activity
|
||||
|
|
@ -14,10 +15,12 @@ import android.app.PendingIntent
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.RingtoneManager
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.Worker
|
||||
|
|
@ -29,6 +32,7 @@ import com.nextcloud.client.integrations.deck.DeckApi
|
|||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.DecryptedPushMessage
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.OwnCloudClientFactory
|
||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
|
|
@ -223,8 +227,17 @@ class NotificationWork constructor(
|
|||
}
|
||||
.build()
|
||||
)
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
notificationManager.notify(notification.getNotificationId(), notificationBuilder.build())
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Log_OC.w(this, "Missing permission to post notifications")
|
||||
} else {
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
notificationManager.notify(notification.getNotificationId(), notificationBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught") // legacy code
|
||||
|
|
@ -236,8 +249,7 @@ class NotificationWork constructor(
|
|||
}
|
||||
val user = optionalUser.get()
|
||||
try {
|
||||
val client = OwnCloudClientManagerFactory.getDefaultSingleton()
|
||||
.getClientFor(user.toOwnCloudAccount(), context)
|
||||
val client = OwnCloudClientFactory.createNextcloudClient(user, context)
|
||||
val result = GetNotificationRemoteOperation(decryptedPushMessage.nid)
|
||||
.execute(client)
|
||||
if (result.isSuccess) {
|
||||
|
|
@ -287,6 +299,7 @@ class NotificationWork constructor(
|
|||
val user = optionalUser.get()
|
||||
val client = OwnCloudClientManagerFactory.getDefaultSingleton()
|
||||
.getClientFor(user.toOwnCloudAccount(), context)
|
||||
val nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, context)
|
||||
val actionType = intent.getStringExtra(KEY_NOTIFICATION_ACTION_TYPE)
|
||||
val actionLink = intent.getStringExtra(KEY_NOTIFICATION_ACTION_LINK)
|
||||
val success: Boolean = if (!actionType.isNullOrEmpty() && !actionLink.isNullOrEmpty()) {
|
||||
|
|
@ -294,7 +307,7 @@ class NotificationWork constructor(
|
|||
resultCode == HttpStatus.SC_OK || resultCode == HttpStatus.SC_ACCEPTED
|
||||
} else {
|
||||
DeleteNotificationRemoteOperation(numericNotificationId)
|
||||
.execute(client).isSuccess
|
||||
.execute(nextcloudClient).isSuccess
|
||||
}
|
||||
if (success) {
|
||||
if (oldNotification == null) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* Copyright (C) 2018 Mario Danic
|
||||
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ import com.owncloud.android.utils.FileStorageUtils
|
|||
import java.io.File
|
||||
|
||||
@Suppress("LongParameterList") // Legacy code
|
||||
class OfflineSyncWork constructor(
|
||||
class OfflineSyncWork(
|
||||
private val context: Context,
|
||||
params: WorkerParameters,
|
||||
private val contentResolver: ContentResolver,
|
||||
|
|
@ -65,7 +65,7 @@ class OfflineSyncWork constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val updatedEtag = checkEtagChanged(folderName, storageManager, user) ?: return
|
||||
val updatedEtag = checkETagChanged(folderName, storageManager, user) ?: return
|
||||
|
||||
// iterate over downloaded files
|
||||
val files = folder.listFiles { obj: File -> obj.isFile }
|
||||
|
|
@ -77,7 +77,9 @@ class OfflineSyncWork constructor(
|
|||
user,
|
||||
true,
|
||||
context,
|
||||
storageManager
|
||||
storageManager,
|
||||
true,
|
||||
false
|
||||
)
|
||||
synchronizeFileOperation.execute(context)
|
||||
}
|
||||
|
|
@ -101,41 +103,39 @@ class OfflineSyncWork constructor(
|
|||
}
|
||||
|
||||
/**
|
||||
* @return new etag if changed, `null` otherwise
|
||||
* @return new eTag if changed, `null` otherwise
|
||||
*/
|
||||
private fun checkEtagChanged(folderName: String, storageManager: FileDataStorageManager, user: User): String? {
|
||||
val ocFolder = storageManager.getFileByPath(folderName) ?: return null
|
||||
private fun checkETagChanged(folderName: String, storageManager: FileDataStorageManager, user: User): String? {
|
||||
val folder = storageManager.getFileByEncryptedRemotePath(folderName) ?: return null
|
||||
|
||||
Log_OC.d(TAG, "$folderName: currentEtag: ${ocFolder.etag}")
|
||||
Log_OC.d(TAG, "$folderName: current eTag: ${folder.etag}")
|
||||
|
||||
// check for etag change, if false, skip
|
||||
val checkEtagOperation = CheckEtagRemoteOperation(
|
||||
ocFolder.remotePath,
|
||||
ocFolder.etagOnServer
|
||||
)
|
||||
val result = checkEtagOperation.execute(user, context)
|
||||
val operation = CheckEtagRemoteOperation(folder.remotePath, folder.etagOnServer)
|
||||
val result = operation.execute(user, context)
|
||||
|
||||
return when (result.code) {
|
||||
ResultCode.ETAG_UNCHANGED -> {
|
||||
Log_OC.d(TAG, "$folderName: eTag unchanged")
|
||||
null
|
||||
}
|
||||
ResultCode.FILE_NOT_FOUND -> {
|
||||
val removalResult = storageManager.removeFolder(ocFolder, true, true)
|
||||
val removalResult = storageManager.removeFolder(folder, true, true)
|
||||
if (!removalResult) {
|
||||
Log_OC.e(TAG, "removal of " + ocFolder.storagePath + " failed: file not found")
|
||||
Log_OC.e(TAG, "removal of " + folder.storagePath + " failed: file not found")
|
||||
}
|
||||
null
|
||||
}
|
||||
ResultCode.ETAG_CHANGED -> {
|
||||
Log_OC.d(TAG, "$folderName: eTag changed")
|
||||
result.data[0] as String
|
||||
result?.data?.get(0) as? String
|
||||
}
|
||||
else -> if (connectivityService.isInternetWalled) {
|
||||
Log_OC.d(TAG, "No connectivity, skipping sync")
|
||||
null
|
||||
} else {
|
||||
Log_OC.d(TAG, "$folderName: eTag changed")
|
||||
result.data[0] as String
|
||||
result?.data?.get(0) as? String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
|
|
@ -11,11 +11,8 @@ import androidx.work.Data
|
|||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
|
||||
class TestJob(
|
||||
appContext: Context,
|
||||
params: WorkerParameters,
|
||||
private val backgroundJobManager: BackgroundJobManager
|
||||
) : Worker(appContext, params) {
|
||||
class TestJob(appContext: Context, params: WorkerParameters, private val backgroundJobManager: BackgroundJobManager) :
|
||||
Worker(appContext, params) {
|
||||
|
||||
companion object {
|
||||
private const val MAX_PROGRESS = 100
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.clipboard
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
|
||||
class ClipboardClearWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
private val tag = ClipboardClearWorker::class.java.name
|
||||
|
||||
companion object {
|
||||
const val CLIPBOARD_TEXT = "clipboard_text"
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "ReturnCount")
|
||||
override fun doWork(): Result {
|
||||
try {
|
||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val currentClip = clipboardManager.primaryClip ?: return Result.success()
|
||||
val clipboardText = currentClip.getItemAt(0).text?.toString() ?: return Result.success()
|
||||
val copiedText = inputData.getString(CLIPBOARD_TEXT)
|
||||
if (copiedText != clipboardText) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
clipboardManager.clearPrimaryClip()
|
||||
} else {
|
||||
val newEmptyClip = ClipData.newPlainText("EmptyClipContent", "")
|
||||
clipboardManager.setPrimaryClip(newEmptyClip)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(tag, "Error in clipboard clear worker", e)
|
||||
return Result.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +1,49 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.nextcloud.client.jobs.notification.WorkerNotificationManager
|
||||
import com.nextcloud.utils.numberFormatter.NumberFormatter
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.resources.files.FileUtils
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class DownloadNotificationManager(
|
||||
private val id: Int,
|
||||
private val context: Context,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) {
|
||||
private var notification: Notification
|
||||
private var notificationBuilder: NotificationCompat.Builder
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
class DownloadNotificationManager(id: Int, private val context: Context, viewThemeUtils: ViewThemeUtils) :
|
||||
WorkerNotificationManager(id, context, viewThemeUtils, R.string.downloader_download_in_progress_ticker) {
|
||||
|
||||
private var lastPercent = -1
|
||||
|
||||
init {
|
||||
notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
|
||||
setContentTitle(context.getString(R.string.downloader_download_in_progress_ticker))
|
||||
setTicker(context.getString(R.string.downloader_download_in_progress_ticker))
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
|
||||
}
|
||||
notificationBuilder.apply {
|
||||
setSound(null)
|
||||
setVibrate(null)
|
||||
setOnlyAlertOnce(true)
|
||||
setSilent(true)
|
||||
}
|
||||
|
||||
notification = notificationBuilder.build()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun prepareForStart(operation: DownloadFileOperation) {
|
||||
notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setOngoing(true)
|
||||
currentOperationTitle = File(operation.savePath).name
|
||||
|
||||
notificationBuilder.run {
|
||||
setContentTitle(currentOperationTitle)
|
||||
setOngoing(false)
|
||||
setProgress(100, 0, operation.size < 0)
|
||||
setContentText(
|
||||
String.format(
|
||||
context.getString(R.string.downloader_download_in_progress), 0,
|
||||
File(operation.savePath).name
|
||||
)
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
|
||||
}
|
||||
|
||||
notificationManager.notify(
|
||||
id,
|
||||
this.build()
|
||||
)
|
||||
}
|
||||
|
||||
showNotification()
|
||||
}
|
||||
|
||||
fun prepareForResult() {
|
||||
|
|
@ -82,23 +54,21 @@ class DownloadNotificationManager(
|
|||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun updateDownloadProgress(filePath: String, percent: Int, totalToTransfer: Long) {
|
||||
notificationBuilder.run {
|
||||
setProgress(100, percent, totalToTransfer < 0)
|
||||
val fileName: String = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1)
|
||||
val text =
|
||||
String.format(context.getString(R.string.downloader_download_in_progress), percent, fileName)
|
||||
val title =
|
||||
context.getString(R.string.downloader_download_in_progress_ticker)
|
||||
updateNotificationText(title, text)
|
||||
fun updateDownloadProgress(percent: Int, totalToTransfer: Long) {
|
||||
// If downloads are so fast, no need to notify again.
|
||||
if (percent == lastPercent) {
|
||||
return
|
||||
}
|
||||
lastPercent = percent
|
||||
|
||||
val progressText = NumberFormatter.getPercentageText(percent)
|
||||
setProgress(percent, progressText, totalToTransfer < 0)
|
||||
showNotification()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun dismissNotification() {
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
notificationManager.cancel(id)
|
||||
}, 2000)
|
||||
dismissNotification(2000)
|
||||
}
|
||||
|
||||
fun showNewNotification(text: String) {
|
||||
|
|
@ -106,24 +76,12 @@ class DownloadNotificationManager(
|
|||
|
||||
notificationBuilder.run {
|
||||
setProgress(0, 0, false)
|
||||
setContentTitle(null)
|
||||
setContentText(text)
|
||||
setContentTitle(text)
|
||||
setOngoing(false)
|
||||
notificationManager.notify(notifyId, this.build())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotificationText(title: String?, text: String) {
|
||||
notificationBuilder.run {
|
||||
title?.let {
|
||||
setContentTitle(title)
|
||||
}
|
||||
|
||||
setContentText(text)
|
||||
notificationManager.notify(id, this.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setContentIntent(intent: Intent, flag: Int) {
|
||||
notificationBuilder.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
|
|
@ -134,12 +92,4 @@ class DownloadNotificationManager(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun getId(): Int {
|
||||
return id
|
||||
}
|
||||
|
||||
fun getNotification(): Notification {
|
||||
return notificationBuilder.build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
|
|
@ -45,9 +45,7 @@ class DownloadTask(
|
|||
private val clientProvider: () -> OwnCloudClient,
|
||||
private val contentResolver: ContentResolver
|
||||
) {
|
||||
fun create(): DownloadTask {
|
||||
return DownloadTask(context, contentResolver, clientProvider)
|
||||
}
|
||||
fun create(): DownloadTask = DownloadTask(context, contentResolver, clientProvider)
|
||||
}
|
||||
|
||||
// Unused progress, isCancelled arguments needed for TransferManagerTest
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
enum class FileDownloadError {
|
||||
Failed, Cancelled
|
||||
Failed,
|
||||
Cancelled
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
|
|
@ -30,10 +30,8 @@ class FileDownloadHelper {
|
|||
companion object {
|
||||
private var instance: FileDownloadHelper? = null
|
||||
|
||||
fun instance(): FileDownloadHelper {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: FileDownloadHelper().also { instance = it }
|
||||
}
|
||||
fun instance(): FileDownloadHelper = instance ?: synchronized(this) {
|
||||
instance ?: FileDownloadHelper().also { instance = it }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,11 +48,13 @@ class FileDownloadHelper {
|
|||
val topParentId = fileStorageManager.getTopParentId(file)
|
||||
|
||||
val isJobScheduled = backgroundJobManager.isStartFileDownloadJobScheduled(user, file.fileId)
|
||||
return isJobScheduled || if (file.isFolder) {
|
||||
backgroundJobManager.isStartFileDownloadJobScheduled(user, topParentId)
|
||||
} else {
|
||||
FileDownloadWorker.isDownloading(user.accountName, file.fileId)
|
||||
}
|
||||
return isJobScheduled ||
|
||||
if (file.isFolder) {
|
||||
FileDownloadWorker.isDownloadingFolder(file.fileId) ||
|
||||
backgroundJobManager.isStartFileDownloadJobScheduled(user, topParentId)
|
||||
} else {
|
||||
FileDownloadWorker.isDownloading(user.accountName, file.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelPendingOrCurrentDownloads(user: User?, files: List<OCFile>?) {
|
||||
|
|
@ -81,11 +81,7 @@ class FileDownloadHelper {
|
|||
backgroundJobManager.cancelFilesDownloadJob(currentUser, currentFile.fileId)
|
||||
}
|
||||
|
||||
fun saveFile(
|
||||
file: OCFile,
|
||||
currentDownload: DownloadFileOperation?,
|
||||
storageManager: FileDataStorageManager?
|
||||
) {
|
||||
fun saveFile(file: OCFile, currentDownload: DownloadFileOperation?, storageManager: FileDataStorageManager?) {
|
||||
val syncDate = System.currentTimeMillis()
|
||||
|
||||
file.apply {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
|
|
@ -22,63 +22,53 @@ import com.owncloud.android.ui.preview.PreviewImageFragment
|
|||
|
||||
class FileDownloadIntents(private val context: Context) {
|
||||
|
||||
fun newDownloadIntent(
|
||||
download: DownloadFileOperation,
|
||||
linkedToRemotePath: String
|
||||
): Intent {
|
||||
return Intent(FileDownloadWorker.getDownloadAddedMessage()).apply {
|
||||
fun newDownloadIntent(download: DownloadFileOperation, linkedToRemotePath: String): Intent =
|
||||
Intent(FileDownloadWorker.getDownloadAddedMessage()).apply {
|
||||
putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName)
|
||||
putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath)
|
||||
putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, linkedToRemotePath)
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadFinishedIntent(
|
||||
download: DownloadFileOperation,
|
||||
downloadResult: RemoteOperationResult<*>,
|
||||
unlinkedFromRemotePath: String?
|
||||
): Intent {
|
||||
return Intent(FileDownloadWorker.getDownloadFinishMessage()).apply {
|
||||
putExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess)
|
||||
putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName)
|
||||
putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath)
|
||||
putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour)
|
||||
putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName)
|
||||
putExtra(SendShareDialog.PACKAGE_NAME, download.packageName)
|
||||
if (unlinkedFromRemotePath != null) {
|
||||
putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
|
||||
}
|
||||
setPackage(context.packageName)
|
||||
): Intent = Intent(FileDownloadWorker.getDownloadFinishMessage()).apply {
|
||||
putExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess)
|
||||
putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName)
|
||||
putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath)
|
||||
putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour)
|
||||
putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName)
|
||||
putExtra(SendShareDialog.PACKAGE_NAME, download.packageName)
|
||||
if (unlinkedFromRemotePath != null) {
|
||||
putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
|
||||
}
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
|
||||
fun credentialContentIntent(user: User): Intent {
|
||||
return Intent(context, AuthenticatorActivity::class.java).apply {
|
||||
putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount())
|
||||
putExtra(
|
||||
AuthenticatorActivity.EXTRA_ACTION,
|
||||
AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
|
||||
)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
addFlags(Intent.FLAG_FROM_BACKGROUND)
|
||||
}
|
||||
fun credentialContentIntent(user: User): Intent = Intent(context, AuthenticatorActivity::class.java).apply {
|
||||
putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount())
|
||||
putExtra(
|
||||
AuthenticatorActivity.EXTRA_ACTION,
|
||||
AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
|
||||
)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
addFlags(Intent.FLAG_FROM_BACKGROUND)
|
||||
}
|
||||
|
||||
fun detailsIntent(operation: DownloadFileOperation?): Intent {
|
||||
return if (operation != null) {
|
||||
if (PreviewImageFragment.canBePreviewed(operation.file)) {
|
||||
Intent(context, PreviewImageActivity::class.java)
|
||||
} else {
|
||||
Intent(context, FileDisplayActivity::class.java)
|
||||
}.apply {
|
||||
putExtra(FileActivity.EXTRA_FILE, operation.file)
|
||||
putExtra(FileActivity.EXTRA_USER, operation.user)
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
fun detailsIntent(operation: DownloadFileOperation?): Intent = if (operation != null) {
|
||||
if (PreviewImageFragment.canBePreviewed(operation.file)) {
|
||||
Intent(context, PreviewImageActivity::class.java)
|
||||
} else {
|
||||
Intent()
|
||||
Intent(context, FileDisplayActivity::class.java)
|
||||
}.apply {
|
||||
putExtra(FileActivity.EXTRA_FILE, operation.file)
|
||||
putExtra(FileActivity.EXTRA_USER, operation.user)
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
} else {
|
||||
Intent()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
|
|
@ -16,13 +16,16 @@ import android.util.Pair
|
|||
import androidx.core.util.component1
|
||||
import androidx.core.util.component2
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.User
|
||||
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
|
||||
import com.owncloud.android.datamodel.ForegroundServiceType
|
||||
|
|
@ -36,11 +39,14 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo
|
|||
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.events.EventBusFactory
|
||||
import com.owncloud.android.ui.events.FileDownloadProgressEvent
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import java.security.SecureRandom
|
||||
import java.util.AbstractList
|
||||
import java.util.Optional
|
||||
import java.util.Vector
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.random.Random
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
class FileDownloadWorker(
|
||||
|
|
@ -49,12 +55,15 @@ class FileDownloadWorker(
|
|||
private var localBroadcastManager: LocalBroadcastManager,
|
||||
private val context: Context,
|
||||
params: WorkerParameters
|
||||
) : Worker(context, params), OnAccountsUpdateListener, OnDatatransferProgressListener {
|
||||
) : CoroutineWorker(context, params),
|
||||
OnAccountsUpdateListener,
|
||||
OnDatatransferProgressListener {
|
||||
|
||||
companion object {
|
||||
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 {
|
||||
|
|
@ -62,10 +71,12 @@ class FileDownloadWorker(
|
|||
}
|
||||
}
|
||||
|
||||
fun isDownloading(accountName: String, fileId: Long): Boolean {
|
||||
return pendingDownloads.all.any { it.value?.payload?.isMatching(accountName, fileId) == true }
|
||||
fun isDownloading(accountName: String, fileId: Long): Boolean = pendingDownloads.all.any {
|
||||
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"
|
||||
|
|
@ -73,19 +84,14 @@ class FileDownloadWorker(
|
|||
const val ACTIVITY_NAME = "ACTIVITY_NAME"
|
||||
const val PACKAGE_NAME = "PACKAGE_NAME"
|
||||
const val CONFLICT_UPLOAD_ID = "CONFLICT_UPLOAD_ID"
|
||||
|
||||
const val EXTRA_DOWNLOAD_RESULT = "EXTRA_DOWNLOAD_RESULT"
|
||||
const val EXTRA_REMOTE_PATH = "EXTRA_REMOTE_PATH"
|
||||
const val EXTRA_LINKED_TO_PATH = "EXTRA_LINKED_TO_PATH"
|
||||
const val EXTRA_ACCOUNT_NAME = "EXTRA_ACCOUNT_NAME"
|
||||
|
||||
fun getDownloadAddedMessage(): String {
|
||||
return FileDownloadWorker::class.java.name + "DOWNLOAD_ADDED"
|
||||
}
|
||||
fun getDownloadAddedMessage(): String = FileDownloadWorker::class.java.name + "DOWNLOAD_ADDED"
|
||||
|
||||
fun getDownloadFinishMessage(): String {
|
||||
return FileDownloadWorker::class.java.name + "DOWNLOAD_FINISH"
|
||||
}
|
||||
fun getDownloadFinishMessage(): String = FileDownloadWorker::class.java.name + "DOWNLOAD_FINISH"
|
||||
}
|
||||
|
||||
private var currentDownload: DownloadFileOperation? = null
|
||||
|
|
@ -95,7 +101,7 @@ class FileDownloadWorker(
|
|||
|
||||
private val intents = FileDownloadIntents(context)
|
||||
private var notificationManager = DownloadNotificationManager(
|
||||
SecureRandom().nextInt(),
|
||||
Random.nextInt(),
|
||||
context,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
|
@ -110,18 +116,17 @@ class FileDownloadWorker(
|
|||
|
||||
private var downloadError: FileDownloadError? = null
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun doWork(): Result {
|
||||
return try {
|
||||
val requestDownloads = getRequestDownloads()
|
||||
addAccountUpdateListener()
|
||||
@Suppress("TooGenericExceptionCaught", "ReturnCount")
|
||||
override suspend fun doWork(): Result {
|
||||
val foregroundInfo = createWorkerForegroundInfo()
|
||||
setForeground(foregroundInfo)
|
||||
|
||||
val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
|
||||
notificationManager.getId(),
|
||||
notificationManager.getNotification(),
|
||||
ForegroundServiceType.DataSync
|
||||
)
|
||||
setForegroundAsync(foregroundInfo)
|
||||
return try {
|
||||
setUser()
|
||||
val remotePath = inputData.keyValueMap[FILE_REMOTE_PATH] as String? ?: return Result.failure()
|
||||
val ocFile = fileDataStorageManager?.getFileByEncryptedRemotePath(remotePath) ?: return Result.failure()
|
||||
val requestDownloads = getRequestDownloads(ocFile)
|
||||
addAccountUpdateListener()
|
||||
|
||||
requestDownloads.forEach {
|
||||
downloadFile(it)
|
||||
|
|
@ -132,43 +137,43 @@ class FileDownloadWorker(
|
|||
notificationManager.dismissNotification()
|
||||
}
|
||||
|
||||
setIdleWorkerState()
|
||||
|
||||
Log_OC.e(TAG, "FilesDownloadWorker successfully completed")
|
||||
Result.success()
|
||||
} catch (t: Throwable) {
|
||||
notificationManager.dismissNotification()
|
||||
notificationManager.showNewNotification(context.getString(R.string.downloader_unexpected_error))
|
||||
Log_OC.e(TAG, "Error caught at FilesDownloadWorker(): " + t.localizedMessage)
|
||||
setIdleWorkerState()
|
||||
Result.failure()
|
||||
} finally {
|
||||
Log_OC.e(TAG, "FilesDownloadWorker cleanup")
|
||||
notificationManager.dismissNotification()
|
||||
setIdleWorkerState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
Log_OC.e(TAG, "FilesDownloadWorker stopped")
|
||||
|
||||
notificationManager.dismissNotification()
|
||||
setIdleWorkerState()
|
||||
|
||||
super.onStopped()
|
||||
}
|
||||
private fun createWorkerForegroundInfo(): ForegroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
|
||||
notificationManager.getId(),
|
||||
notificationManager.getNotification(),
|
||||
ForegroundServiceType.DataSync
|
||||
)
|
||||
|
||||
private fun setWorkerState(user: User?) {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Download(user, currentDownload))
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.DownloadStarted(user, currentDownload))
|
||||
}
|
||||
|
||||
private fun setIdleWorkerState() {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle)
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.DownloadFinished(getCurrentFile()))
|
||||
}
|
||||
|
||||
private fun removePendingDownload(accountName: String?) {
|
||||
pendingDownloads.remove(accountName)
|
||||
}
|
||||
|
||||
private fun getRequestDownloads(): AbstractList<String> {
|
||||
setUser()
|
||||
val files = getFiles()
|
||||
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?
|
||||
|
|
@ -221,15 +226,10 @@ class FileDownloadWorker(
|
|||
fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
|
||||
}
|
||||
|
||||
private fun getFiles(): List<OCFile> {
|
||||
val remotePath = inputData.keyValueMap[FILE_REMOTE_PATH] as String?
|
||||
val file = fileDataStorageManager?.getFileByEncryptedRemotePath(remotePath) ?: return listOf()
|
||||
|
||||
return if (file.isFolder) {
|
||||
fileDataStorageManager?.getAllFilesRecursivelyInsideFolder(file) ?: listOf()
|
||||
} else {
|
||||
listOf(file)
|
||||
}
|
||||
private fun getFiles(file: OCFile): List<OCFile> = if (file.isFolder) {
|
||||
fileDataStorageManager?.getAllFilesRecursivelyInsideFolder(file) ?: listOf()
|
||||
} else {
|
||||
listOf(file)
|
||||
}
|
||||
|
||||
private fun getDownloadType(): DownloadType? {
|
||||
|
|
@ -267,7 +267,12 @@ class FileDownloadWorker(
|
|||
return
|
||||
}
|
||||
|
||||
notifyDownloadStart(currentDownload!!)
|
||||
lastPercent = 0
|
||||
notificationManager.run {
|
||||
prepareForStart(currentDownload!!)
|
||||
setContentIntent(intents.detailsIntent(currentDownload!!), PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
var downloadResult: RemoteOperationResult<*>? = null
|
||||
try {
|
||||
val ocAccount = getOCAccountForDownload()
|
||||
|
|
@ -288,15 +293,6 @@ class FileDownloadWorker(
|
|||
}
|
||||
}
|
||||
|
||||
private fun notifyDownloadStart(download: DownloadFileOperation) {
|
||||
lastPercent = 0
|
||||
|
||||
notificationManager.run {
|
||||
prepareForStart(download)
|
||||
setContentIntent(intents.detailsIntent(download), PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getOCAccountForDownload(): OwnCloudAccount {
|
||||
val currentDownloadAccount = currentDownload?.user?.toPlatformAccount()
|
||||
|
|
@ -350,6 +346,7 @@ class FileDownloadWorker(
|
|||
|
||||
private fun checkDownloadError(result: RemoteOperationResult<*>) {
|
||||
if (result.isSuccess || downloadError != null) {
|
||||
notificationManager.dismissNotification()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -365,6 +362,7 @@ class FileDownloadWorker(
|
|||
FileDownloadError.Cancelled -> {
|
||||
context.getString(R.string.downloader_file_download_cancelled)
|
||||
}
|
||||
|
||||
FileDownloadError.Failed -> {
|
||||
context.getString(R.string.downloader_file_download_failed)
|
||||
}
|
||||
|
|
@ -373,10 +371,7 @@ class FileDownloadWorker(
|
|||
notificationManager.showNewNotification(text)
|
||||
}
|
||||
|
||||
private fun notifyDownloadResult(
|
||||
download: DownloadFileOperation,
|
||||
downloadResult: RemoteOperationResult<*>
|
||||
) {
|
||||
private fun notifyDownloadResult(download: DownloadFileOperation, downloadResult: RemoteOperationResult<*>) {
|
||||
if (downloadResult.isCancelled) {
|
||||
return
|
||||
}
|
||||
|
|
@ -404,6 +399,10 @@ class FileDownloadWorker(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private val minProgressUpdateInterval = 750
|
||||
private var lastUpdateTime = 0L
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onTransferProgress(
|
||||
progressRate: Long,
|
||||
|
|
@ -411,23 +410,25 @@ class FileDownloadWorker(
|
|||
totalToTransfer: Long,
|
||||
filePath: String
|
||||
) {
|
||||
val percent: Int = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt()
|
||||
val percent: Int = downloadProgressListener.getPercent(totalTransferredSoFar, totalToTransfer)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
if (percent != lastPercent) {
|
||||
if (percent != lastPercent && (currentTime - lastUpdateTime) >= minProgressUpdateInterval) {
|
||||
notificationManager.run {
|
||||
updateDownloadProgress(filePath, percent, totalToTransfer)
|
||||
updateDownloadProgress(percent, totalToTransfer)
|
||||
}
|
||||
lastUpdateTime = currentTime
|
||||
}
|
||||
|
||||
lastPercent = percent
|
||||
EventBusFactory.downloadProgressEventBus.post(FileDownloadProgressEvent(percent))
|
||||
}
|
||||
|
||||
// CHECK: Is this class still needed after conversion from Foreground Services to Worker?
|
||||
inner class FileDownloadProgressListener : OnDatatransferProgressListener {
|
||||
private val boundListeners: MutableMap<Long, OnDatatransferProgressListener> = HashMap()
|
||||
|
||||
fun isDownloading(user: User?, file: OCFile?): Boolean {
|
||||
return FileDownloadHelper.instance().isDownloading(user, file)
|
||||
}
|
||||
fun isDownloading(user: User?, file: OCFile?): Boolean = FileDownloadHelper.instance().isDownloading(user, file)
|
||||
|
||||
fun addDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
|
||||
if (file == null || listener == null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.utils.extensions.getNonEncryptedSubfolders
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.RefreshFolderOperation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MetadataWorker(private val context: Context, params: WorkerParameters, private val user: User) :
|
||||
CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MetadataWorker"
|
||||
const val FILE_PATH = "file_path"
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "ReturnCount")
|
||||
override suspend fun doWork(): Result {
|
||||
val storageManager = FileDataStorageManager(user, context.contentResolver)
|
||||
val filePath = inputData.getString(FILE_PATH)
|
||||
if (filePath == null) {
|
||||
Log_OC.e(TAG, "❌ Invalid folder path. Aborting metadata sync. $filePath")
|
||||
return Result.failure()
|
||||
}
|
||||
val currentDir = storageManager.getFileByDecryptedRemotePath(filePath)
|
||||
if (currentDir == null) {
|
||||
Log_OC.e(TAG, "❌ Current directory is null. Aborting metadata sync. $filePath")
|
||||
return Result.failure()
|
||||
}
|
||||
Log_OC.d(TAG, "🕒 Starting metadata sync for folder: $filePath")
|
||||
|
||||
// first check current dir
|
||||
refreshFolder(currentDir, storageManager)
|
||||
|
||||
// then get up-to-date subfolders
|
||||
val subfolders = storageManager.getNonEncryptedSubfolders(currentDir.fileId, user.accountName)
|
||||
subfolders.forEach { subFolder ->
|
||||
refreshFolder(subFolder, storageManager)
|
||||
}
|
||||
|
||||
Log_OC.d(TAG, "🏁 Metadata sync completed for folder: $filePath")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager) =
|
||||
withContext(Dispatchers.IO) {
|
||||
Log_OC.d(
|
||||
TAG,
|
||||
"📂 eTag check\n" +
|
||||
" Path: " + folder.remotePath + "\n" +
|
||||
" eTag: " + folder.etag + "\n" +
|
||||
" eTagOnServer: " + folder.etagOnServer
|
||||
)
|
||||
if (!folder.isEtagChanged) {
|
||||
Log_OC.d(TAG, "Skipping ${folder.remotePath}, eTag didn't change")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
Log_OC.d(TAG, "⏳ Fetching metadata for: ${folder.remotePath}")
|
||||
|
||||
val operation = RefreshFolderOperation(folder, storageManager, user, context)
|
||||
val result = operation.execute(user, context)
|
||||
if (result.isSuccess) {
|
||||
Log_OC.d(TAG, "✅ Successfully fetched metadata for: ${folder.remotePath}")
|
||||
} else {
|
||||
Log_OC.e(TAG, "❌ Failed to fetch metadata for: ${folder.remotePath}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.notification
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
|
||||
open class WorkerNotificationManager(
|
||||
private val id: Int,
|
||||
private val context: Context,
|
||||
viewThemeUtils: ViewThemeUtils,
|
||||
private val tickerId: Int,
|
||||
private val channelId: String = NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS
|
||||
) {
|
||||
var currentOperationTitle: String? = null
|
||||
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
var notificationBuilder: NotificationCompat.Builder =
|
||||
NotificationUtils.newNotificationBuilder(
|
||||
context,
|
||||
channelId,
|
||||
viewThemeUtils
|
||||
).apply {
|
||||
setTicker(context.getString(tickerId))
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
|
||||
setStyle(NotificationCompat.BigTextStyle())
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
|
||||
fun showNotification() {
|
||||
notificationManager.notify(id, notificationBuilder.build())
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun setProgress(percent: Int, progressText: String?, indeterminate: Boolean) {
|
||||
notificationBuilder.run {
|
||||
setProgress(100, percent, indeterminate)
|
||||
setContentTitle(currentOperationTitle)
|
||||
|
||||
progressText?.let {
|
||||
setContentText(progressText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissNotification(delay: Long = 0) {
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
notificationManager.cancel(id)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
fun getId(): Int = id
|
||||
|
||||
fun getNotification(): Notification = notificationBuilder.build()
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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.offlineOperations
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.client.jobs.notification.WorkerNotificationManager
|
||||
import com.nextcloud.client.jobs.offlineOperations.receiver.OfflineOperationReceiver
|
||||
import com.nextcloud.utils.extensions.getErrorMessage
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.ui.activity.ConflictsResolveActivity
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
|
||||
class OfflineOperationsNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) :
|
||||
WorkerNotificationManager(
|
||||
ID,
|
||||
context,
|
||||
viewThemeUtils,
|
||||
tickerId = R.string.offline_operations_worker_notification_manager_ticker,
|
||||
channelId = NotificationUtils.NOTIFICATION_CHANNEL_OFFLINE_OPERATIONS
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val ID = 121
|
||||
const val ERROR_ID = 122
|
||||
|
||||
private const val ONE_HUNDRED_PERCENT = 100
|
||||
}
|
||||
|
||||
init {
|
||||
notificationBuilder.apply {
|
||||
setSound(null)
|
||||
setVibrate(null)
|
||||
setOnlyAlertOnce(true)
|
||||
setSilent(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
notificationBuilder.run {
|
||||
setContentTitle(context.getString(R.string.offline_operations_worker_notification_start_text))
|
||||
setProgress(ONE_HUNDRED_PERCENT, 0, false)
|
||||
}
|
||||
|
||||
showNotification()
|
||||
}
|
||||
|
||||
fun update(totalOperationSize: Int, currentOperationIndex: Int, filename: String) {
|
||||
val title = if (totalOperationSize > 1) {
|
||||
String.format(
|
||||
context.getString(R.string.offline_operations_worker_progress_text),
|
||||
currentOperationIndex,
|
||||
totalOperationSize,
|
||||
filename
|
||||
)
|
||||
} else {
|
||||
filename
|
||||
}
|
||||
|
||||
val progress = (currentOperationIndex * ONE_HUNDRED_PERCENT) / totalOperationSize
|
||||
|
||||
notificationBuilder.run {
|
||||
setContentTitle(title)
|
||||
setProgress(ONE_HUNDRED_PERCENT, progress, false)
|
||||
}
|
||||
|
||||
showNotification()
|
||||
}
|
||||
|
||||
fun showNewNotification(id: Int?, result: RemoteOperationResult<*>, operation: RemoteOperation<*>) {
|
||||
val reason = (result to operation).getErrorMessage()
|
||||
val text = context.getString(R.string.offline_operations_worker_notification_error_text, reason)
|
||||
val cancelOfflineOperationAction = id?.let { getCancelOfflineOperationAction(it) }
|
||||
|
||||
notificationBuilder.run {
|
||||
cancelOfflineOperationAction?.let {
|
||||
addAction(it)
|
||||
}
|
||||
setContentTitle(text)
|
||||
setOngoing(false)
|
||||
setProgress(0, 0, false)
|
||||
notificationManager.notify(ERROR_ID, this.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun showConflictNotificationForDeleteOrRemoveOperation(entity: OfflineOperationEntity?) {
|
||||
val id = entity?.id
|
||||
if (id == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val title = entity.getConflictText(context)
|
||||
|
||||
notificationBuilder
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.clearActions()
|
||||
.setContentTitle(title)
|
||||
|
||||
notificationManager.notify(id, notificationBuilder.build())
|
||||
}
|
||||
|
||||
fun showConflictResolveNotification(file: OCFile, entity: OfflineOperationEntity?) {
|
||||
val path = entity?.path
|
||||
val id = entity?.id
|
||||
|
||||
if (path == null || id == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val resolveConflictAction = getResolveConflictAction(file, id, path)
|
||||
|
||||
val title = entity.getConflictText(context)
|
||||
|
||||
notificationBuilder
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.clearActions()
|
||||
.setContentTitle(title)
|
||||
.setContentIntent(resolveConflictAction.actionIntent)
|
||||
.addAction(resolveConflictAction)
|
||||
|
||||
notificationManager.notify(id, notificationBuilder.build())
|
||||
}
|
||||
|
||||
private fun getResolveConflictAction(file: OCFile, id: Int, path: String): NotificationCompat.Action {
|
||||
val intent = ConflictsResolveActivity.createIntent(file, path, context)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
id,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return NotificationCompat.Action(
|
||||
R.drawable.ic_cloud_upload,
|
||||
context.getString(R.string.upload_list_resolve_conflict),
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCancelOfflineOperationAction(id: Int): NotificationCompat.Action {
|
||||
val intent = Intent(context, OfflineOperationReceiver::class.java).apply {
|
||||
putExtra(OfflineOperationReceiver.ID, id)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
id,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return NotificationCompat.Action(
|
||||
R.drawable.ic_delete,
|
||||
context.getString(R.string.common_cancel),
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
|
||||
fun dismissNotification(id: Int?) {
|
||||
if (id == null) return
|
||||
notificationManager.cancel(id)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* 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.offlineOperations
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository
|
||||
import com.nextcloud.client.network.ClientFactoryImpl
|
||||
import com.nextcloud.client.network.ConnectivityService
|
||||
import com.nextcloud.model.OfflineOperationType
|
||||
import com.nextcloud.model.WorkerState
|
||||
import com.nextcloud.model.WorkerStateLiveData
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
|
||||
import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation
|
||||
import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation
|
||||
import com.owncloud.android.lib.resources.files.model.RemoteFile
|
||||
import com.owncloud.android.operations.CreateFolderOperation
|
||||
import com.owncloud.android.operations.RemoveFileOperation
|
||||
import com.owncloud.android.operations.RenameFileOperation
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
private typealias OfflineOperationResult = Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>?
|
||||
|
||||
class OfflineOperationsWorker(
|
||||
private val user: User,
|
||||
private val context: Context,
|
||||
private val connectivityService: ConnectivityService,
|
||||
viewThemeUtils: ViewThemeUtils,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
private val TAG = OfflineOperationsWorker::class.java.simpleName
|
||||
const val JOB_NAME = "JOB_NAME"
|
||||
|
||||
private const val ONE_SECOND = 1000L
|
||||
}
|
||||
|
||||
private val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
|
||||
private val clientFactory = ClientFactoryImpl(context)
|
||||
private val notificationManager = OfflineOperationsNotificationManager(context, viewThemeUtils)
|
||||
private var repository = OfflineOperationsRepository(fileDataStorageManager)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jobName = inputData.getString(JOB_NAME)
|
||||
Log_OC.d(TAG, "[$jobName] OfflineOperationsWorker started for user: ${user.accountName}")
|
||||
|
||||
// check network connection
|
||||
if (!isNetworkAndServerAvailable()) {
|
||||
Log_OC.w(TAG, "⚠️ No internet/server connection. Retrying later...")
|
||||
return@withContext Result.retry()
|
||||
}
|
||||
|
||||
// check offline operations
|
||||
val operations = fileDataStorageManager.offlineOperationDao.getAll()
|
||||
if (operations.isEmpty()) {
|
||||
Log_OC.d(TAG, "Skipping, no offline operation found")
|
||||
return@withContext Result.success()
|
||||
}
|
||||
|
||||
// process offline operations
|
||||
notificationManager.start()
|
||||
val client = clientFactory.create(user)
|
||||
processOperations(operations, client)
|
||||
|
||||
// finish
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.OfflineOperationsCompleted)
|
||||
Log_OC.d(TAG, "🏁 Worker finished with result")
|
||||
return@withContext Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "💥 ProcessOperations failed: ${e.message}")
|
||||
return@withContext Result.failure()
|
||||
} finally {
|
||||
notificationManager.dismissNotification()
|
||||
}
|
||||
}
|
||||
|
||||
// region Handle offline operations
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private suspend fun processOperations(operations: List<OfflineOperationEntity>, client: OwnCloudClient) {
|
||||
val totalOperationSize = operations.size
|
||||
operations.forEachIndexed { index, operation ->
|
||||
try {
|
||||
Log_OC.d(TAG, "Processing operation, path: ${operation.path}")
|
||||
val result = executeOperation(operation, client)
|
||||
handleResult(operation, totalOperationSize, index, result)
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "💥 Exception while processing operation id=${operation.id}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResult(
|
||||
operation: OfflineOperationEntity,
|
||||
totalOperations: Int,
|
||||
currentSuccessfulOperationIndex: Int,
|
||||
result: OfflineOperationResult
|
||||
) {
|
||||
val operationResult = result?.first ?: return
|
||||
val logMessage = if (operationResult.isSuccess) "Operation completed" else "Operation failed"
|
||||
Log_OC.d(TAG, "$logMessage filename: ${operation.filename}, type: ${operation.type}")
|
||||
|
||||
return if (result.first?.isSuccess == true) {
|
||||
handleSuccessResult(operation, totalOperations, currentSuccessfulOperationIndex)
|
||||
} else {
|
||||
handleErrorResult(operation.id, result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccessResult(
|
||||
operation: OfflineOperationEntity,
|
||||
totalOperations: Int,
|
||||
currentSuccessfulOperationIndex: Int
|
||||
) {
|
||||
if (operation.type is OfflineOperationType.RemoveFile) {
|
||||
val operationType = operation.type as OfflineOperationType.RemoveFile
|
||||
fileDataStorageManager.getFileByDecryptedRemotePath(operationType.path)?.let { ocFile ->
|
||||
repository.deleteOperation(ocFile)
|
||||
}
|
||||
} else {
|
||||
repository.updateNextOperations(operation)
|
||||
}
|
||||
|
||||
fileDataStorageManager.offlineOperationDao.delete(operation)
|
||||
notificationManager.update(totalOperations, currentSuccessfulOperationIndex + 1, operation.filename ?: "")
|
||||
}
|
||||
|
||||
private fun handleErrorResult(id: Int?, result: OfflineOperationResult) {
|
||||
val operationResult = result?.first ?: return
|
||||
val operation = result.second ?: return
|
||||
Log_OC.e(TAG, "❌ Operation failed [id=$id]: code=${operationResult.code}, message=${operationResult.message}")
|
||||
val excludedErrorCodes =
|
||||
listOf(RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS, RemoteOperationResult.ResultCode.LOCKED)
|
||||
|
||||
if (!excludedErrorCodes.contains(operationResult.code)) {
|
||||
notificationManager.showNewNotification(id, operationResult, operation)
|
||||
} else {
|
||||
Log_OC.d(TAG, "ℹ️ Ignored error: ${operationResult.code}")
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
private suspend fun isNetworkAndServerAvailable(): Boolean = suspendCoroutine { continuation ->
|
||||
connectivityService.isNetworkAndServerAvailable { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
// region Operation Execution
|
||||
@Suppress("ComplexCondition", "LongMethod")
|
||||
private suspend fun executeOperation(
|
||||
operation: OfflineOperationEntity,
|
||||
client: OwnCloudClient
|
||||
): OfflineOperationResult? = withContext(Dispatchers.IO) {
|
||||
var path = (operation.path)
|
||||
if (path == null) {
|
||||
Log_OC.w(TAG, "⚠️ Skipped: path is null for operation id=${operation.id}")
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
if (operation.type is OfflineOperationType.CreateFile && path.endsWith(OCFile.PATH_SEPARATOR)) {
|
||||
Log_OC.w(
|
||||
TAG,
|
||||
"Create file operation should not ends with path separator removing suffix, " +
|
||||
"operation id=${operation.id}"
|
||||
)
|
||||
path = path.removeSuffix(OCFile.PATH_SEPARATOR)
|
||||
}
|
||||
|
||||
val remoteFile = getRemoteFile(path)
|
||||
val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(path)
|
||||
|
||||
if (remoteFile != null && ocFile != null && isFileChanged(remoteFile, ocFile)) {
|
||||
Log_OC.w(TAG, "⚠️ Conflict detected: File already exists on server. Skipping operation id=${operation.id}")
|
||||
|
||||
if (operation.isRenameOrRemove()) {
|
||||
Log_OC.d(TAG, "🗑 Removing conflicting rename/remove operation id=${operation.id}")
|
||||
fileDataStorageManager.offlineOperationDao.delete(operation)
|
||||
notificationManager.showConflictNotificationForDeleteOrRemoveOperation(operation)
|
||||
} else {
|
||||
Log_OC.d(TAG, "📌 Showing conflict resolution for operation id=${operation.id}")
|
||||
notificationManager.showConflictResolveNotification(ocFile, operation)
|
||||
}
|
||||
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
if (operation.isRenameOrRemove() && ocFile == null) {
|
||||
Log_OC.d(TAG, "Skipping, attempting to delete or rename non-existing file")
|
||||
fileDataStorageManager.offlineOperationDao.delete(operation)
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
if (operation.isCreate() && remoteFile != null && ocFile != null && !isFileChanged(remoteFile, ocFile)) {
|
||||
Log_OC.d(TAG, "Skipping, attempting to create same file creation")
|
||||
fileDataStorageManager.offlineOperationDao.delete(operation)
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
return@withContext when (val type = operation.type) {
|
||||
is OfflineOperationType.CreateFolder -> {
|
||||
Log_OC.d(TAG, "📂 Creating folder at ${type.path}")
|
||||
createFolder(operation, client)
|
||||
}
|
||||
is OfflineOperationType.CreateFile -> {
|
||||
Log_OC.d(TAG, "📤 Uploading file: local=${type.localPath} → remote=${type.remotePath}")
|
||||
createFile(operation, client)
|
||||
}
|
||||
is OfflineOperationType.RenameFile -> {
|
||||
Log_OC.d(TAG, "✏️ Renaming ${operation.path} → ${type.newName}")
|
||||
renameFile(operation, client)
|
||||
}
|
||||
is OfflineOperationType.RemoveFile -> {
|
||||
Log_OC.d(TAG, "🗑 Removing file: ${operation.path}")
|
||||
ocFile?.let { removeFile(it, client) }
|
||||
}
|
||||
else -> {
|
||||
Log_OC.d(TAG, "⚠️ Unsupported operation type: $type")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun createFolder(operation: OfflineOperationEntity, client: OwnCloudClient): OfflineOperationResult {
|
||||
val operationType = (operation.type as OfflineOperationType.CreateFolder)
|
||||
val createFolderOperation = CreateFolderOperation(operationType.path, user, context, fileDataStorageManager)
|
||||
return createFolderOperation.execute(client) to createFolderOperation
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun createFile(operation: OfflineOperationEntity, client: OwnCloudClient): OfflineOperationResult {
|
||||
val operationType = (operation.type as OfflineOperationType.CreateFile)
|
||||
val lastModificationDate = System.currentTimeMillis() / ONE_SECOND
|
||||
val createFileOperation = UploadFileRemoteOperation(
|
||||
operationType.localPath,
|
||||
operationType.remotePath,
|
||||
operationType.mimeType,
|
||||
"",
|
||||
operation.modifiedAt ?: lastModificationDate,
|
||||
operation.createdAt ?: System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
return createFileOperation.execute(client) to createFileOperation
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun renameFile(operation: OfflineOperationEntity, client: OwnCloudClient): OfflineOperationResult {
|
||||
val operationType = (operation.type as OfflineOperationType.RenameFile)
|
||||
val renameFileOperation = RenameFileOperation(operation.path, operationType.newName, fileDataStorageManager)
|
||||
return renameFileOperation.execute(client) to renameFileOperation
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun removeFile(ocFile: OCFile, client: OwnCloudClient): OfflineOperationResult {
|
||||
val removeFileOperation = RemoveFileOperation(ocFile, false, user, true, context, fileDataStorageManager)
|
||||
return removeFileOperation.execute(client) to removeFileOperation
|
||||
}
|
||||
// endregion
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getRemoteFile(remotePath: String): RemoteFile? {
|
||||
val mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath)
|
||||
val isFolder = MimeTypeUtil.isFolder(mimeType)
|
||||
val client = ClientFactoryImpl(context).create(user)
|
||||
val result = if (isFolder) {
|
||||
ReadFolderRemoteOperation(remotePath).execute(client)
|
||||
} else {
|
||||
ReadFileRemoteOperation(remotePath).execute(client)
|
||||
}
|
||||
|
||||
return if (result.isSuccess) {
|
||||
result.data[0] as? RemoteFile
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFileChanged(remoteFile: RemoteFile, ocFile: OCFile): Boolean = remoteFile.etag != ocFile.etagOnServer
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.offlineOperations.receiver
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsNotificationManager
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class OfflineOperationReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
const val ID = "id"
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var storageManager: FileDataStorageManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
MainApp.getAppComponent().inject(this)
|
||||
|
||||
val id = intent.getIntExtra(ID, -1)
|
||||
if (id == -1) {
|
||||
return
|
||||
}
|
||||
|
||||
storageManager.offlineOperationDao.deleteById(id)
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(
|
||||
OfflineOperationsNotificationManager.ERROR_ID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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.offlineOperations.repository
|
||||
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.nextcloud.model.OfflineOperationType
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.utils.MimeType
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
|
||||
class OfflineOperationsRepository(private val fileDataStorageManager: FileDataStorageManager) :
|
||||
OfflineOperationsRepositoryType {
|
||||
|
||||
private val dao = fileDataStorageManager.offlineOperationDao
|
||||
private val pathSeparator = '/'
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
override fun getAllSubEntities(fileId: Long): List<OfflineOperationEntity> {
|
||||
val result = mutableListOf<OfflineOperationEntity>()
|
||||
val queue = ArrayDeque<Long>()
|
||||
queue.add(fileId)
|
||||
val processedIds = mutableSetOf<Long>()
|
||||
|
||||
while (queue.isNotEmpty()) {
|
||||
val currentFileId = queue.removeFirst()
|
||||
if (currentFileId in processedIds || currentFileId == 1L) continue
|
||||
|
||||
processedIds.add(currentFileId)
|
||||
|
||||
val subDirectories = dao.getSubEntitiesByParentOCFileId(currentFileId)
|
||||
result.addAll(subDirectories)
|
||||
|
||||
subDirectories.forEach {
|
||||
val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(it.path)
|
||||
ocFile?.fileId?.let { newFileId ->
|
||||
if (newFileId != 1L && newFileId !in processedIds) {
|
||||
queue.add(newFileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun deleteOperation(file: OCFile) {
|
||||
if (file.isFolder) {
|
||||
getAllSubEntities(file.fileId).forEach {
|
||||
dao.delete(it)
|
||||
}
|
||||
}
|
||||
|
||||
file.decryptedRemotePath?.let {
|
||||
dao.deleteByPath(it)
|
||||
}
|
||||
|
||||
fileDataStorageManager.removeFile(file, true, true)
|
||||
}
|
||||
|
||||
override fun updateNextOperations(operation: OfflineOperationEntity) {
|
||||
val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation.path)
|
||||
val fileId = ocFile?.fileId ?: return
|
||||
|
||||
getAllSubEntities(fileId)
|
||||
.mapNotNull { nextOperation ->
|
||||
nextOperation.parentOCFileId?.let { parentId ->
|
||||
fileDataStorageManager.getFileById(parentId)?.let { ocFile ->
|
||||
ocFile.decryptedRemotePath?.let { updatedPath ->
|
||||
val newPath = updatedPath + nextOperation.filename + pathSeparator
|
||||
|
||||
if (newPath != nextOperation.path) {
|
||||
nextOperation.apply {
|
||||
type = when (type) {
|
||||
is OfflineOperationType.CreateFile ->
|
||||
(type as OfflineOperationType.CreateFile).copy(
|
||||
remotePath = newPath
|
||||
)
|
||||
|
||||
is OfflineOperationType.CreateFolder ->
|
||||
(type as OfflineOperationType.CreateFolder).copy(
|
||||
path = newPath
|
||||
)
|
||||
|
||||
else -> type
|
||||
}
|
||||
path = newPath
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.forEach { dao.update(it) }
|
||||
}
|
||||
|
||||
override fun convertToOCFiles(fileId: Long): List<OCFile> =
|
||||
dao.getSubEntitiesByParentOCFileId(fileId).map { entity ->
|
||||
OCFile(entity.path).apply {
|
||||
mimeType = if (entity.type is OfflineOperationType.CreateFolder) {
|
||||
MimeType.DIRECTORY
|
||||
} else {
|
||||
MimeTypeUtil.getMimeTypeFromPath(entity.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.offlineOperations.repository
|
||||
|
||||
import com.nextcloud.client.database.entity.OfflineOperationEntity
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
|
||||
interface OfflineOperationsRepositoryType {
|
||||
fun getAllSubEntities(fileId: Long): List<OfflineOperationEntity>
|
||||
fun deleteOperation(file: OCFile)
|
||||
fun updateNextOperations(operation: OfflineOperationEntity)
|
||||
fun convertToOCFiles(fileId: Long): List<OCFile>
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.operation
|
||||
|
||||
import android.content.Context
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.utils.extensions.getErrorMessage
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.RemoveFileOperation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FileOperationHelper(
|
||||
private val user: User,
|
||||
private val context: Context,
|
||||
private val fileDataStorageManager: FileDataStorageManager
|
||||
) {
|
||||
companion object {
|
||||
private val TAG = FileOperationHelper::class.java.simpleName
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "Deprecation")
|
||||
suspend fun removeFile(
|
||||
file: OCFile,
|
||||
onlyLocalCopy: Boolean,
|
||||
inBackground: Boolean,
|
||||
client: OwnCloudClient
|
||||
): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val operation = async {
|
||||
RemoveFileOperation(
|
||||
file,
|
||||
onlyLocalCopy,
|
||||
user,
|
||||
inBackground,
|
||||
context,
|
||||
fileDataStorageManager
|
||||
)
|
||||
}
|
||||
val operationResult = operation.await()
|
||||
val result = operationResult.execute(client)
|
||||
|
||||
return@withContext if (result.isSuccess) {
|
||||
true
|
||||
} else {
|
||||
val reason = (result to operationResult).getErrorMessage()
|
||||
Log_OC.e(TAG, "Error occurred while removing file: $reason")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Error occurred while removing file: $e")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.transfer
|
||||
|
||||
|
|
@ -41,27 +41,23 @@ class FileTransferService : LifecycleService() {
|
|||
const val EXTRA_REQUEST = "request"
|
||||
const val EXTRA_USER = "user"
|
||||
|
||||
fun createBindIntent(context: Context, user: User): Intent {
|
||||
return Intent(context, FileTransferService::class.java).apply {
|
||||
fun createBindIntent(context: Context, user: User): Intent =
|
||||
Intent(context, FileTransferService::class.java).apply {
|
||||
putExtra(EXTRA_USER, user)
|
||||
}
|
||||
}
|
||||
|
||||
fun createTransferRequestIntent(context: Context, request: Request): Intent {
|
||||
return Intent(context, FileTransferService::class.java).apply {
|
||||
fun createTransferRequestIntent(context: Context, request: Request): Intent =
|
||||
Intent(context, FileTransferService::class.java).apply {
|
||||
action = ACTION_TRANSFER
|
||||
putExtra(EXTRA_REQUEST, request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binder forwards [TransferManager] API calls to selected instance of downloader.
|
||||
*/
|
||||
class Binder(
|
||||
downloader: TransferManagerImpl,
|
||||
service: FileTransferService
|
||||
) : LocalBinder<FileTransferService>(service),
|
||||
class Binder(downloader: TransferManagerImpl, service: FileTransferService) :
|
||||
LocalBinder<FileTransferService>(service),
|
||||
TransferManager by downloader
|
||||
|
||||
@Inject
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.transfer
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.transfer
|
||||
|
||||
|
|
@ -19,11 +19,7 @@ interface TransferManager {
|
|||
/**
|
||||
* Snapshot of transfer manager status. All data is immutable and can be safely shared.
|
||||
*/
|
||||
data class Status(
|
||||
val pending: List<Transfer>,
|
||||
val running: List<Transfer>,
|
||||
val completed: List<Transfer>
|
||||
) {
|
||||
data class Status(val pending: List<Transfer>, val running: List<Transfer>, val completed: List<Transfer>) {
|
||||
companion object {
|
||||
val EMPTY = Status(emptyList(), emptyList(), emptyList())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.transfer
|
||||
|
||||
|
|
@ -16,10 +16,9 @@ import com.nextcloud.client.files.Request
|
|||
import com.owncloud.android.datamodel.OCFile
|
||||
import java.util.UUID
|
||||
|
||||
class TransferManagerConnection(
|
||||
context: Context,
|
||||
val user: User
|
||||
) : LocalConnection<FileTransferService>(context), TransferManager {
|
||||
class TransferManagerConnection(context: Context, val user: User) :
|
||||
LocalConnection<FileTransferService>(context),
|
||||
TransferManager {
|
||||
|
||||
private var transferListeners: MutableSet<(Transfer) -> Unit> = mutableSetOf()
|
||||
private var statusListeners: MutableSet<(TransferManager.Status) -> Unit> = mutableSetOf()
|
||||
|
|
@ -64,9 +63,7 @@ class TransferManagerConnection(
|
|||
binder?.removeStatusListener(listener)
|
||||
}
|
||||
|
||||
override fun createBindIntent(): Intent {
|
||||
return FileTransferService.createBindIntent(context, user)
|
||||
}
|
||||
override fun createBindIntent(): Intent = FileTransferService.createBindIntent(context, user)
|
||||
|
||||
override fun onBound(binder: IBinder) {
|
||||
super.onBound(binder)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.transfer
|
||||
|
||||
|
|
@ -118,8 +118,8 @@ class TransferManagerImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createDownloadTask(request: DownloadRequest): TaskFunction<DownloadTask.Result, Int> {
|
||||
return if (request.test) {
|
||||
private fun createDownloadTask(request: DownloadRequest): TaskFunction<DownloadTask.Result, Int> =
|
||||
if (request.test) {
|
||||
{ progress: OnProgressCallback<Int>, isCancelled: IsCancelled ->
|
||||
testDownloadTask(request.file, progress, isCancelled)
|
||||
}
|
||||
|
|
@ -130,25 +130,22 @@ class TransferManagerImpl(
|
|||
}
|
||||
wrapper
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUploadTask(request: UploadRequest): TaskFunction<UploadTask.Result, Int> {
|
||||
return if (request.test) {
|
||||
{ progress: OnProgressCallback<Int>, isCancelled: IsCancelled ->
|
||||
val file = UploadFileOperation.obtainNewOCFileToUpload(
|
||||
request.upload.remotePath,
|
||||
request.upload.localPath,
|
||||
request.upload.mimeType
|
||||
)
|
||||
testUploadTask(file, progress, isCancelled)
|
||||
}
|
||||
} else {
|
||||
val uploadTask = uploadTaskFactory.create()
|
||||
val wrapper: TaskFunction<UploadTask.Result, Int> = { _: ((Int) -> Unit), _ ->
|
||||
uploadTask.upload(request.user, request.upload)
|
||||
}
|
||||
wrapper
|
||||
private fun createUploadTask(request: UploadRequest): TaskFunction<UploadTask.Result, Int> = if (request.test) {
|
||||
{ progress: OnProgressCallback<Int>, isCancelled: IsCancelled ->
|
||||
val file = UploadFileOperation.obtainNewOCFileToUpload(
|
||||
request.upload.remotePath,
|
||||
request.upload.localPath,
|
||||
request.upload.mimeType
|
||||
)
|
||||
testUploadTask(file, progress, isCancelled)
|
||||
}
|
||||
} else {
|
||||
val uploadTask = uploadTaskFactory.create()
|
||||
val wrapper: TaskFunction<UploadTask.Result, Int> = { _: ((Int) -> Unit), _ ->
|
||||
uploadTask.upload(request.user, request.upload)
|
||||
}
|
||||
wrapper
|
||||
}
|
||||
|
||||
private fun onTransferUpdate(transfer: Transfer) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.transfer
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.upload
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
class FileUploadBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var uploadsStorageManager: UploadsStorageManager
|
||||
|
||||
companion object {
|
||||
const val UPLOAD_ID = "UPLOAD_ID"
|
||||
const val REMOTE_PATH = "REMOTE_PATH"
|
||||
const val STORAGE_PATH = "STORAGE_PATH"
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
MainApp.getAppComponent().inject(this)
|
||||
|
||||
val remotePath = intent.getStringExtra(REMOTE_PATH) ?: return
|
||||
val storagePath = intent.getStringExtra(STORAGE_PATH) ?: return
|
||||
val uploadId = intent.getLongExtra(UPLOAD_ID, -1L)
|
||||
if (uploadId == -1L) {
|
||||
return
|
||||
}
|
||||
|
||||
uploadsStorageManager.removeUpload(uploadId)
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(
|
||||
NotificationUtils.createUploadNotificationTag(remotePath, storagePath),
|
||||
FileUploadWorker.NOTIFICATION_ERROR_ID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
|
|
@ -12,25 +12,37 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.device.BatteryStatus
|
||||
import com.nextcloud.client.device.PowerManagementService
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.currentUploadFileOperation
|
||||
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.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus
|
||||
import com.owncloud.android.db.OCUpload
|
||||
import com.owncloud.android.db.UploadResult
|
||||
import com.owncloud.android.files.services.NameCollisionPolicy
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
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.FileUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Semaphore
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
@ -45,6 +57,11 @@ class FileUploadHelper {
|
|||
@Inject
|
||||
lateinit var uploadsStorageManager: UploadsStorageManager
|
||||
|
||||
@Inject
|
||||
lateinit var fileStorageManager: FileDataStorageManager
|
||||
|
||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
init {
|
||||
MainApp.getAppComponent().inject(this)
|
||||
}
|
||||
|
|
@ -59,15 +76,13 @@ class FileUploadHelper {
|
|||
|
||||
private var instance: FileUploadHelper? = null
|
||||
|
||||
fun instance(): FileUploadHelper {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: FileUploadHelper().also { instance = it }
|
||||
}
|
||||
private val retryFailedUploadsSemaphore = Semaphore(1)
|
||||
|
||||
fun instance(): FileUploadHelper = instance ?: synchronized(this) {
|
||||
instance ?: FileUploadHelper().also { instance = it }
|
||||
}
|
||||
|
||||
fun buildRemoteName(accountName: String, remotePath: String): String {
|
||||
return accountName + remotePath
|
||||
}
|
||||
fun buildRemoteName(accountName: String, remotePath: String): String = accountName + remotePath
|
||||
}
|
||||
|
||||
fun retryFailedUploads(
|
||||
|
|
@ -76,19 +91,27 @@ class FileUploadHelper {
|
|||
accountManager: UserAccountManager,
|
||||
powerManagementService: PowerManagementService
|
||||
) {
|
||||
val failedUploads = uploadsStorageManager.failedUploads
|
||||
if (failedUploads == null || failedUploads.isEmpty()) {
|
||||
Log_OC.d(TAG, "Failed uploads are empty or null")
|
||||
return
|
||||
}
|
||||
if (retryFailedUploadsSemaphore.tryAcquire()) {
|
||||
try {
|
||||
val failedUploads = uploadsStorageManager.failedUploads
|
||||
if (failedUploads == null || failedUploads.isEmpty()) {
|
||||
Log_OC.d(TAG, "Failed uploads are empty or null")
|
||||
return
|
||||
}
|
||||
|
||||
retryUploads(
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
accountManager,
|
||||
powerManagementService,
|
||||
failedUploads
|
||||
)
|
||||
retryUploads(
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
accountManager,
|
||||
powerManagementService,
|
||||
failedUploads
|
||||
)
|
||||
} finally {
|
||||
retryFailedUploadsSemaphore.release()
|
||||
}
|
||||
} else {
|
||||
Log_OC.d(TAG, "Skip retryFailedUploads since it is already running")
|
||||
}
|
||||
}
|
||||
|
||||
fun retryCancelledUploads(
|
||||
|
|
@ -120,37 +143,54 @@ class FileUploadHelper {
|
|||
failedUploads: Array<OCUpload>
|
||||
): Boolean {
|
||||
var showNotExistMessage = false
|
||||
val (gotNetwork, _, gotWifi) = connectivityService.connectivity
|
||||
val isOnline = checkConnectivity(connectivityService)
|
||||
val connectivity = connectivityService.connectivity
|
||||
val batteryStatus = powerManagementService.battery
|
||||
val charging = batteryStatus.isCharging || batteryStatus.isFull
|
||||
val isPowerSaving = powerManagementService.isPowerSavingEnabled
|
||||
var uploadUser = Optional.empty<User>()
|
||||
val accountNames = accountManager.accounts.filter { account ->
|
||||
accountManager.getUser(account.name).isPresent
|
||||
}.map { account ->
|
||||
account.name
|
||||
}.toHashSet()
|
||||
|
||||
for (failedUpload in failedUploads) {
|
||||
// 1. extract failed upload owner account and cache it between loops (expensive query)
|
||||
if (!uploadUser.isPresent || !uploadUser.get().nameEquals(failedUpload.accountName)) {
|
||||
uploadUser = accountManager.getUser(failedUpload.accountName)
|
||||
if (!accountNames.contains(failedUpload.accountName)) {
|
||||
uploadsStorageManager.removeUpload(failedUpload)
|
||||
continue
|
||||
}
|
||||
val isDeleted = !File(failedUpload.localPath).exists()
|
||||
if (isDeleted) {
|
||||
showNotExistMessage = true
|
||||
|
||||
// 2A. for deleted files, mark as permanently failed
|
||||
if (failedUpload.lastResult != UploadResult.FILE_NOT_FOUND) {
|
||||
failedUpload.lastResult = UploadResult.FILE_NOT_FOUND
|
||||
val uploadResult =
|
||||
checkUploadConditions(failedUpload, connectivity, batteryStatus, powerManagementService, isOnline)
|
||||
|
||||
if (uploadResult != UploadResult.UPLOADED) {
|
||||
if (failedUpload.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
|
||||
|
||||
failedUpload.lastResult = uploadResult
|
||||
uploadsStorageManager.updateUpload(failedUpload)
|
||||
}
|
||||
} else if (!isPowerSaving && gotNetwork &&
|
||||
canUploadBeRetried(failedUpload, gotWifi, charging) && !connectivityService.isInternetWalled
|
||||
) {
|
||||
// 2B. for existing local files, try restarting it if possible
|
||||
retryUpload(failedUpload, uploadUser.get())
|
||||
if (uploadResult == UploadResult.FILE_NOT_FOUND) {
|
||||
showNotExistMessage = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
failedUpload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
|
||||
uploadsStorageManager.updateUpload(failedUpload)
|
||||
}
|
||||
|
||||
accountNames.forEach { accountName ->
|
||||
val user = accountManager.getUser(accountName)
|
||||
if (user.isPresent) {
|
||||
backgroundJobManager.startFilesUploadJob(user.get(), failedUploads.getUploadIds(), false)
|
||||
}
|
||||
}
|
||||
|
||||
return showNotExistMessage
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@Suppress("LongParameterList")
|
||||
fun uploadNewFiles(
|
||||
user: User,
|
||||
|
|
@ -161,7 +201,8 @@ class FileUploadHelper {
|
|||
createdBy: Int,
|
||||
requiresWifi: Boolean,
|
||||
requiresCharging: Boolean,
|
||||
nameCollisionPolicy: NameCollisionPolicy
|
||||
nameCollisionPolicy: NameCollisionPolicy,
|
||||
showSameFileAlreadyExistsNotification: Boolean = true
|
||||
) {
|
||||
val uploads = localPaths.mapIndexed { index, localPath ->
|
||||
OCUpload(localPath, remotePaths[index], user.accountName).apply {
|
||||
|
|
@ -175,7 +216,7 @@ class FileUploadHelper {
|
|||
}
|
||||
}
|
||||
uploadsStorageManager.storeUploads(uploads)
|
||||
backgroundJobManager.startFilesUploadJob(user)
|
||||
backgroundJobManager.startFilesUploadJob(user, uploads.getUploadIds(), showSameFileAlreadyExistsNotification)
|
||||
}
|
||||
|
||||
fun removeFileUpload(remotePath: String, accountName: String) {
|
||||
|
|
@ -185,25 +226,42 @@ class FileUploadHelper {
|
|||
// need to update now table in mUploadsStorageManager,
|
||||
// since the operation will not get to be run by FileUploader#uploadFile
|
||||
uploadsStorageManager.removeUpload(accountName, remotePath)
|
||||
|
||||
cancelAndRestartUploadJob(user)
|
||||
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!")
|
||||
Log_OC.e(TAG, "Error cancelling current upload because user does not exist!: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFileUpload(remotePath: String, accountName: String) {
|
||||
uploadsStorageManager.getUploadByRemotePath(remotePath).run {
|
||||
removeFileUpload(remotePath, accountName)
|
||||
uploadStatus = UploadStatus.UPLOAD_CANCELLED
|
||||
uploadsStorageManager.storeUpload(this)
|
||||
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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAndRestartUploadJob(user: User) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAndRestartUploadJob(user: User, uploadIds: LongArray) {
|
||||
backgroundJobManager.run {
|
||||
cancelFilesUploadJob(user)
|
||||
startFilesUploadJob(user)
|
||||
startFilesUploadJob(user, uploadIds, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,15 +271,67 @@ class FileUploadHelper {
|
|||
return false
|
||||
}
|
||||
|
||||
val upload: OCUpload = uploadsStorageManager.getUploadByRemotePath(file.remotePath) ?: return false
|
||||
return upload.uploadStatus == UploadStatus.UPLOAD_IN_PROGRESS
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private fun canUploadBeRetried(upload: OCUpload, gotWifi: Boolean, isCharging: Boolean): Boolean {
|
||||
val file = File(upload.localPath)
|
||||
val needsWifi = upload.isUseWifiOnly
|
||||
val needsCharging = upload.isWhileChargingOnly
|
||||
return file.exists() && (!needsWifi || gotWifi) && (!needsCharging || isCharging)
|
||||
private fun checkConnectivity(connectivityService: ConnectivityService): Boolean {
|
||||
// check that connection isn't walled off and that the server is reachable
|
||||
return connectivityService.getConnectivity().isConnected && !connectivityService.isInternetWalled()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dupe of [UploadFileOperation.checkConditions], needed to check if the upload should even be scheduled
|
||||
* @return [UploadResult.UPLOADED] if the upload should be scheduled, otherwise the reason why it shouldn't
|
||||
*/
|
||||
private fun checkUploadConditions(
|
||||
upload: OCUpload,
|
||||
connectivity: Connectivity,
|
||||
battery: BatteryStatus,
|
||||
powerManagementService: PowerManagementService,
|
||||
hasGeneralConnection: Boolean
|
||||
): UploadResult {
|
||||
var conditions = UploadResult.UPLOADED
|
||||
|
||||
// check that internet is available
|
||||
if (!hasGeneralConnection) {
|
||||
conditions = UploadResult.NETWORK_CONNECTION
|
||||
}
|
||||
|
||||
// check that local file exists; skip the upload otherwise
|
||||
if (!File(upload.localPath).exists()) {
|
||||
conditions = UploadResult.FILE_NOT_FOUND
|
||||
}
|
||||
|
||||
// check that connectivity conditions are met; delay upload otherwise
|
||||
if (upload.isUseWifiOnly && (!connectivity.isWifi || connectivity.isMetered)) {
|
||||
conditions = UploadResult.DELAYED_FOR_WIFI
|
||||
}
|
||||
|
||||
// check if charging conditions are met; delay upload otherwise
|
||||
if (upload.isWhileChargingOnly && !battery.isCharging && !battery.isFull) {
|
||||
conditions = UploadResult.DELAYED_FOR_CHARGING
|
||||
}
|
||||
|
||||
// check that device is not in power save mode; delay upload otherwise
|
||||
if (powerManagementService.isPowerSavingEnabled) {
|
||||
conditions = UploadResult.DELAYED_IN_POWER_SAVE_MODE
|
||||
}
|
||||
|
||||
return conditions
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
|
|
@ -266,7 +376,43 @@ class FileUploadHelper {
|
|||
}
|
||||
}
|
||||
uploadsStorageManager.storeUploads(uploads)
|
||||
backgroundJobManager.startFilesUploadJob(user)
|
||||
val uploadIds: LongArray = uploads.filterNotNull().map { it.uploadId }.toLongArray()
|
||||
backgroundJobManager.startFilesUploadJob(user, uploadIds, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any existing file in the same directory that has the same name as the provided new file.
|
||||
*
|
||||
* This function checks the parent directory of the given `newFile` for any file with the same name.
|
||||
* If such a file is found, it is removed using the `RemoveFileOperation`.
|
||||
*
|
||||
* @param duplicatedFile File to be deleted
|
||||
* @param client Needed for executing RemoveFileOperation
|
||||
* @param user Needed for creating client
|
||||
*/
|
||||
fun removeDuplicatedFile(duplicatedFile: OCFile, client: OwnCloudClient, user: User, onCompleted: () -> Unit) {
|
||||
val job = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
job.launch {
|
||||
val removeFileOperation = RemoveFileOperation(
|
||||
duplicatedFile,
|
||||
false,
|
||||
user,
|
||||
true,
|
||||
MainApp.getAppContext(),
|
||||
fileStorageManager
|
||||
)
|
||||
|
||||
val result = removeFileOperation.execute(client)
|
||||
|
||||
if (result.isSuccess) {
|
||||
Log_OC.d(TAG, "Replaced file successfully removed")
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
onCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun retryUpload(upload: OCUpload, user: User) {
|
||||
|
|
@ -275,25 +421,20 @@ class FileUploadHelper {
|
|||
upload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
|
||||
uploadsStorageManager.updateUpload(upload)
|
||||
|
||||
backgroundJobManager.startFilesUploadJob(user)
|
||||
backgroundJobManager.startFilesUploadJob(user, longArrayOf(upload.uploadId), false)
|
||||
}
|
||||
|
||||
fun cancel(accountName: String) {
|
||||
uploadsStorageManager.removeUploads(accountName)
|
||||
cancelAndRestartUploadJob(accountManager.getUser(accountName).get())
|
||||
val uploadIds = uploadsStorageManager.getCurrentUploadIds(accountName)
|
||||
cancelAndRestartUploadJob(accountManager.getUser(accountName).get(), uploadIds)
|
||||
}
|
||||
|
||||
fun addUploadTransferProgressListener(
|
||||
listener: OnDatatransferProgressListener,
|
||||
targetKey: String
|
||||
) {
|
||||
fun addUploadTransferProgressListener(listener: OnDatatransferProgressListener, targetKey: String) {
|
||||
mBoundListeners[targetKey] = listener
|
||||
}
|
||||
|
||||
fun removeUploadTransferProgressListener(
|
||||
listener: OnDatatransferProgressListener,
|
||||
targetKey: String
|
||||
) {
|
||||
fun removeUploadTransferProgressListener(listener: OnDatatransferProgressListener, targetKey: String) {
|
||||
if (mBoundListeners[targetKey] === listener) {
|
||||
mBoundListeners.remove(targetKey)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
|
|
@ -21,11 +21,13 @@ 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.extensions.getPercent
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
||||
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.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
|
||||
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
|
|
@ -35,6 +37,7 @@ import com.owncloud.android.operations.UploadFileOperation
|
|||
import com.owncloud.android.utils.ErrorMessageAdapter
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import java.io.File
|
||||
import kotlin.random.Random
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class FileUploadWorker(
|
||||
|
|
@ -48,20 +51,28 @@ class FileUploadWorker(
|
|||
val preferences: AppPreferences,
|
||||
val context: Context,
|
||||
params: WorkerParameters
|
||||
) : Worker(context, params), OnDatatransferProgressListener {
|
||||
) : Worker(context, params),
|
||||
OnDatatransferProgressListener {
|
||||
|
||||
companion object {
|
||||
val TAG: String = FileUploadWorker::class.java.simpleName
|
||||
|
||||
const val NOTIFICATION_ERROR_ID: Int = 413
|
||||
private const val MAX_PROGRESS: Int = 100
|
||||
|
||||
const val ACCOUNT = "data_account"
|
||||
const val UPLOAD_IDS = "uploads_ids"
|
||||
const val CURRENT_BATCH_INDEX = "batch_index"
|
||||
const val TOTAL_UPLOAD_SIZE = "total_upload_size"
|
||||
const val SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION = "show_same_file_already_exists_notification"
|
||||
|
||||
var currentUploadFileOperation: UploadFileOperation? = null
|
||||
|
||||
private const val UPLOADS_ADDED_MESSAGE = "UPLOADS_ADDED"
|
||||
private const val UPLOAD_START_MESSAGE = "UPLOAD_START"
|
||||
private const val UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH"
|
||||
|
||||
private const val BATCH_SIZE = 100
|
||||
|
||||
const val EXTRA_UPLOAD_RESULT = "RESULT"
|
||||
const val EXTRA_REMOTE_PATH = "REMOTE_PATH"
|
||||
const val EXTRA_OLD_REMOTE_PATH = "OLD_REMOTE_PATH"
|
||||
|
|
@ -75,35 +86,32 @@ class FileUploadWorker(
|
|||
const val LOCAL_BEHAVIOUR_FORGET = 2
|
||||
const val LOCAL_BEHAVIOUR_DELETE = 3
|
||||
|
||||
fun getUploadsAddedMessage(): String {
|
||||
return FileUploadWorker::class.java.name + UPLOADS_ADDED_MESSAGE
|
||||
}
|
||||
fun getUploadsAddedMessage(): String = FileUploadWorker::class.java.name + UPLOADS_ADDED_MESSAGE
|
||||
|
||||
fun getUploadStartMessage(): String {
|
||||
return FileUploadWorker::class.java.name + UPLOAD_START_MESSAGE
|
||||
}
|
||||
fun getUploadStartMessage(): String = FileUploadWorker::class.java.name + UPLOAD_START_MESSAGE
|
||||
|
||||
fun getUploadFinishMessage(): String {
|
||||
return FileUploadWorker::class.java.name + UPLOAD_FINISH_MESSAGE
|
||||
}
|
||||
fun getUploadFinishMessage(): String = FileUploadWorker::class.java.name + UPLOAD_FINISH_MESSAGE
|
||||
}
|
||||
|
||||
private var lastPercent = 0
|
||||
private val notificationManager = UploadNotificationManager(context, viewThemeUtils)
|
||||
private val notificationManager = UploadNotificationManager(context, viewThemeUtils, Random.nextInt())
|
||||
private val intents = FileUploaderIntents(context)
|
||||
private val fileUploaderDelegate = FileUploaderDelegate()
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun doWork(): Result {
|
||||
return try {
|
||||
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
|
||||
val result = retrievePagesBySortingUploadsByID()
|
||||
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
|
||||
result
|
||||
} catch (t: Throwable) {
|
||||
Log_OC.e(TAG, "Error caught at FileUploadWorker " + t.localizedMessage)
|
||||
Result.failure()
|
||||
override fun doWork(): Result = try {
|
||||
Log_OC.d(TAG, "FileUploadWorker started")
|
||||
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
|
||||
val result = uploadFiles()
|
||||
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
|
||||
notificationManager.dismissNotification()
|
||||
if (result == Result.success()) {
|
||||
setIdleWorkerState()
|
||||
}
|
||||
result
|
||||
} catch (t: Throwable) {
|
||||
Log_OC.e(TAG, "Error caught at FileUploadWorker $t")
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
|
|
@ -111,27 +119,59 @@ class FileUploadWorker(
|
|||
|
||||
setIdleWorkerState()
|
||||
currentUploadFileOperation?.cancel(null)
|
||||
notificationManager.dismissWorkerNotifications()
|
||||
notificationManager.dismissNotification()
|
||||
|
||||
super.onStopped()
|
||||
}
|
||||
|
||||
private fun setWorkerState(user: User?, uploads: List<OCUpload>) {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Upload(user, uploads))
|
||||
private fun setWorkerState(user: User?) {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.UploadStarted(user))
|
||||
}
|
||||
|
||||
private fun setIdleWorkerState() {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle)
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.UploadFinished(currentUploadFileOperation?.file))
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private fun retrievePagesBySortingUploadsByID(): Result {
|
||||
val accountName = inputData.getString(ACCOUNT) ?: return Result.failure()
|
||||
var currentPage = uploadsStorageManager.getCurrentAndPendingUploadsForAccountPageAscById(-1, accountName)
|
||||
@Suppress("ReturnCount", "LongMethod")
|
||||
private fun uploadFiles(): Result {
|
||||
val accountName = inputData.getString(ACCOUNT)
|
||||
if (accountName == null) {
|
||||
Log_OC.e(TAG, "accountName is null")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
notificationManager.dismissWorkerNotifications()
|
||||
val uploadIds = inputData.getLongArray(UPLOAD_IDS)
|
||||
if (uploadIds == null) {
|
||||
Log_OC.e(TAG, "uploadIds is null")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
while (currentPage.isNotEmpty() && !isStopped) {
|
||||
val currentBatchIndex = inputData.getInt(CURRENT_BATCH_INDEX, -1)
|
||||
if (currentBatchIndex == -1) {
|
||||
Log_OC.e(TAG, "currentBatchIndex is -1, cancelling")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val totalUploadSize = inputData.getInt(TOTAL_UPLOAD_SIZE, -1)
|
||||
if (totalUploadSize == -1) {
|
||||
Log_OC.e(TAG, "totalUploadSize is -1, cancelling")
|
||||
return 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()
|
||||
}
|
||||
|
||||
val user = optionalUser.get()
|
||||
val previouslyUploadedFileSize = currentBatchIndex * FileUploadHelper.MAX_FILE_COUNT
|
||||
val uploads = uploadsStorageManager.getUploadsByIds(uploadIds, accountName)
|
||||
val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context)
|
||||
val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context)
|
||||
|
||||
for ((index, upload) in uploads.withIndex()) {
|
||||
if (preferences.isGlobalUploadPaused) {
|
||||
Log_OC.d(TAG, "Upload is paused, skip uploading files!")
|
||||
notificationManager.notifyPaused(
|
||||
|
|
@ -140,86 +180,101 @@ class FileUploadWorker(
|
|||
return Result.success()
|
||||
}
|
||||
|
||||
Log_OC.d(TAG, "Handling ${currentPage.size} uploads for account $accountName")
|
||||
val lastId = currentPage.last().uploadId
|
||||
uploadFiles(currentPage, accountName)
|
||||
currentPage =
|
||||
uploadsStorageManager.getCurrentAndPendingUploadsForAccountPageAscById(lastId, accountName)
|
||||
if (canExitEarly()) {
|
||||
notificationManager.showConnectionErrorNotification()
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
if (isStopped) {
|
||||
continue
|
||||
}
|
||||
|
||||
setWorkerState(user)
|
||||
val operation = createUploadFileOperation(upload, user)
|
||||
currentUploadFileOperation = operation
|
||||
|
||||
val currentIndex = (index + 1)
|
||||
val currentUploadIndex = (currentIndex + previouslyUploadedFileSize)
|
||||
notificationManager.prepareForStart(
|
||||
operation,
|
||||
cancelPendingIntent = intents.startIntent(operation),
|
||||
startIntent = intents.notificationStartIntent(operation),
|
||||
currentUploadIndex = currentUploadIndex,
|
||||
totalUploadSize = totalUploadSize
|
||||
)
|
||||
|
||||
val result = upload(operation, user, client)
|
||||
currentUploadFileOperation = null
|
||||
sendUploadFinishEvent(totalUploadSize, currentUploadIndex, operation, result)
|
||||
}
|
||||
|
||||
if (isStopped) {
|
||||
Log_OC.d(TAG, "FileUploadWorker for account $accountName was stopped")
|
||||
} else {
|
||||
Log_OC.d(TAG, "No more pending uploads for account $accountName, stopping work")
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun uploadFiles(uploads: List<OCUpload>, accountName: String) {
|
||||
val user = userAccountManager.getUser(accountName)
|
||||
setWorkerState(user.get(), uploads)
|
||||
private fun sendUploadFinishEvent(
|
||||
totalUploadSize: Int,
|
||||
currentUploadIndex: Int,
|
||||
operation: UploadFileOperation,
|
||||
result: RemoteOperationResult<*>
|
||||
) {
|
||||
val shouldBroadcast =
|
||||
(totalUploadSize > BATCH_SIZE && currentUploadIndex > 0) && currentUploadIndex % BATCH_SIZE == 0
|
||||
|
||||
for (upload in uploads) {
|
||||
if (isStopped) {
|
||||
break
|
||||
}
|
||||
|
||||
if (user.isPresent) {
|
||||
val uploadFileOperation = createUploadFileOperation(upload, user.get())
|
||||
|
||||
currentUploadFileOperation = uploadFileOperation
|
||||
val result = upload(uploadFileOperation, user.get())
|
||||
currentUploadFileOperation = null
|
||||
|
||||
fileUploaderDelegate.sendBroadcastUploadFinished(
|
||||
uploadFileOperation,
|
||||
result,
|
||||
uploadFileOperation.oldFile?.storagePath,
|
||||
context,
|
||||
localBroadcastManager
|
||||
)
|
||||
} else {
|
||||
uploadsStorageManager.removeUpload(upload.uploadId)
|
||||
}
|
||||
if (shouldBroadcast) {
|
||||
// delay broadcast
|
||||
fileUploaderDelegate.sendBroadcastUploadFinished(
|
||||
operation,
|
||||
result,
|
||||
operation.oldFile?.storagePath,
|
||||
context,
|
||||
localBroadcastManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation {
|
||||
return UploadFileOperation(
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
powerManagementService,
|
||||
user,
|
||||
null,
|
||||
upload,
|
||||
upload.nameCollisionPolicy,
|
||||
upload.localAction,
|
||||
context,
|
||||
upload.isUseWifiOnly,
|
||||
upload.isWhileChargingOnly,
|
||||
true,
|
||||
FileDataStorageManager(user, context.contentResolver)
|
||||
).apply {
|
||||
addDataTransferProgressListener(this@FileUploadWorker)
|
||||
private fun canExitEarly(): Boolean {
|
||||
val result = !connectivityService.isConnected ||
|
||||
connectivityService.isInternetWalled ||
|
||||
isStopped
|
||||
|
||||
if (result) {
|
||||
Log_OC.d(TAG, "No internet connection, stopping worker.")
|
||||
} else {
|
||||
notificationManager.dismissErrorNotification()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
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)
|
||||
).apply {
|
||||
addDataTransferProgressListener(this@FileUploadWorker)
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "DEPRECATION")
|
||||
private fun upload(uploadFileOperation: UploadFileOperation, user: User): RemoteOperationResult<Any?> {
|
||||
private fun upload(
|
||||
uploadFileOperation: UploadFileOperation,
|
||||
user: User,
|
||||
client: OwnCloudClient
|
||||
): RemoteOperationResult<Any?> {
|
||||
lateinit var result: RemoteOperationResult<Any?>
|
||||
|
||||
notificationManager.prepareForStart(
|
||||
uploadFileOperation,
|
||||
cancelPendingIntent = intents.startIntent(uploadFileOperation),
|
||||
intents.notificationStartIntent(uploadFileOperation)
|
||||
)
|
||||
|
||||
try {
|
||||
val storageManager = uploadFileOperation.storageManager
|
||||
val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context)
|
||||
val uploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context)
|
||||
result = uploadFileOperation.execute(uploadClient)
|
||||
|
||||
result = uploadFileOperation.execute(client)
|
||||
val task = ThumbnailsCacheManager.ThumbnailGenerationTask(storageManager, user)
|
||||
val file = File(uploadFileOperation.originalStoragePath)
|
||||
val remoteId: String? = uploadFileOperation.file.remoteId
|
||||
|
|
@ -238,16 +293,17 @@ class FileUploadWorker(
|
|||
if (!isStopped || !result.isCancelled) {
|
||||
uploadsStorageManager.updateDatabaseUploadResult(result, uploadFileOperation)
|
||||
notifyUploadResult(uploadFileOperation, result)
|
||||
notificationManager.dismissWorkerNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
@Suppress("ReturnCount", "LongMethod")
|
||||
private fun notifyUploadResult(
|
||||
uploadFileOperation: UploadFileOperation,
|
||||
uploadResult: RemoteOperationResult<Any?>
|
||||
) {
|
||||
Log_OC.d(TAG, "NotifyUploadResult with resultCode: " + uploadResult.code)
|
||||
val showSameFileAlreadyExistsNotification =
|
||||
inputData.getBoolean(SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION, false)
|
||||
|
||||
if (uploadResult.isSuccess) {
|
||||
notificationManager.dismissOldErrorNotification(uploadFileOperation)
|
||||
|
|
@ -259,10 +315,19 @@ class FileUploadWorker(
|
|||
}
|
||||
|
||||
// Only notify if it is not same file on remote that causes conflict
|
||||
if (uploadResult.code == ResultCode.SYNC_CONFLICT && FileUploadHelper().isSameFileOnRemote(
|
||||
uploadFileOperation.user, File(uploadFileOperation.storagePath), uploadFileOperation.remotePath, context
|
||||
if (uploadResult.code == ResultCode.SYNC_CONFLICT &&
|
||||
FileUploadHelper().isSameFileOnRemote(
|
||||
uploadFileOperation.user,
|
||||
File(uploadFileOperation.storagePath),
|
||||
uploadFileOperation.remotePath,
|
||||
context
|
||||
)
|
||||
) {
|
||||
if (showSameFileAlreadyExistsNotification) {
|
||||
notificationManager.showSameFileAlreadyExistsNotification(uploadFileOperation.fileName)
|
||||
}
|
||||
|
||||
uploadFileOperation.handleLocalBehaviour()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -300,31 +365,51 @@ class FileUploadWorker(
|
|||
null
|
||||
}
|
||||
|
||||
notifyForFailedResult(uploadResult.code, conflictResolveIntent, credentialIntent, errorMessage)
|
||||
showNewNotification(uploadFileOperation)
|
||||
val cancelUploadActionIntent = if (conflictResolveIntent != null) {
|
||||
intents.cancelUploadActionIntent(uploadFileOperation)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
notifyForFailedResult(
|
||||
uploadFileOperation,
|
||||
uploadResult.code,
|
||||
conflictResolveIntent,
|
||||
cancelUploadActionIntent,
|
||||
credentialIntent,
|
||||
errorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private val minProgressUpdateInterval = 750
|
||||
private var lastUpdateTime = 0L
|
||||
|
||||
/**
|
||||
* Receives from [com.owncloud.android.operations.UploadFileOperation.normalUpload]
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
override fun onTransferProgress(
|
||||
progressRate: Long,
|
||||
totalTransferredSoFar: Long,
|
||||
totalToTransfer: Long,
|
||||
fileAbsoluteName: String
|
||||
) {
|
||||
val percent = (MAX_PROGRESS * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt()
|
||||
val percent = getPercent(totalTransferredSoFar, totalToTransfer)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
if (percent != lastPercent) {
|
||||
if (percent != lastPercent && (currentTime - lastUpdateTime) >= minProgressUpdateInterval) {
|
||||
notificationManager.run {
|
||||
val accountName = currentUploadFileOperation?.user?.accountName
|
||||
val remotePath = currentUploadFileOperation?.remotePath
|
||||
val filename = currentUploadFileOperation?.fileName ?: ""
|
||||
|
||||
updateUploadProgress(filename, percent, currentUploadFileOperation)
|
||||
updateUploadProgress(percent, currentUploadFileOperation)
|
||||
|
||||
if (accountName != null && remotePath != null) {
|
||||
val key: String =
|
||||
FileUploadHelper.buildRemoteName(accountName, remotePath)
|
||||
val key: String = FileUploadHelper.buildRemoteName(accountName, remotePath)
|
||||
val boundListener = FileUploadHelper.mBoundListeners[key]
|
||||
val filename = currentUploadFileOperation?.fileName ?: ""
|
||||
|
||||
boundListener?.onTransferProgress(
|
||||
progressRate,
|
||||
|
|
@ -336,6 +421,7 @@ class FileUploadWorker(
|
|||
|
||||
dismissOldErrorNotification(currentUploadFileOperation)
|
||||
}
|
||||
lastUpdateTime = currentTime
|
||||
}
|
||||
|
||||
lastPercent = percent
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
|
|
@ -12,7 +12,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.owncloud.android.authentication.AuthenticatorActivity
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||
import com.owncloud.android.operations.UploadFileOperation
|
||||
import com.owncloud.android.ui.activity.ConflictsResolveActivity.Companion.createIntent
|
||||
import com.owncloud.android.ui.activity.UploadListActivity
|
||||
|
|
@ -57,32 +56,6 @@ class FileUploaderIntents(private val context: Context) {
|
|||
)
|
||||
}
|
||||
|
||||
fun resultIntent(resultCode: ResultCode, operation: UploadFileOperation): PendingIntent {
|
||||
val intent = if (resultCode == ResultCode.SYNC_CONFLICT) {
|
||||
createIntent(
|
||||
operation.file,
|
||||
operation.user,
|
||||
operation.ocUploadId,
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
context
|
||||
)
|
||||
} else {
|
||||
UploadListActivity.createIntent(
|
||||
operation.file,
|
||||
operation.user,
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
System.currentTimeMillis().toInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
fun notificationStartIntent(operation: UploadFileOperation?): PendingIntent {
|
||||
val intent = UploadListActivity.createIntent(
|
||||
operation?.file,
|
||||
|
|
@ -119,4 +92,19 @@ class FileUploaderIntents(private val context: Context) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelUploadActionIntent(uploadFileOperation: UploadFileOperation): PendingIntent {
|
||||
val intent = Intent(context, FileUploadBroadcastReceiver::class.java).apply {
|
||||
putExtra(FileUploadBroadcastReceiver.UPLOAD_ID, uploadFileOperation.ocUploadId)
|
||||
putExtra(FileUploadBroadcastReceiver.REMOTE_PATH, uploadFileOperation.file.remotePath)
|
||||
putExtra(FileUploadBroadcastReceiver.STORAGE_PATH, uploadFileOperation.file.storagePath)
|
||||
}
|
||||
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
|
|
|
|||
|
|
@ -1,65 +1,52 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.nextcloud.client.jobs.notification.WorkerNotificationManager
|
||||
import com.nextcloud.utils.extensions.isFileSpecificError
|
||||
import com.nextcloud.utils.numberFormatter.NumberFormatter
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.operations.UploadFileOperation
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
|
||||
class UploadNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) {
|
||||
companion object {
|
||||
private const val ID = 411
|
||||
}
|
||||
|
||||
private var notification: Notification? = null
|
||||
private var notificationBuilder: NotificationCompat.Builder =
|
||||
NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
|
||||
setContentTitle(context.getString(R.string.foreground_service_upload))
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD)
|
||||
}
|
||||
}
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
init {
|
||||
notification = notificationBuilder.build()
|
||||
}
|
||||
class UploadNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils, id: Int) :
|
||||
WorkerNotificationManager(id, context, viewThemeUtils, R.string.foreground_service_upload) {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun prepareForStart(
|
||||
uploadFileOperation: UploadFileOperation,
|
||||
cancelPendingIntent: PendingIntent,
|
||||
startIntent: PendingIntent
|
||||
startIntent: PendingIntent,
|
||||
currentUploadIndex: Int,
|
||||
totalUploadSize: Int
|
||||
) {
|
||||
notificationBuilder.run {
|
||||
setContentTitle(context.getString(R.string.uploader_upload_in_progress_ticker))
|
||||
setContentText(
|
||||
String.format(
|
||||
context.getString(R.string.uploader_upload_in_progress),
|
||||
0,
|
||||
uploadFileOperation.fileName
|
||||
)
|
||||
currentOperationTitle = if (totalUploadSize > 1) {
|
||||
String.format(
|
||||
context.getString(R.string.upload_notification_manager_start_text),
|
||||
currentUploadIndex,
|
||||
totalUploadSize,
|
||||
uploadFileOperation.fileName
|
||||
)
|
||||
setTicker(context.getString(R.string.foreground_service_upload))
|
||||
} else {
|
||||
uploadFileOperation.fileName
|
||||
}
|
||||
|
||||
val progressText = NumberFormatter.getPercentageText(0)
|
||||
|
||||
notificationBuilder.run {
|
||||
setProgress(100, 0, false)
|
||||
setOngoing(true)
|
||||
setContentTitle(currentOperationTitle)
|
||||
setContentText(progressText)
|
||||
setOngoing(false)
|
||||
clearActions()
|
||||
|
||||
addAction(
|
||||
|
|
@ -76,13 +63,27 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun updateUploadProgress(percent: Int, currentOperation: UploadFileOperation?) {
|
||||
val progressText = NumberFormatter.getPercentageText(percent)
|
||||
setProgress(percent, progressText, false)
|
||||
showNotification()
|
||||
dismissOldErrorNotification(currentOperation)
|
||||
}
|
||||
|
||||
fun notifyForFailedResult(
|
||||
uploadFileOperation: UploadFileOperation,
|
||||
resultCode: RemoteOperationResult.ResultCode,
|
||||
conflictsResolveIntent: PendingIntent?,
|
||||
cancelUploadActionIntent: PendingIntent?,
|
||||
credentialIntent: PendingIntent?,
|
||||
errorMessage: String
|
||||
) {
|
||||
val textId = resultTitle(resultCode)
|
||||
if (uploadFileOperation.isMissingPermissionThrown) {
|
||||
return
|
||||
}
|
||||
|
||||
val textId = getFailedResultTitleId(resultCode)
|
||||
|
||||
notificationBuilder.run {
|
||||
setTicker(context.getString(textId))
|
||||
|
|
@ -100,15 +101,29 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi
|
|||
)
|
||||
}
|
||||
|
||||
cancelUploadActionIntent?.let {
|
||||
addAction(
|
||||
R.drawable.ic_delete,
|
||||
R.string.upload_list_cancel_upload,
|
||||
cancelUploadActionIntent
|
||||
)
|
||||
}
|
||||
|
||||
credentialIntent?.let {
|
||||
setContentIntent(it)
|
||||
}
|
||||
|
||||
setContentText(errorMessage)
|
||||
}
|
||||
|
||||
if (resultCode.isFileSpecificError()) {
|
||||
showNewNotification(uploadFileOperation)
|
||||
} else {
|
||||
showNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resultTitle(resultCode: RemoteOperationResult.ResultCode): Int {
|
||||
private fun getFailedResultTitleId(resultCode: RemoteOperationResult.ResultCode): Int {
|
||||
val needsToUpdateCredentials = (resultCode == RemoteOperationResult.ResultCode.UNAUTHORIZED)
|
||||
|
||||
return if (needsToUpdateCredentials) {
|
||||
|
|
@ -128,7 +143,7 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi
|
|||
)
|
||||
}
|
||||
|
||||
fun showNewNotification(operation: UploadFileOperation) {
|
||||
private fun showNewNotification(operation: UploadFileOperation) {
|
||||
notificationManager.notify(
|
||||
NotificationUtils.createUploadNotificationTag(operation.file),
|
||||
FileUploadWorker.NOTIFICATION_ERROR_ID,
|
||||
|
|
@ -136,20 +151,35 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi
|
|||
)
|
||||
}
|
||||
|
||||
private fun showNotification() {
|
||||
notificationManager.notify(ID, notificationBuilder.build())
|
||||
fun showSameFileAlreadyExistsNotification(filename: String) {
|
||||
notificationBuilder.run {
|
||||
setAutoCancel(true)
|
||||
clearActions()
|
||||
setContentText("")
|
||||
setProgress(0, 0, false)
|
||||
setContentTitle(context.getString(R.string.file_upload_worker_same_file_already_exists, filename))
|
||||
}
|
||||
|
||||
val notificationId = filename.hashCode()
|
||||
|
||||
notificationManager.notify(
|
||||
notificationId,
|
||||
notificationBuilder.build()
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun updateUploadProgress(filename: String, percent: Int, currentOperation: UploadFileOperation?) {
|
||||
notificationBuilder.run {
|
||||
setProgress(100, percent, false)
|
||||
val text = String.format(context.getString(R.string.uploader_upload_in_progress), percent, filename)
|
||||
setContentText(text)
|
||||
fun showConnectionErrorNotification() {
|
||||
notificationManager.cancel(getId())
|
||||
|
||||
showNotification()
|
||||
dismissOldErrorNotification(currentOperation)
|
||||
notificationBuilder.run {
|
||||
setContentTitle(context.getString(R.string.file_upload_worker_error_notification_title))
|
||||
setContentText("")
|
||||
}
|
||||
|
||||
notificationManager.notify(
|
||||
FileUploadWorker.NOTIFICATION_ERROR_ID,
|
||||
notificationBuilder.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun dismissOldErrorNotification(operation: UploadFileOperation?) {
|
||||
|
|
@ -164,6 +194,8 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi
|
|||
}
|
||||
}
|
||||
|
||||
fun dismissErrorNotification() = notificationManager.cancel(FileUploadWorker.NOTIFICATION_ERROR_ID)
|
||||
|
||||
fun dismissOldErrorNotification(remotePath: String, localPath: String) {
|
||||
notificationManager.cancel(
|
||||
NotificationUtils.createUploadNotificationTag(remotePath, localPath),
|
||||
|
|
@ -171,15 +203,11 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi
|
|||
)
|
||||
}
|
||||
|
||||
fun dismissWorkerNotifications() {
|
||||
notificationManager.cancel(ID)
|
||||
}
|
||||
|
||||
fun notifyPaused(intent: PendingIntent) {
|
||||
notificationBuilder.apply {
|
||||
notificationBuilder.run {
|
||||
setContentTitle(context.getString(R.string.upload_global_pause_title))
|
||||
setTicker(context.getString(R.string.upload_global_pause_title))
|
||||
setOngoing(true)
|
||||
setOngoing(false)
|
||||
setAutoCancel(false)
|
||||
setProgress(0, 0, false)
|
||||
clearActions()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
|
|
@ -44,16 +44,14 @@ class UploadTask(
|
|||
private val clientProvider: () -> OwnCloudClient,
|
||||
private val fileDataStorageManager: FileDataStorageManager
|
||||
) {
|
||||
fun create(): UploadTask {
|
||||
return UploadTask(
|
||||
applicationContext,
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
powerManagementService,
|
||||
clientProvider,
|
||||
fileDataStorageManager
|
||||
)
|
||||
}
|
||||
fun create(): UploadTask = UploadTask(
|
||||
applicationContext,
|
||||
uploadsStorageManager,
|
||||
connectivityService,
|
||||
powerManagementService,
|
||||
clientProvider,
|
||||
fileDataStorageManager
|
||||
)
|
||||
}
|
||||
|
||||
fun upload(user: User, upload: OCUpload): Result {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs.upload
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue