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 @@ -25,10 +25,10 @@ import com.owncloud.android.R
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.db.OCUpload
import com.owncloud.android.ui.dialog.ConflictsResolveDialog
import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Companion.newInstance
import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision
import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener
import com.owncloud.android.ui.dialog.conflict.ConflictsResolveDialog
import com.owncloud.android.ui.dialog.conflict.ConflictsResolveDialog.Decision
import com.owncloud.android.ui.dialog.conflict.ConflictsResolveDialog.OnConflictDecisionMadeListener
import com.owncloud.android.ui.dialog.conflict.ConflictResolveDialogFactory
import com.owncloud.android.utils.EspressoIdlingResource
import com.owncloud.android.utils.FileStorageUtils
import com.owncloud.android.utils.ScreenshotTest
Expand Down Expand Up @@ -69,7 +69,7 @@ class ConflictsResolveActivityIT : AbstractIT() {
launchActivity<ConflictsResolveActivity>(intent).use { scenario ->
var dialog: ConflictsResolveDialog? = null
scenario.onActivity { sut ->
dialog = newInstance(
dialog = ConflictResolveDialogFactory.forNormal(
storageManager.getDecryptedPath(existingFile),
targetContext,
newFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment;
import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
import com.owncloud.android.ui.dialog.ConflictsResolveDialog;
import com.owncloud.android.ui.dialog.conflict.ConflictsResolveDialog;
import com.owncloud.android.ui.dialog.CreateAlbumDialogFragment;
import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ import com.owncloud.android.files.services.NameCollisionPolicy
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.ui.dialog.ConflictsResolveDialog
import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision
import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener
import com.owncloud.android.ui.dialog.conflict.ConflictsResolveDialog.Decision
import com.owncloud.android.ui.dialog.conflict.ConflictsResolveDialog.OnConflictDecisionMadeListener
import com.owncloud.android.ui.dialog.conflict.ConflictResolveDialogFactory
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.FileStorageUtils
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -273,7 +273,7 @@ class ConflictsResolveActivity :
}

val (ft, _) = prepareDialogTransaction()
ConflictsResolveDialog.newInstance(
ConflictResolveDialogFactory.forOffline(
context = this,
leftFile = offlineOperation,
rightFile = newFile!!
Expand Down Expand Up @@ -307,7 +307,7 @@ class ConflictsResolveActivity :
private fun showFileConflictDialog(remotePath: String) {
val (ft, user) = prepareDialogTransaction()
if (existingFile != null && storageManager.fileExists(remotePath) && newFile != null) {
ConflictsResolveDialog.newInstance(
ConflictResolveDialogFactory.forNormal(
title = storageManager.getDecryptedPath(existingFile!!),
context = this,
leftFile = newFile!!,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.owncloud.android.ui.dialog.conflict

import android.content.Context
import android.os.Bundle
import androidx.annotation.StringRes
import com.nextcloud.client.account.User
import com.nextcloud.client.database.entity.OfflineOperationEntity
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.dialog.conflict.model.ConflictDialogData
import com.owncloud.android.ui.dialog.conflict.model.ConflictDialogType
import com.owncloud.android.ui.dialog.conflict.model.ConflictFileData
import com.owncloud.android.utils.DisplayUtils
import java.io.File

object ConflictResolveDialogFactory {

private const val SECONDS_TO_MILLIS = 1000L
private const val UNKNOWN_FOLDER_SIZE = 0L

fun forOffline(context: Context, leftFile: OfflineOperationEntity, rightFile: OCFile): ConflictsResolveDialog {
val data = ConflictDialogData(
headline = context.getString(R.string.conflict_folder_headline),
description = context.getString(R.string.conflict_message_description_for_folder),
localFile = context.conflictFileData(
titleId = R.string.prefs_synced_folders_local_path_title,
timestamp = (leftFile.createdAt ?: 0L) * SECONDS_TO_MILLIS,
fileLength = UNKNOWN_FOLDER_SIZE
),
serverFile = context.conflictFileData(R.string.prefs_synced_folders_remote_path_title, rightFile)
)

return createDialog(ConflictDialogType.Offline(data)) {
putParcelable(ConflictsResolveDialog.ARG_RIGHT_FILE, rightFile)
}
}

fun forNormal(
title: String,
context: Context,
leftFile: OCFile,
rightFile: OCFile,
user: User?
): ConflictsResolveDialog {
val localFile = File(leftFile.storagePath)
val data = ConflictDialogData(
headline = context.getString(R.string.choose_which_file),
description = context.getString(R.string.conflict_message_description),
localFile = context.conflictFileData(
titleId = R.string.conflict_local_file,
timestamp = localFile.lastModified(),
fileLength = localFile.length()
),
serverFile = context.conflictFileData(R.string.conflict_server_file, rightFile)
)

return createDialog(ConflictDialogType.Normal(title, data)) {
putSerializable(ConflictsResolveDialog.ARG_LEFT_FILE, localFile)
putParcelable(ConflictsResolveDialog.ARG_RIGHT_FILE, rightFile)
putParcelable(ConflictsResolveDialog.ARG_USER, user)
}
}

private fun Context.conflictFileData(@StringRes titleId: Int, file: OCFile): ConflictFileData =
conflictFileData(titleId, file.modificationTimestamp, file.fileLength)

private fun Context.conflictFileData(@StringRes titleId: Int, timestamp: Long, fileLength: Long): ConflictFileData =
ConflictFileData(
title = getString(titleId),
timestamp = DisplayUtils.getRelativeTimestamp(this, timestamp).toString(),
fileSize = DisplayUtils.bytesToHumanReadable(fileLength)
)

private fun createDialog(type: ConflictDialogType, putFiles: Bundle.() -> Unit): ConflictsResolveDialog =
ConflictsResolveDialog().apply {
arguments = Bundle().apply {
putParcelable(ConflictsResolveDialog.ARG_CONFLICT_DATA, type)
putFiles()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.ui.dialog
package com.owncloud.android.ui.dialog.conflict

import android.app.Dialog
import android.content.Context
Expand All @@ -17,7 +17,6 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.client.account.User
import com.nextcloud.client.database.entity.OfflineOperationEntity
import com.nextcloud.client.di.Injectable
import com.nextcloud.utils.extensions.getParcelableArgument
import com.nextcloud.utils.extensions.getSerializableArgument
Expand All @@ -28,8 +27,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.ui.adapter.localFileList.LocalFileThumbnailBinder
import com.owncloud.android.ui.dialog.parcel.ConflictDialogData
import com.owncloud.android.ui.dialog.parcel.ConflictFileData
import com.owncloud.android.ui.dialog.conflict.model.ConflictDialogType
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.MimeTypeUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
Expand All @@ -47,7 +45,7 @@ class ConflictsResolveDialog :
var listener: OnConflictDecisionMadeListener? = null
private var positiveButton: MaterialButton? = null

private var data: ConflictDialogData? = null
private lateinit var dialogType: ConflictDialogType
private var user: User? = null
private var leftDataFile: File? = null
private var rightDataFile: OCFile? = null
Expand Down Expand Up @@ -110,25 +108,13 @@ class ConflictsResolveDialog :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val bundle = savedInstanceState ?: arguments

if (bundle != null) {
data = bundle.getParcelableArgument(ARG_CONFLICT_DATA, ConflictDialogData::class.java)
leftDataFile = bundle.getSerializableArgument(ARG_LEFT_FILE, File::class.java)
rightDataFile = bundle.getParcelableArgument(ARG_RIGHT_FILE, OCFile::class.java)
user = bundle.getParcelableArgument(ARG_USER, User::class.java)
} else {
activity?.let {
DisplayUtils.showSnackMessage(it, R.string.failed_to_create_conflict_dialog)
}
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.run {
putParcelable(ARG_CONFLICT_DATA, data)
val bundle = requireArguments()
dialogType = requireNotNull(bundle.getParcelableArgument(ARG_CONFLICT_DATA, ConflictDialogType::class.java)) {
"$ARG_CONFLICT_DATA is required, create this dialog through ConflictResolveDialogFactory"
}
leftDataFile = bundle.getSerializableArgument(ARG_LEFT_FILE, File::class.java)
rightDataFile = bundle.getParcelableArgument(ARG_RIGHT_FILE, OCFile::class.java)
user = bundle.getParcelableArgument(ARG_USER, User::class.java)
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
Expand Down Expand Up @@ -156,20 +142,20 @@ class ConflictsResolveDialog :
.setNegativeButton(R.string.common_cancel) { _: DialogInterface?, _: Int ->
listener?.conflictDecisionMade(Decision.CANCEL)
}
.setTitle(data?.dialogTitle)
.setTitle(dialogType.dialogTitle)

private fun okButtonClick() {
binding.run {
val isDialogTitleNotExists = (data?.dialogTitle == null)
val isDialogTypeOffline = (dialogType is ConflictDialogType.Offline)
val decision = when {
leftCheckbox.isChecked && rightCheckbox.isChecked ->
if (isDialogTitleNotExists) Decision.KEEP_BOTH_FOLDER else Decision.KEEP_BOTH
if (isDialogTypeOffline) Decision.KEEP_BOTH_FOLDER else Decision.KEEP_BOTH

leftCheckbox.isChecked ->
if (isDialogTitleNotExists) Decision.KEEP_OFFLINE_FOLDER else Decision.KEEP_LOCAL
if (isDialogTypeOffline) Decision.KEEP_OFFLINE_FOLDER else Decision.KEEP_LOCAL

rightCheckbox.isChecked ->
if (isDialogTitleNotExists) Decision.KEEP_SERVER_FOLDER else Decision.KEEP_SERVER
if (isDialogTypeOffline) Decision.KEEP_SERVER_FOLDER else Decision.KEEP_SERVER

else -> null
}
Expand All @@ -179,34 +165,26 @@ class ConflictsResolveDialog :
}

private fun setupUI() {
val data = dialogType.data

binding.run {
data?.let {
val (leftData, rightData) = it.checkboxData

headline.visibility = if (it.headline == null) {
View.GONE
} else {
View.VISIBLE
}
headline.text = it.headline

description.text = it.description

leftCheckbox.text = leftData.title
leftTimestamp.text = leftData.timestamp
leftFileSize.text = leftData.fileSize

rightCheckbox.text = rightData.title
rightTimestamp.text = rightData.timestamp
rightFileSize.text = rightData.fileSize

if (leftDataFile != null && rightDataFile != null && user != null) {
setThumbnailsForFileConflicts()
} else {
val folderIcon = MimeTypeUtil.getDefaultFolderIcon(requireContext(), viewThemeUtils)
leftThumbnail.setImageDrawable(folderIcon)
rightThumbnail.setImageDrawable(folderIcon)
}
headline.text = data.headline
description.text = data.description

leftCheckbox.text = data.localFile.title
leftTimestamp.text = data.localFile.timestamp
leftFileSize.text = data.localFile.fileSize

rightCheckbox.text = data.serverFile.title
rightTimestamp.text = data.serverFile.timestamp
rightFileSize.text = data.serverFile.fileSize

if (leftDataFile != null && rightDataFile != null && user != null) {
setThumbnailsForFileConflicts()
} else {
val folderIcon = MimeTypeUtil.getDefaultFolderIcon(requireContext(), viewThemeUtils)
leftThumbnail.setImageDrawable(folderIcon)
rightThumbnail.setImageDrawable(folderIcon)
}
}
}
Expand Down Expand Up @@ -269,88 +247,9 @@ class ConflictsResolveDialog :
}

companion object {
private const val ARG_CONFLICT_DATA = "CONFLICT_DATA"
private const val ARG_LEFT_FILE = "LEFT_FILE"
private const val ARG_RIGHT_FILE = "RIGHT_FILE"
private const val ARG_USER = "USER"

@JvmStatic
fun newInstance(
title: String,
context: Context,
leftFile: OCFile,
rightFile: OCFile,
user: User?
): ConflictsResolveDialog {
val file = File(leftFile.storagePath)
val conflictData = getFileConflictData(title, file, rightFile, context)

val bundle = Bundle().apply {
putParcelable(ARG_CONFLICT_DATA, conflictData)
putSerializable(ARG_LEFT_FILE, file)
putParcelable(ARG_RIGHT_FILE, rightFile)
putParcelable(ARG_USER, user)
}

return ConflictsResolveDialog().apply {
arguments = bundle
}
}

fun newInstance(context: Context, leftFile: OfflineOperationEntity, rightFile: OCFile): ConflictsResolveDialog {
val conflictData = getFolderConflictData(leftFile, rightFile, context)

val bundle = Bundle().apply {
putParcelable(ARG_CONFLICT_DATA, conflictData)
putParcelable(ARG_RIGHT_FILE, rightFile)
}

return ConflictsResolveDialog().apply {
arguments = bundle
}
}

@Suppress("MagicNumber")
private fun getFolderConflictData(
offlineOperation: OfflineOperationEntity,
rightFile: OCFile,
context: Context
): ConflictDialogData {
val leftTitle = context.getString(R.string.prefs_synced_folders_local_path_title)
val leftTimestamp =
DisplayUtils.getRelativeTimestamp(context, offlineOperation.createdAt?.times(1000L) ?: 0)
val leftFileSize = DisplayUtils.bytesToHumanReadable(0)
val leftCheckBoxData = ConflictFileData(leftTitle, leftTimestamp.toString(), leftFileSize)

val rightTitle = context.getString(R.string.prefs_synced_folders_remote_path_title)
val rightTimestamp = DisplayUtils.getRelativeTimestamp(context, rightFile.modificationTimestamp)
val rightFileSize = DisplayUtils.bytesToHumanReadable(rightFile.fileLength)
val rightCheckBoxData = ConflictFileData(rightTitle, rightTimestamp.toString(), rightFileSize)

val title = context.getString(R.string.conflict_folder_headline)
val description = context.getString(R.string.conflict_message_description_for_folder)
return ConflictDialogData(null, title, description, Pair(leftCheckBoxData, rightCheckBoxData))
}

private fun getFileConflictData(
title: String,
leftFile: File,
rightFile: OCFile,
context: Context
): ConflictDialogData {
val leftTitle = context.getString(R.string.conflict_local_file)
val leftTimestamp = DisplayUtils.getRelativeTimestamp(context, leftFile.lastModified())
val leftFileSize = DisplayUtils.bytesToHumanReadable(leftFile.length())
val leftCheckBoxData = ConflictFileData(leftTitle, leftTimestamp.toString(), leftFileSize)

val rightTitle = context.getString(R.string.conflict_server_file)
val rightTimestamp = DisplayUtils.getRelativeTimestamp(context, rightFile.modificationTimestamp)
val rightFileSize = DisplayUtils.bytesToHumanReadable(rightFile.fileLength)
val rightCheckBoxData = ConflictFileData(rightTitle, rightTimestamp.toString(), rightFileSize)

val headline = context.getString(R.string.choose_which_file)
val description = context.getString(R.string.conflict_message_description)
return ConflictDialogData(title, headline, description, Pair(leftCheckBoxData, rightCheckBoxData))
}
internal const val ARG_CONFLICT_DATA = "CONFLICT_DATA"
internal const val ARG_LEFT_FILE = "LEFT_FILE"
internal const val ARG_RIGHT_FILE = "RIGHT_FILE"
internal const val ARG_USER = "USER"
}
}
Loading
Loading