Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import com.nextcloud.client.database.entity.OfflineOperationEntity

@Dao
interface OfflineOperationDao {
@Query("SELECT * FROM offline_operations")
@Query("SELECT * FROM offline_operations ORDER BY _id ASC")
fun getAll(): List<OfflineOperationEntity>

@Query("SELECT * FROM offline_operations WHERE _id = :id LIMIT 1")
fun getById(id: Int): OfflineOperationEntity?

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg entity: OfflineOperationEntity)

Expand All @@ -38,6 +41,15 @@ interface OfflineOperationDao {
@Query("SELECT * FROM offline_operations WHERE offline_operations_parent_oc_file_id = :parentOCFileId")
fun getSubEntitiesByParentOCFileId(parentOCFileId: Long): List<OfflineOperationEntity>

@Query(
"SELECT * FROM offline_operations WHERE offline_operations_path != :folderPath AND " +
"substr(offline_operations_path, 1, length(:folderPath)) = :folderPath"
)
fun getAllInsideFolder(folderPath: String): List<OfflineOperationEntity>

@Query("SELECT * FROM offline_operations WHERE offline_operations_path = :path")
fun getAllByPath(path: String): List<OfflineOperationEntity>

@Query("DELETE FROM offline_operations")
fun clearTable()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.logger.Logger
import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.repository.ClientRepository
import com.owncloud.android.datamodel.ArbitraryDataProvider
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.datamodel.UploadsStorageManager
Expand Down Expand Up @@ -71,7 +72,8 @@ class BackgroundJobFactory @Inject constructor(
private val generatePdfUseCase: GeneratePDFUseCase,
private val syncedFolderProvider: SyncedFolderProvider,
private val database: NextcloudDatabase,
private val uploadFileOperationFactory: UploadFileOperationFactory
private val uploadFileOperationFactory: UploadFileOperationFactory,
private val clientRepository: ClientRepository
) : WorkerFactory() {

@SuppressLint("NewApi")
Expand Down Expand Up @@ -123,6 +125,7 @@ class BackgroundJobFactory @Inject constructor(
accountManager.user,
context,
connectivityService,
clientRepository,
viewThemeUtils.get(),
params
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class OfflineOperationsNotificationManager(private val context: Context, viewThe
context,
id,
intent,
PendingIntent.FLAG_IMMUTABLE
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

return NotificationCompat.Action(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ 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.repository.ClientRepository
import com.nextcloud.utils.extensions.isNetworkAndServerAvailableSuspended
import com.nextcloud.utils.extensions.isTheSameAs
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.common.OwnCloudClient
Expand All @@ -31,18 +32,21 @@ import com.owncloud.android.operations.CreateFolderOperation
import com.owncloud.android.operations.RemoveFileOperation
import com.owncloud.android.operations.RenameFileOperation
import com.owncloud.android.utils.FileStorageUtils
import com.owncloud.android.utils.FileUtil
import com.owncloud.android.utils.MimeTypeUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.nextcloud.utils.AppConstants.MILLIS_PER_SECOND
import java.io.File

private typealias OfflineOperationResult = Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>?

class OfflineOperationsWorker(
private val user: User,
private val context: Context,
private val connectivityService: ConnectivityService,
private val clientRepository: ClientRepository,
viewThemeUtils: ViewThemeUtils,
params: WorkerParameters
) : CoroutineWorker(context, params) {
Expand All @@ -53,7 +57,6 @@ class OfflineOperationsWorker(
}

private val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
private val clientFactory = ClientFactoryImpl(context)
private val notificationManager = OfflineOperationsNotificationManager(context, viewThemeUtils)
private var repository = OfflineOperationsRepository(fileDataStorageManager)

Expand All @@ -76,8 +79,13 @@ class OfflineOperationsWorker(
}

// process offline operations
val client = clientRepository.getOwncloudClient()
if (client == null) {
Log_OC.e(TAG, "💥 Client is not available, operations are not processed")
return@withContext Result.failure()
}

notificationManager.start()
val client = clientFactory.create(user)
processOperations(operations, client)

Log_OC.d(TAG, "🏁 Worker finished with result")
Expand All @@ -93,11 +101,17 @@ class OfflineOperationsWorker(
// region Handle offline operations
private suspend fun processOperations(operations: List<OfflineOperationEntity>, client: OwnCloudClient) {
val totalOperationSize = operations.size
operations.forEachIndexed { index, operation ->
operations.forEachIndexed { index, snapshot ->
val operation = snapshot.id?.let { fileDataStorageManager.offlineOperationDao.getById(it) }
if (operation == null) {
Log_OC.d(TAG, "Skipping, operation was removed during this run, id=${snapshot.id}")
return@forEachIndexed
}

try {
Log_OC.d(TAG, "Processing operation, path: ${operation.path}")
val result = executeOperation(operation, client)
handleResult(operation, totalOperationSize, index, result)
handleResult(operation, totalOperationSize, index, result, client)
} catch (e: Exception) {
Log_OC.e(TAG, "💥 Exception while processing operation id=${operation.id}: ${e.message}")
}
Expand All @@ -108,14 +122,15 @@ class OfflineOperationsWorker(
operation: OfflineOperationEntity,
totalOperations: Int,
currentSuccessfulOperationIndex: Int,
result: OfflineOperationResult
result: OfflineOperationResult,
client: OwnCloudClient
) {
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)
handleSuccessResult(operation, totalOperations, currentSuccessfulOperationIndex, client)
} else {
handleErrorResult(operation.id, result)
}
Expand All @@ -124,21 +139,67 @@ class OfflineOperationsWorker(
private fun handleSuccessResult(
operation: OfflineOperationEntity,
totalOperations: Int,
currentSuccessfulOperationIndex: Int
currentSuccessfulOperationIndex: Int,
client: OwnCloudClient
) {
if (operation.type is OfflineOperationType.RemoveFile) {
val operationType = operation.type as OfflineOperationType.RemoveFile
fileDataStorageManager.getFileByDecryptedRemotePath(operationType.path)?.let { ocFile ->
repository.deleteOperation(ocFile)
val operationType = operation.type
if (operationType is OfflineOperationType.RemoveFile) {
if (operationType.path.endsWith(OCFile.PATH_SEPARATOR)) {
repository.deleteOperationsInsideFolder(operationType.path)
.forEach { notificationManager.dismissNotification(it) }
}
} else {
if (operation.isCreate()) {
getExecutionPath(operation)?.let { getRemoteFile(it, client) }?.let { saveRemoteFile(it) }
}
repository.updateNextOperations(operation)
}

fileDataStorageManager.offlineOperationDao.delete(operation)
deleteOperation(operation)
notificationManager.update(totalOperations, currentSuccessfulOperationIndex + 1, operation.filename ?: "")
}

private fun deleteOperation(operation: OfflineOperationEntity) {
fileDataStorageManager.offlineOperationDao.delete(operation)
notificationManager.dismissNotification(operation.id)
}

private fun handleExistingRemoteFile(
operation: OfflineOperationEntity,
localPath: String,
remoteFile: RemoteFile,
ocFile: OCFile?
) {
val serverFile = FileStorageUtils.fillOCFile(remoteFile)
if (serverFile.isTheSameAs(File(localPath))) {
Log_OC.d(TAG, "Skipping, same file already exists on server: ${operation.path}")
saveRemoteFile(remoteFile)
deleteOperation(operation)
return
}

Log_OC.w(TAG, "⚠️ Conflict detected: different file already exists on server, operation id=${operation.id}")
ocFile?.let {
serverFile.fileId = it.fileId
serverFile.parentId = it.parentId
}
notificationManager.showConflictResolveNotification(serverFile, operation)
}

private fun saveRemoteFile(remoteFile: RemoteFile) {
val remotePath = remoteFile.remotePath ?: return
val parentPath = FileStorageUtils.getParentPath(remotePath) ?: return
val parent = fileDataStorageManager.getFileByDecryptedRemotePath(parentPath) ?: return
val existingFile = fileDataStorageManager.getFileByDecryptedRemotePath(remotePath)

val createdFile = FileStorageUtils.fillOCFile(remoteFile).apply {
existingFile?.let { fileId = it.fileId }
parentId = parent.fileId
}

fileDataStorageManager.saveFile(createdFile)
}

private fun handleErrorResult(id: Int?, result: OfflineOperationResult) {
val operationResult = result?.first ?: return
val operation = result.second ?: return
Expand Down Expand Up @@ -176,8 +237,7 @@ class OfflineOperationsWorker(
}

repository.updateNextOperations(operation)
fileDataStorageManager.offlineOperationDao.delete(operation)
notificationManager.dismissNotification(operation.id)
deleteOperation(operation)
}

// region Operation Execution
Expand All @@ -192,7 +252,7 @@ class OfflineOperationsWorker(
return@withContext null
}

val remoteFile = getRemoteFile(path)
val remoteFile = getRemoteFile(path, client)
val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(path)

if (operation.type is OfflineOperationType.CreateFolder && remoteFile != null) {
Expand All @@ -201,6 +261,12 @@ class OfflineOperationsWorker(
return@withContext null
}

val operationType = operation.type
if (operationType is OfflineOperationType.CreateFile && remoteFile != null) {
handleExistingRemoteFile(operation, operationType.localPath, remoteFile, ocFile)
return@withContext null
}

if (remoteFile != null && ocFile != null && isFileChanged(remoteFile, ocFile)) {
Log_OC.w(TAG, "⚠️ Conflict detected: File already exists on server. Skipping operation id=${operation.id}")

Expand All @@ -218,13 +284,7 @@ class OfflineOperationsWorker(

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)
deleteOperation(operation)
return@withContext null
}

Expand Down Expand Up @@ -266,14 +326,14 @@ class OfflineOperationsWorker(
@Suppress("DEPRECATION")
private fun createFile(operation: OfflineOperationEntity, client: OwnCloudClient): OfflineOperationResult {
val operationType = (operation.type as OfflineOperationType.CreateFile)
val lastModificationDate = System.currentTimeMillis() / MILLIS_PER_SECOND
val localFile = File(operationType.localPath)
val createFileOperation = UploadFileRemoteOperation(
operationType.localPath,
operationType.remotePath,
operationType.mimeType,
"",
operation.modifiedAt ?: lastModificationDate,
operation.createdAt ?: System.currentTimeMillis(),
localFile.lastModified() / MILLIS_PER_SECOND,
FileUtil.getCreationTimestamp(localFile),
true
)
return createFileOperation.execute(client) to createFileOperation
Expand All @@ -294,10 +354,9 @@ class OfflineOperationsWorker(
// endregion

@Suppress("DEPRECATION")
private fun getRemoteFile(remotePath: String): RemoteFile? {
private fun getRemoteFile(remotePath: String, client: OwnCloudClient): 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsNotificationManager
import com.nextcloud.model.OfflineOperationType
import com.owncloud.android.MainApp
import com.owncloud.android.datamodel.FileDataStorageManager
import javax.inject.Inject
Expand All @@ -32,8 +33,16 @@ class OfflineOperationReceiver : BroadcastReceiver() {
return
}

val operation = storageManager.offlineOperationDao.getById(id)
storageManager.offlineOperationDao.deleteById(id)

val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val folderPath = operation?.path?.takeIf { operation.type is OfflineOperationType.CreateFolder }
folderPath?.let { path ->
storageManager.offlineOperationsRepository.deleteOperationsInsideFolder(path)
.forEach { notificationManager.cancel(it) }
}

notificationManager.cancel(
OfflineOperationsNotificationManager.ERROR_ID
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,30 @@ class OfflineOperationsRepository(private val fileDataStorageManager: FileDataSt
.mapNotNull { nextOperation ->
nextOperation.parentOCFileId?.let { parentId ->
fileDataStorageManager.getFileById(parentId)?.let { ocFile ->
ocFile.decryptedRemotePath?.let { updatedPath ->
val newPath = updatedPath + nextOperation.filename + pathSeparator
updateOperationPath(newPath, nextOperation)
}
ocFile.decryptedRemotePath
?.let { parentPath -> nextOperation.getPathInside(parentPath) }
?.let { newPath -> updateOperationPath(newPath, nextOperation) }
}
}
}
.forEach { dao.update(it) }
}

private fun OfflineOperationEntity.getPathInside(parentPath: String): String? {
val currentPath = path ?: return null
val isFolder = currentPath.endsWith(pathSeparator)
val name = currentPath.removeSuffix(pathSeparator.toString()).substringAfterLast(pathSeparator)
val suffix = if (isFolder) pathSeparator.toString() else ""
return parentPath + name + suffix
}

private fun updateOperationPath(newPath: String, nextOperation: OfflineOperationEntity): OfflineOperationEntity? {
if (newPath == nextOperation.path) return null

val updatedType = when (val currentType = nextOperation.type) {
is OfflineOperationType.CreateFile -> currentType.copy(remotePath = newPath)
is OfflineOperationType.CreateFolder -> currentType.copy(path = newPath)
is OfflineOperationType.RemoveFile -> currentType.copy(path = newPath)
else -> currentType
}

Expand All @@ -96,6 +104,12 @@ class OfflineOperationsRepository(private val fileDataStorageManager: FileDataSt
}
}

override fun deleteOperationsInsideFolder(folderPath: String): List<Int> {
val operations = dao.getAllInsideFolder(folderPath)
operations.forEach { dao.delete(it) }
return operations.mapNotNull { it.id }
}

override fun updateOperationForKeepBoth(operation: OfflineOperationEntity, newPath: String) {
updateOperationPath(newPath, operation)
dao.update(operation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface OfflineOperationsRepositoryType {
fun getAllSubEntities(fileId: Long): List<OfflineOperationEntity>
fun deleteOperation(file: OCFile)
fun updateNextOperations(operation: OfflineOperationEntity)
fun deleteOperationsInsideFolder(folderPath: String): List<Int>
fun updateOperationForKeepBoth(operation: OfflineOperationEntity, newPath: String)
fun convertToOCFiles(fileId: Long): List<OCFile>
}
Loading
Loading