added DEV version to repo

This commit is contained in:
Fr4nz D13trich 2025-09-18 18:43:03 +02:00
parent 1ef725ef20
commit 23e673bfdf
2135 changed files with 97033 additions and 21206 deletions

View file

@ -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)
}
}