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
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
android.defaults.buildfeatures.buildconfig=true

android.enableJetifier=true
android.nonFinalResIds=false
android.nonTransitiveRClass=false
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536M
android.overridePathCheck=true
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ ksp = "1.9.20-1.0.14"
ktlint = "11.1.0"
markwon = "4.6.2"
material = "1.8.0"
mockk = "1.13.3"
mockk = "1.13.13"
moshi = "1.15.0"
patternlockview = "a90b0d4bf0"
photoView = "2.3.0"
Expand Down
5 changes: 5 additions & 0 deletions opencloudApp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ android {

buildFeatures {
viewBinding true
buildConfig true
}

packagingOptions {
Expand Down Expand Up @@ -249,3 +250,7 @@ static def getGitOriginRemote() {
def found = values.find { it.startsWith("origin") && it.endsWith("(push)") }
return found.replace("origin", "").replace("(push)", "").replace(".git", "").trim()
}

tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach {
jvmTarget = "17"
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ class AvatarUtils : KoinComponent {
fun loadAvatarForAccount(
imageView: ImageView,
account: Account,
fetchIfNotCached: Boolean = false,
displayRadius: Float
@Suppress("UnusedParameter") fetchIfNotCached: Boolean = false,
@Suppress("UnusedParameter") displayRadius: Float
) {
// Tech debt: Move this to a viewModel and use its viewModelScope instead
CoroutineScope(Dispatchers.IO).launch {
Expand All @@ -76,8 +76,8 @@ class AvatarUtils : KoinComponent {
fun loadAvatarForAccount(
menuItem: MenuItem,
account: Account,
fetchIfNotCached: Boolean = false,
displayRadius: Float
@Suppress("UnusedParameter") fetchIfNotCached: Boolean = false,
@Suppress("UnusedParameter") displayRadius: Float
) {
CoroutineScope(Dispatchers.IO).launch {
val drawable = avatarManager.getAvatarForAccount(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,11 @@ class FileDetailsFragment : FileFragment() {
if (thumbnail == null) {
thumbnail = ThumbnailsCacheManager.mDefaultImg
}
val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(MainApp.appContext.resources, thumbnail, task)
val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(
MainApp.appContext.resources,
thumbnail,
task
)
imageView.setImageDrawable(asyncDrawable)
task.execute(ocFile)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,12 @@ class MainFileListFragment : Fragment(),
"${getString(R.string.actionbar_select_inverse)} $roleAccessibilityDescription"
findItem(R.id.action_open_file_with)?.contentDescription =
"${getString(R.string.actionbar_open_with)} $roleAccessibilityDescription"
findItem(R.id.action_rename_file)?.contentDescription = "${getString(R.string.common_rename)} $roleAccessibilityDescription"
findItem(R.id.action_move)?.contentDescription = "${getString(R.string.actionbar_move)} $roleAccessibilityDescription"
findItem(R.id.action_copy)?.contentDescription = "${getString(R.string.copy)} $roleAccessibilityDescription"
findItem(R.id.action_rename_file)?.contentDescription =
"${getString(R.string.common_rename)} $roleAccessibilityDescription"
findItem(R.id.action_move)?.contentDescription =
"${getString(R.string.actionbar_move)} $roleAccessibilityDescription"
findItem(R.id.action_copy)?.contentDescription =
"${getString(R.string.copy)} $roleAccessibilityDescription"
findItem(R.id.action_send_file)?.contentDescription =
"${getString(R.string.actionbar_send_file)} $roleAccessibilityDescription"
findItem(R.id.action_set_available_offline)?.contentDescription =
Expand All @@ -283,7 +286,8 @@ class MainFileListFragment : Fragment(),
"${getString(R.string.unset_available_offline)} $roleAccessibilityDescription"
findItem(R.id.action_see_details)?.contentDescription =
"${getString(R.string.actionbar_see_details)} $roleAccessibilityDescription"
findItem(R.id.action_remove_file)?.contentDescription = "${getString(R.string.common_remove)} $roleAccessibilityDescription"
findItem(R.id.action_remove_file)?.contentDescription =
"${getString(R.string.common_remove)} $roleAccessibilityDescription"
}
}
}
Expand Down Expand Up @@ -368,7 +372,10 @@ class MainFileListFragment : Fragment(),
// Set view and footer correctly
if (mainFileListViewModel.isGridModeSetAsPreferred()) {
layoutManager =
StaggeredGridLayoutManager(ColumnQuantity(requireContext(), R.layout.grid_item).calculateNoOfColumns(), RecyclerView.VERTICAL)
StaggeredGridLayoutManager(
ColumnQuantity(requireContext(), R.layout.grid_item).calculateNoOfColumns(),
RecyclerView.VERTICAL
)
viewType = ViewType.VIEW_TYPE_GRID
} else {
layoutManager = StaggeredGridLayoutManager(1, RecyclerView.VERTICAL)
Expand Down Expand Up @@ -494,7 +501,9 @@ class MainFileListFragment : Fragment(),
fileActions?.onCurrentFolderUpdated(currentFolderDisplayed, mainFileListViewModel.getSpace())
val fileListOption = mainFileListViewModel.fileListOption.value
val refreshFolderNeeded = fileListOption.isAllFiles() ||
(!fileListOption.isAllFiles() && currentFolderDisplayed.remotePath != ROOT_PATH && !fileListOption.isAvailableOffline())
(!fileListOption.isAllFiles() &&
currentFolderDisplayed.remotePath != ROOT_PATH &&
!fileListOption.isAvailableOffline())
if (refreshFolderNeeded) {
fileOperationsViewModel.performOperation(
FileOperation.RefreshFolderOperation(
Expand Down Expand Up @@ -543,7 +552,8 @@ class MainFileListFragment : Fragment(),
// Mimetypes not supported via open in web, send 500
if (uiResult.error is InstanceNotConfiguredException) {
val message =
getString(R.string.open_in_web_error_generic) + " " + getString(R.string.error_reason) +
getString(R.string.open_in_web_error_generic) + " " +
getString(R.string.error_reason) +
" " + getString(R.string.open_in_web_error_not_supported)
this.showMessageInSnackbar(message, Snackbar.LENGTH_LONG)
} else if (uiResult.error is TooEarlyException) {
Expand Down Expand Up @@ -602,19 +612,27 @@ class MainFileListFragment : Fragment(),
thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_archive)
} else {
// Set file icon depending on its mimetype. Ask for thumbnail later.
thumbnailBottomSheet.setImageResource(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName))
thumbnailBottomSheet.setImageResource(
MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)
)
if (file.remoteId != null) {
val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(file.remoteId)
if (thumbnail != null) {
thumbnailBottomSheet.setImageBitmap(thumbnail)
}
if (file.needsToUpdateThumbnail && ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailBottomSheet)) {
if (file.needsToUpdateThumbnail &&
ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailBottomSheet)
) {
// generate new Thumbnail
val task = ThumbnailsCacheManager.ThumbnailGenerationTask(
thumbnailBottomSheet,
AccountUtils.getCurrentOpenCloudAccount(requireContext())
)
val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(resources, thumbnail, task)
val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(
resources,
thumbnail,
task
)

// If drawable is not visible, do not update it.
if (asyncDrawable.minimumHeight > 0 && asyncDrawable.minimumWidth > 0) {
Expand All @@ -624,7 +642,9 @@ class MainFileListFragment : Fragment(),
}

if (file.mimeType == "image/png") {
thumbnailBottomSheet.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.background_color))
thumbnailBottomSheet.setBackgroundColor(
ContextCompat.getColor(requireContext(), R.color.background_color)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ class ShareFileFragment : Fragment(), ShareUserListAdapter.ShareUserAdapterListe
R.string.share_via_link_default_name_template,
file?.fileName
)
val defaultNameNumberedRegex = QUOTE_START + defaultName + QUOTE_END + DEFAULT_NAME_REGEX_SUFFIX
val defaultNameNumberedRegex =
QUOTE_START + defaultName + QUOTE_END + DEFAULT_NAME_REGEX_SUFFIX
val usedNumbers = ArrayList<Int>()
var isDefaultNameSet = false
var number: String
Expand Down Expand Up @@ -217,7 +218,8 @@ class ShareFileFragment : Fragment(), ShareUserListAdapter.ShareUserAdapterListe
_binding = ShareFileLayoutBinding.inflate(inflater, container, false)
return binding.root.apply {
// Allow or disallow touches with other visible windows
filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context)
filterTouchesWhenObscured =
PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,17 @@ object ThumbnailsRequester : KoinComponent {
.build()
}

fun getPreviewUriForSpaceSpecial(spaceSpecial: SpaceSpecial): String {
// Converts dp to pixel
val spacesThumbnailSize = appContext.resources.getDimension(R.dimen.spaces_thumbnail_height).roundToInt()
return String.format(
fun getPreviewUriForSpaceSpecial(spaceSpecial: SpaceSpecial): String =
String.format(
Locale.ROOT,
SPACE_SPECIAL_PREVIEW_URI,
spaceSpecial.webDavUrl,
spacesThumbnailSize,
spacesThumbnailSize,
appContext.resources.getDimension(R.dimen.spaces_thumbnail_height).roundToInt(),
appContext.resources.getDimension(R.dimen.spaces_thumbnail_height).roundToInt(),
spaceSpecial.eTag
)
}

@Suppress("ExpressionBodySyntax")
fun getPreviewUriForFile(ocFile: OCFileWithSyncInfo, account: Account): String {
var baseUrl = getOpenCloudClient().baseUri.toString() + "/remote.php/dav/files/" + account.name.split("@".toRegex())
.dropLastWhile { it.isEmpty() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import eu.opencloud.android.domain.transfers.TransferRepository
import eu.opencloud.android.extensions.getWorkInfoByTags
import eu.opencloud.android.workers.UploadFileFromContentUriWorker
import timber.log.Timber
import java.io.File

class RetryUploadFromContentUriUseCase(
private val workManager: WorkManager,
Expand All @@ -52,11 +53,18 @@ class RetryUploadFromContentUriUseCase(
if (workInfos.isEmpty() || workInfos.firstOrNull()?.state == WorkInfo.State.FAILED) {
transferRepository.updateTransferStatusToEnqueuedById(params.uploadIdInStorageManager)

val lastModifiedInSeconds = File(uploadToRetry.localPath)
.takeIf { it.exists() && it.isFile }
?.lastModified()
?.takeIf { it > 0 }
?.div(1000)
?.toString()

uploadFileFromContentUriUseCase(
UploadFileFromContentUriUseCase.Params(
accountName = uploadToRetry.accountName,
contentUri = uploadToRetry.localPath.toUri(),
lastModifiedInSeconds = (uploadToRetry.transferEndTimestamp?.div(1000)).toString(),
lastModifiedInSeconds = lastModifiedInSeconds,
behavior = uploadToRetry.localBehaviour.name,
uploadPath = uploadToRetry.remotePath,
uploadIdInStorageManager = params.uploadIdInStorageManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import eu.opencloud.android.domain.transfers.TransferRepository
import eu.opencloud.android.extensions.getWorkInfoByTags
import eu.opencloud.android.workers.UploadFileFromFileSystemWorker
import timber.log.Timber
import java.io.File

class RetryUploadFromSystemUseCase(
private val workManager: WorkManager,
Expand All @@ -52,11 +53,18 @@ class RetryUploadFromSystemUseCase(
if (workInfos.isEmpty() || workInfos.firstOrNull()?.state == WorkInfo.State.FAILED) {
transferRepository.updateTransferStatusToEnqueuedById(params.uploadIdInStorageManager)

val lastModifiedInSeconds = File(uploadToRetry.localPath)
.takeIf { it.exists() && it.isFile }
?.lastModified()
?.takeIf { it > 0 }
?.div(1000)
?.toString()

uploadFileFromSystemUseCase(
UploadFileFromSystemUseCase.Params(
accountName = uploadToRetry.accountName,
localPath = uploadToRetry.localPath,
lastModifiedInSeconds = (uploadToRetry.transferEndTimestamp?.div(1000)).toString(),
lastModifiedInSeconds = lastModifiedInSeconds,
behavior = uploadToRetry.localBehaviour.name,
uploadPath = uploadToRetry.remotePath,
sourcePath = uploadToRetry.sourcePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ package eu.opencloud.android.usecases.transfers.uploads

import android.net.Uri
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import eu.opencloud.android.domain.BaseUseCase
import eu.opencloud.android.domain.automaticuploads.model.UploadBehavior
import eu.opencloud.android.workers.RemoveSourceFileWorker
Expand All @@ -39,17 +40,21 @@ class UploadFileFromContentUriUseCase(
) : BaseUseCase<Unit, UploadFileFromContentUriUseCase.Params>() {

override fun run(params: Params) {
val inputDataUploadFileFromContentUriWorker = workDataOf(
UploadFileFromContentUriWorker.KEY_PARAM_ACCOUNT_NAME to params.accountName,
UploadFileFromContentUriWorker.KEY_PARAM_BEHAVIOR to params.behavior,
UploadFileFromContentUriWorker.KEY_PARAM_CONTENT_URI to params.contentUri.toString(),
UploadFileFromContentUriWorker.KEY_PARAM_LAST_MODIFIED to params.lastModifiedInSeconds,
UploadFileFromContentUriWorker.KEY_PARAM_UPLOAD_PATH to params.uploadPath,
UploadFileFromContentUriWorker.KEY_PARAM_UPLOAD_ID to params.uploadIdInStorageManager
)
val inputDataRemoveSourceFileWorker = workDataOf(
UploadFileFromContentUriWorker.KEY_PARAM_CONTENT_URI to params.contentUri.toString(),
)
val inputDataUploadFileFromContentUriWorker = Data.Builder()
.putString(UploadFileFromContentUriWorker.KEY_PARAM_ACCOUNT_NAME, params.accountName)
.putString(UploadFileFromContentUriWorker.KEY_PARAM_BEHAVIOR, params.behavior)
.putString(UploadFileFromContentUriWorker.KEY_PARAM_CONTENT_URI, params.contentUri.toString())
.putString(UploadFileFromContentUriWorker.KEY_PARAM_UPLOAD_PATH, params.uploadPath)
.putLong(UploadFileFromContentUriWorker.KEY_PARAM_UPLOAD_ID, params.uploadIdInStorageManager)
.apply {
params.lastModifiedInSeconds?.let {
putString(UploadFileFromContentUriWorker.KEY_PARAM_LAST_MODIFIED, it)
}
}
.build()
val inputDataRemoveSourceFileWorker = Data.Builder()
.putString(UploadFileFromContentUriWorker.KEY_PARAM_CONTENT_URI, params.contentUri.toString())
.build()

val networkRequired = if (params.wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED
val constraints = Constraints.Builder()
Expand All @@ -64,25 +69,35 @@ class UploadFileFromContentUriUseCase(
.addTag(params.uploadIdInStorageManager.toString())
.build()

// Use unique work name based on upload ID to prevent concurrent uploads of same file
val uniqueWorkName = "upload_content_uri_${params.uploadIdInStorageManager}"

val behavior = UploadBehavior.fromString(params.behavior)
if (behavior == UploadBehavior.MOVE) {
val removeSourceFileWorker = OneTimeWorkRequestBuilder<RemoveSourceFileWorker>()
.setInputData(inputDataRemoveSourceFileWorker)
.build()
workManager.beginWith(uploadFileFromContentUriWorker)
.then(removeSourceFileWorker) // File is already uploaded, so the original one can be removed if the behaviour is MOVE
.enqueue()
workManager.beginUniqueWork(
uniqueWorkName,
ExistingWorkPolicy.KEEP, // Keep existing work to prevent duplicate uploads
uploadFileFromContentUriWorker
).then(removeSourceFileWorker) // File is already uploaded, so the original one can be removed if the behaviour is MOVE
.enqueue()
} else {
workManager.enqueue(uploadFileFromContentUriWorker)
workManager.enqueueUniqueWork(
uniqueWorkName,
ExistingWorkPolicy.KEEP, // Keep existing work to prevent duplicate uploads
uploadFileFromContentUriWorker
)
}

Timber.i("Plain upload of ${params.contentUri.path} has been enqueued.")
Timber.i("Plain upload of ${params.contentUri.path} has been enqueued with unique work name: $uniqueWorkName")
}

data class Params(
val accountName: String,
val contentUri: Uri,
val lastModifiedInSeconds: String,
val lastModifiedInSeconds: String?,
val behavior: String,
val uploadPath: String,
val uploadIdInStorageManager: Long,
Expand Down
Loading
Loading