Skip to content
Open
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 @@ -36,6 +36,7 @@ import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.datamodel.ArbitraryDataProvider
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.datamodel.UploadsStorageManager
import com.owncloud.android.operations.factory.UploadFileOperationFactory
import com.owncloud.android.utils.theme.ViewThemeUtils
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
Expand Down Expand Up @@ -66,7 +67,8 @@ class BackgroundJobFactory @Inject constructor(
private val localBroadcastManager: Provider<LocalBroadcastManager>,
private val generatePdfUseCase: GeneratePDFUseCase,
private val syncedFolderProvider: SyncedFolderProvider,
private val database: NextcloudDatabase
private val database: NextcloudDatabase,
private val uploadFileOperationFactory: UploadFileOperationFactory
) : WorkerFactory() {

@SuppressLint("NewApi")
Expand Down Expand Up @@ -247,6 +249,7 @@ class BackgroundJobFactory @Inject constructor(
FileSystemRepository(dao = database.fileSystemDao(), uploadsStorageManager, context),
syncedFolderProvider,
context,
uploadFileOperationFactory,
params
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.client.notifications.AppWideNotificationManager
import com.nextcloud.utils.extensions.checkWCFRestrictions
import com.nextcloud.utils.extensions.getUploadIds
import com.nextcloud.utils.extensions.isAnonymous
import com.nextcloud.utils.extensions.isLastResultConflictError
import com.nextcloud.utils.extensions.isSame
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.datamodel.FileDataStorageManager
Expand All @@ -36,6 +38,7 @@ 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.OwnCloudClientFactory
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
Expand All @@ -45,8 +48,9 @@ import com.owncloud.android.lib.resources.files.model.ServerFileInterface
import com.owncloud.android.lib.resources.status.OCCapability
import com.owncloud.android.operations.RemoveFileOperation
import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.ui.adapter.uploadList.helper.ConflictHandlingResult
import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterActionHandler
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.FileUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -167,25 +171,36 @@ class FileUploadHelper {
}

@Suppress("ComplexCondition")
private fun retryUploads(
private suspend fun retryUploads(
uploadsStorageManager: UploadsStorageManager,
connectivityService: ConnectivityService,
accountManager: UserAccountManager,
powerManagementService: PowerManagementService,
uploads: List<OCUpload>
): Boolean {
): Boolean = withContext(Dispatchers.IO) {
var showNotExistMessage = false
var showSyncConflictNotification = false
var conflictHandlingResult: ConflictHandlingResult? = null
val isOnline = checkConnectivity(connectivityService)
val connectivity = connectivityService.connectivity
val batteryStatus = powerManagementService.battery

val uploadsToRetry = mutableListOf<Long>()

val currentAccount = accountManager.currentAccount
val context = MainApp.getAppContext()
var ownCloudClient: OwnCloudClient? = null
if (!currentAccount.isAnonymous(context)) {
ownCloudClient =
OwnCloudClientFactory.createOwnCloudClient(accountManager.currentAccount, MainApp.getAppContext())
}
val uploadActionHandler = UploadListAdapterActionHandler()

for (upload in uploads) {
if (upload.isLastResultConflictError()) {
Log_OC.d(TAG, "retry upload skipped, sync conflict: ${upload.remotePath}")
showSyncConflictNotification = true
ownCloudClient?.let {
conflictHandlingResult =
uploadActionHandler.handleConflict(upload, ownCloudClient, uploadsStorageManager)
}
continue
}

Expand Down Expand Up @@ -226,11 +241,12 @@ class FileUploadHelper {
)
}

if (showSyncConflictNotification) {
if (conflictHandlingResult is ConflictHandlingResult.ShowConflictResolveDialog) {
Log_OC.d(TAG, "retry upload skipped, sync conflict: ${conflictHandlingResult.file.remotePath}")
AppWideNotificationManager.showSyncConflictNotification(MainApp.getAppContext())
}

return showNotExistMessage
return@withContext showNotExistMessage
}

@JvmOverloads
Expand Down Expand Up @@ -563,25 +579,17 @@ class FileUploadHelper {
}

@Suppress("MagicNumber", "ReturnCount", "ComplexCondition")
fun isSameFileOnRemote(user: User?, localFile: File?, remotePath: String?, context: Context?): Boolean {
if (user == null || localFile == null || remotePath == null || context == null) {
fun isSameFileOnRemote(user: User?, localPath: String?, remotePath: String?, context: Context?): Boolean {
if (user == null || localPath == null || remotePath == null || context == null) {
Log_OC.e(TAG, "cannot compare remote and local file")
return false
}

// Compare remote file to local file
val localLastModifiedTimestamp = localFile.lastModified() / 1000 // remote file timestamp in milli not micro sec
val localCreationTimestamp = FileUtil.getCreationTimestamp(localFile)
val localSize: Long = localFile.length()

val operation = ReadFileRemoteOperation(remotePath)
val result: RemoteOperationResult<*> = operation.execute(user, context)
if (result.isSuccess) {
val remoteFile = result.data[0] as RemoteFile
return remoteFile.size == localSize &&
localCreationTimestamp != null &&
localCreationTimestamp == remoteFile.creationTimestamp &&
remoteFile.modifiedTimestamp == localLastModifiedTimestamp * 1000
return remoteFile.isSame(localPath)
}
return false
}
Expand Down Expand Up @@ -648,7 +656,6 @@ class FileUploadHelper {
files: List<OCFile>,
accountName: String
): Pair<List<SyncedFolderEntity>, List<OCFile>> {

val autoUploadFolders = mutableListOf<SyncedFolderEntity>()
val nonAutoUploadFiles = mutableListOf<OCFile>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import com.nextcloud.utils.extensions.getPercent
import com.nextcloud.utils.extensions.toFile
import com.nextcloud.utils.extensions.updateStatus
import com.owncloud.android.R
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.ForegroundServiceType
import com.owncloud.android.datamodel.SyncedFolder
import com.owncloud.android.datamodel.SyncedFolderProvider
Expand All @@ -44,6 +43,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.operations.factory.UploadFileOperationFactory
import com.owncloud.android.ui.notifications.NotificationUtils
import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlinx.coroutines.Dispatchers
Expand All @@ -66,6 +66,7 @@ class FileUploadWorker(
val filesystemRepository: FileSystemRepository,
val syncedFolderProvider: SyncedFolderProvider,
val context: Context,
val uploadFileOperationFactory: UploadFileOperationFactory,
params: WorkerParameters
) : CoroutineWorker(context, params),
OnDatatransferProgressListener {
Expand Down Expand Up @@ -270,7 +271,7 @@ class FileUploadWorker(
}

fileUploadEventBroadcaster.sendUploadEnqueued(context)
val operation = createUploadFileOperation(upload, user)
val operation = uploadFileOperationFactory.create(upload, this@FileUploadWorker)
activeOperations[upload.uploadId] = operation

val currentIndex = (index + 1)
Expand Down Expand Up @@ -348,24 +349,6 @@ class FileUploadWorker(
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 suspend fun upload(
upload: OCUpload,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import com.owncloud.android.ui.activity.ConflictsResolveActivity
import com.owncloud.android.utils.ErrorMessageAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File

object UploadErrorNotificationManager {
private const val TAG = "UploadErrorNotificationManager"
Expand Down Expand Up @@ -76,7 +75,7 @@ object UploadErrorNotificationManager {
val isSameFile = withContext(Dispatchers.IO) {
FileUploadHelper.instance().isSameFileOnRemote(
operation.user,
File(operation.storagePath),
operation.storagePath,
operation.remotePath,
context
)
Expand Down
42 changes: 4 additions & 38 deletions app/src/main/java/com/nextcloud/utils/OCFileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import android.graphics.drawable.BitmapDrawable
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.drawable.toDrawable
import androidx.exifinterface.media.ExifInterface
import com.nextcloud.utils.extensions.getBitmapSize
import com.nextcloud.utils.extensions.getExifSize
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
Expand Down Expand Up @@ -41,8 +42,8 @@ object OCFileUtils {
// Local file
val path = ocFile.storagePath
if (!path.isNullOrEmpty() && ocFile.exists()) {
getExifSize(path)?.let { return it }
getBitmapSize(path)?.let { return it }
path.getExifSize()?.let { return it }
path.getBitmapSize()?.let { return it }
}

// 3 Fallback
Expand All @@ -55,41 +56,6 @@ object OCFileUtils {
return fallbackPair
}

private fun getExifSize(path: String): Pair<Int, Int>? = try {
val exif = ExifInterface(path)
var w = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0)
var h = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)

val orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
if (orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
orientation == ExifInterface.ORIENTATION_ROTATE_270
) {
val tmp = w
w = h
h = tmp
}

Log_OC.d(TAG, "Using exif imageDimension: $w x $h")
if (w > 0 && h > 0) w to h else null
} catch (_: Exception) {
null
}

private fun getBitmapSize(path: String): Pair<Int, Int>? = try {
val options = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true }
android.graphics.BitmapFactory.decodeFile(path, options)
val w = options.outWidth
val h = options.outHeight

Log_OC.d(TAG, "Using bitmap factory imageDimension: $w x $h")
if (w > 0 && h > 0) w to h else null
} catch (_: Exception) {
null
}

fun getMediaPlaceholder(file: OCFile, imageDimension: Pair<Int, Int>): BitmapDrawable {
val context = MainApp.getAppContext()

Expand Down
36 changes: 36 additions & 0 deletions app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package com.nextcloud.utils.extensions

import androidx.exifinterface.media.ExifInterface
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.utils.DisplayUtils
Expand Down Expand Up @@ -50,3 +51,38 @@ fun String.toFile(): File? {

return file
}

fun String.getExifSize(): Pair<Int, Int>? = try {
val exif = ExifInterface(this)
var w = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0)
var h = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)

val orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
if (orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
orientation == ExifInterface.ORIENTATION_ROTATE_270
) {
val tmp = w
w = h
h = tmp
}

Log_OC.d(TAG, "Using exif imageDimension: $w x $h")
if (w > 0 && h > 0) w to h else null
} catch (_: Exception) {
null
}

fun String.getBitmapSize(): Pair<Int, Int>? = try {
val options = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true }
android.graphics.BitmapFactory.decodeFile(this, options)
val w = options.outWidth
val h = options.outHeight

Log_OC.d(TAG, "Using bitmap factory imageDimension: $w x $h")
if (w > 0 && h > 0) w to h else null
} catch (_: Exception) {
null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.extensions

import com.nextcloud.utils.TimeConstants
import com.owncloud.android.lib.resources.files.model.RemoteFile
import com.owncloud.android.utils.FileUtil
import com.owncloud.android.utils.MimeTypeUtil

fun RemoteFile.isSame(path: String?): Boolean {
val localFile = path?.toFile() ?: return false

// remote file timestamp in millisecond not microsecond
val localLastModifiedTimestamp = localFile.lastModified() / TimeConstants.MILLIS_PER_SECOND
val localCreationTimestamp = FileUtil.getCreationTimestamp(localFile)
val localSize: Long = localFile.length()

return size == localSize &&
localCreationTimestamp != null &&
localCreationTimestamp == creationTimestamp &&
modifiedTimestamp == localLastModifiedTimestamp * TimeConstants.MILLIS_PER_SECOND &&
this.areImageDimensionsSame(path)
}

@Suppress("ReturnCount")
private fun RemoteFile.areImageDimensionsSame(path: String): Boolean {
if (!MimeTypeUtil.isImage(mimeType)) {
// can't compare it's not image
return true
}

val localFileImageDimension = path.getExifSize() ?: path.getBitmapSize()
if (localFileImageDimension == null) {
// can't compare local file image dimension is not determined
return true
}

return localFileImageDimension.first.toFloat() == imageDimension?.width &&
localFileImageDimension.second.toFloat() == imageDimension?.height
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ fun UploadResult.getFailedStatusText(context: Context): String = when (this) {

UploadResult.OLD_ANDROID_API -> context.getString(R.string.upload_old_android)

UploadResult.SYNC_CONFLICT -> context.getString(R.string.upload_sync_conflict)
UploadResult.SYNC_CONFLICT -> context.getString(R.string.upload_sync_conflict_check)

UploadResult.CANNOT_CREATE_FILE -> context.getString(R.string.upload_cannot_create_file)

Expand Down
Loading
Loading